From 1178e51b18f97c9f66925e5d37eb3856631a441a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 18 Sep 2024 12:10:08 +0200 Subject: [PATCH 01/38] server: add a WIP middleware example --- toolkit/server/context.go | 28 +++++++++++++ toolkit/server/middleware.go | 80 ++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 toolkit/server/context.go create mode 100644 toolkit/server/middleware.go diff --git a/toolkit/server/context.go b/toolkit/server/context.go new file mode 100644 index 0000000..83fe096 --- /dev/null +++ b/toolkit/server/context.go @@ -0,0 +1,28 @@ +package server + +import "context" + +type contextKey string + +var ctxUserId = contextKey("userId") +var ctxProjectId = contextKey("projectId") + +// ContextGetUserId return the UserId stored in the context, if it exists. +func ContextGetUserId(ctx context.Context) (string, bool) { + val, ok := ctx.Value(ctxUserId).(string) + return val, ok +} + +func addUserIdToContext(ctx context.Context, userId string) context.Context { + return context.WithValue(ctx, ctxUserId, userId) +} + +// ContextGetProjectId return the ProjectID stored in the context, if it exists. +func ContextGetProjectId(ctx context.Context) (string, bool) { + val, ok := ctx.Value(ctxProjectId).(string) + return val, ok +} + +func addProjectIdToContext(ctx context.Context, projectId string) context.Context { + return context.WithValue(ctx, ctxProjectId, projectId) +} diff --git a/toolkit/server/middleware.go b/toolkit/server/middleware.go new file mode 100644 index 0000000..a9f2fd1 --- /dev/null +++ b/toolkit/server/middleware.go @@ -0,0 +1,80 @@ +package server + +import ( + "encoding/base64" + "io" + "net/http" + "strings" + + "github.com/ucan-wg/go-ucan/delegation" + "github.com/ucan-wg/go-ucan/did" +) + +type Middleware func(http.Handler) http.Handler + +func ExampleMiddleware(serviceDID did.DID) Middleware { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + tokenReader, ok := extractBearerToken(r) + if !ok { + http.Error(w, "missing or malformed UCAN HTTP Bearer token", http.StatusUnauthorized) + return + } + + // decode + // TODO: ultimately, this token should be a container with one invocation and 1+ delegations. + // We are doing something simpler for now. + dlg, err := delegation.FromDagCborReader(tokenReader) + if err != nil { + http.Error(w, "malformed UCAN delegation", http.StatusBadRequest) + return + } + + // optional: http-bearer + + // validate + if dlg.Subject() != serviceDID { + http.Error(w, "invalid UCAN delegation", http.StatusBadRequest) + return + } + // TODO: policies check + inject in context + + // extract values + userId, err := dlg.Meta().GetString("userId") + if err != nil { + http.Error(w, "missing or malformed userId", http.StatusBadRequest) + return + } + projectId, err := dlg.Meta().GetString("projectId") + if err != nil { + http.Error(w, "missing or malformed projectId", http.StatusBadRequest) + return + } + + // inject into context + ctx = addUserIdToContext(ctx, userId) + ctx = addProjectIdToContext(ctx, projectId) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func extractBearerToken(r *http.Request) (io.Reader, bool) { + header := r.Header.Get("Authorization") + if header == "" { + return nil, false + } + + if !strings.HasPrefix(header, "Bearer ") { + return nil, false + } + + // skip prefix + reader := strings.NewReader(header[len("Bearer "):]) + + // base64 decode + return base64.NewDecoder(base64.StdEncoding, reader), true +} From 83f3e4c3b0f8726fd36e438ba045ec350619271d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 8 Oct 2024 12:05:49 +0200 Subject: [PATCH 02/38] server: add a readme --- toolkit/server/Readme.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 toolkit/server/Readme.md diff --git a/toolkit/server/Readme.md b/toolkit/server/Readme.md new file mode 100644 index 0000000..34f8d59 --- /dev/null +++ b/toolkit/server/Readme.md @@ -0,0 +1,6 @@ +# HTTP Server middleware example + +This package shows an example of an HTTP middleware with the following features: +- decoding a UCAN delegation token from the `Authorization: Bearer` HTTP Header, according to the PriorityConnect Step0 way (which is not the fully correct UCAN way) +- minimal verifications, no policies or args +- retrieval of values passed as token metadata, and insertion in the go context \ No newline at end of file From 4c254565836414ce5ffc47f0968b72ba372893c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 8 Oct 2024 12:07:53 +0200 Subject: [PATCH 03/38] server: update deps --- toolkit/server/middleware.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/server/middleware.go b/toolkit/server/middleware.go index a9f2fd1..a956d2d 100644 --- a/toolkit/server/middleware.go +++ b/toolkit/server/middleware.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - "github.com/ucan-wg/go-ucan/delegation" "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/token/delegation" ) type Middleware func(http.Handler) http.Handler From 6f4853cd2f1f4fd13f44f9dbc82f07e9459bbce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 27 Nov 2024 12:23:56 +0100 Subject: [PATCH 04/38] bearer,context: support hash verification of the computed args --- toolkit/server/context.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/toolkit/server/context.go b/toolkit/server/context.go index 83fe096..701d203 100644 --- a/toolkit/server/context.go +++ b/toolkit/server/context.go @@ -2,27 +2,26 @@ package server import "context" -type contextKey string - -var ctxUserId = contextKey("userId") -var ctxProjectId = contextKey("projectId") +type ctxUserIdKey struct{} // ContextGetUserId return the UserId stored in the context, if it exists. func ContextGetUserId(ctx context.Context) (string, bool) { - val, ok := ctx.Value(ctxUserId).(string) + val, ok := ctx.Value(ctxUserIdKey{}).(string) return val, ok } func addUserIdToContext(ctx context.Context, userId string) context.Context { - return context.WithValue(ctx, ctxUserId, userId) + return context.WithValue(ctx, ctxUserIdKey{}, userId) } +type ctxProjectIdKey struct{} + // ContextGetProjectId return the ProjectID stored in the context, if it exists. func ContextGetProjectId(ctx context.Context) (string, bool) { - val, ok := ctx.Value(ctxProjectId).(string) + val, ok := ctx.Value(ctxProjectIdKey{}).(string) return val, ok } func addProjectIdToContext(ctx context.Context, projectId string) context.Context { - return context.WithValue(ctx, ctxProjectId, projectId) + return context.WithValue(ctx, ctxProjectIdKey{}, projectId) } From 6c1602507bfc3128efd1d6ac3994956c2ca78c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 4 Dec 2024 19:59:41 +0100 Subject: [PATCH 05/38] client: add a pool that can find a prood for an invocation --- toolkit/client/pool.go | 128 ++++++++++++++++++++++++++++++++++++ toolkit/client/pool_test.go | 29 ++++++++ 2 files changed, 157 insertions(+) create mode 100644 toolkit/client/pool.go create mode 100644 toolkit/client/pool_test.go diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go new file mode 100644 index 0000000..8d8a64f --- /dev/null +++ b/toolkit/client/pool.go @@ -0,0 +1,128 @@ +package client + +import ( + "math" + "time" + + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type Pool struct { + dlgs map[cid.Cid]*delegation.Bundle +} + +func NewPool() *Pool { + return &Pool{dlgs: make(map[cid.Cid]*delegation.Bundle)} +} + +func (p *Pool) AddBundle(bundle *delegation.Bundle) { + p.dlgs[bundle.Cid] = bundle +} + +// FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. +// - issuer: the DID of the client, also the issuer of the invocation token +// - audience: the DID of the resource to operate on, also the audience (or subject if defined) of the invocation token +// - cmd: the command to execute +// - args: the args to execute +// Note: the implemented algorithm won't perform well with a large number of delegations. +func (p *Pool) FindProof(iss did.DID, aud did.DID, cmd command.Command) []cid.Cid { + p.trim() + + // Find the possible leaf delegations, directly matching the invocation parameters + var candidateLeaf []*delegation.Bundle + + for _, bundle := range p.dlgs { + dlg := bundle.Decoded + + // The Subject of each delegation must equal the invocation's Audience field. - 4f + if dlg.Subject() != aud { + continue + } + // 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 { + continue + } + // The command of each delegation must "allow" the one before it. - 4g + if !dlg.Command().Covers(cmd) { + continue + } + // Time bound - 3b, 3c + if !dlg.IsValidNow() { + continue + } + + // graph.WriteString("[*] --> " + bundle.Cid.String() + "\n") + candidateLeaf = append(candidateLeaf, bundle) + } + + type state struct { + bundle *delegation.Bundle + path []cid.Cid + size int + } + + var bestSize = math.MaxInt + var bestProof []cid.Cid + + // Perform a depth-first search on the DAG of connected delegations, for each of our candidates + for _, leaf := range candidateLeaf { + var stack = []state{{bundle: leaf, path: []cid.Cid{leaf.Cid}, size: len(leaf.Sealed)}} + + for len(stack) > 0 { + // dequeue a delegation + cur := stack[len(stack)-1] + stack = stack[:len(stack)-1] + at := cur.bundle + + // if it's a root delegation, we found a valid proof + if at.Decoded.Issuer() == at.Decoded.Subject() { + if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize { + bestProof = append([]cid.Cid{}, cur.path...) // make a copy + bestSize = cur.size + continue + } + } + + // find parent delegation for our current delegation + for _, candidate := range p.dlgs { + // The Subject of each delegation must equal the invocation's Audience field. - 4f + if candidate.Decoded.Subject() != aud { + continue + } + // 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 candidate.Decoded.Audience() != at.Decoded.Issuer() { + continue + } + // The command of each delegation must "allow" the one before it. - 4g + if !candidate.Decoded.Command().Covers(at.Decoded.Command()) { + continue + } + // Time bound - 3b, 3c + if !candidate.Decoded.IsValidNow() { + continue + } + + newPath := append([]cid.Cid{}, cur.path...) // make copy + newPath = append(newPath, candidate.Cid) + stack = append(stack, state{bundle: candidate, path: newPath, size: cur.size + len(candidate.Sealed)}) + } + } + } + + return bestProof +} + +// trim removes expired tokens +func (p *Pool) trim() { + now := time.Now() + for c, bundle := range p.dlgs { + if bundle.Decoded.Expiration() != nil && bundle.Decoded.Expiration().Before(now) { + delete(p.dlgs, c) + } + } +} diff --git a/toolkit/client/pool_test.go b/toolkit/client/pool_test.go new file mode 100644 index 0000000..a8f756b --- /dev/null +++ b/toolkit/client/pool_test.go @@ -0,0 +1,29 @@ +package client + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" +) + +func TestFindProof(t *testing.T) { + p := NewPool() + + for _, bundle := range delegationtest.AllBundles { + p.AddBundle(bundle) + } + + require.Equal(t, delegationtest.ProofAliceBob, + p.FindProof(didtest.PersonaBob.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarol, + p.FindProof(didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarolDan, + p.FindProof(didtest.PersonaDan.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, + p.FindProof(didtest.PersonaErin.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, + p.FindProof(didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + +} From 547416e60daafe1496c5736566ee627960126b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 5 Dec 2024 16:43:20 +0100 Subject: [PATCH 06/38] client: improve/modularize --- toolkit/client/pool.go | 111 +++++++++--------------------------- toolkit/client/pool_test.go | 13 +++-- toolkit/client/proof.go | 103 +++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 90 deletions(-) create mode 100644 toolkit/client/proof.go diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go index 8d8a64f..6926eb7 100644 --- a/toolkit/client/pool.go +++ b/toolkit/client/pool.go @@ -1,7 +1,8 @@ package client import ( - "math" + "iter" + "sync" "time" "github.com/ipfs/go-cid" @@ -11,6 +12,7 @@ import ( ) type Pool struct { + mu sync.RWMutex dlgs map[cid.Cid]*delegation.Bundle } @@ -19,106 +21,45 @@ func NewPool() *Pool { } func (p *Pool) AddBundle(bundle *delegation.Bundle) { + p.mu.Lock() + defer p.mu.Unlock() p.dlgs[bundle.Cid] = bundle } +func (p *Pool) AddBundles(bundles iter.Seq[*delegation.Bundle]) { + for bundle := range bundles { + p.AddBundle(bundle) + } +} + // FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. -// - issuer: the DID of the client, also the issuer of the invocation token -// - audience: the DID of the resource to operate on, also the audience (or subject if defined) of the invocation token // - cmd: the command to execute -// - args: the args to execute +// - issuer: the DID of the client, also the issuer of the invocation token +// - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. -func (p *Pool) FindProof(iss did.DID, aud did.DID, cmd command.Command) []cid.Cid { +func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { p.trim() - // Find the possible leaf delegations, directly matching the invocation parameters - var candidateLeaf []*delegation.Bundle + p.mu.RLock() + defer p.mu.RUnlock() - for _, bundle := range p.dlgs { - dlg := bundle.Decoded - - // The Subject of each delegation must equal the invocation's Audience field. - 4f - if dlg.Subject() != aud { - continue - } - // 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 { - continue - } - // The command of each delegation must "allow" the one before it. - 4g - if !dlg.Command().Covers(cmd) { - continue - } - // Time bound - 3b, 3c - if !dlg.IsValidNow() { - continue - } - - // graph.WriteString("[*] --> " + bundle.Cid.String() + "\n") - candidateLeaf = append(candidateLeaf, bundle) - } - - type state struct { - bundle *delegation.Bundle - path []cid.Cid - size int - } - - var bestSize = math.MaxInt - var bestProof []cid.Cid - - // Perform a depth-first search on the DAG of connected delegations, for each of our candidates - for _, leaf := range candidateLeaf { - var stack = []state{{bundle: leaf, path: []cid.Cid{leaf.Cid}, size: len(leaf.Sealed)}} - - for len(stack) > 0 { - // dequeue a delegation - cur := stack[len(stack)-1] - stack = stack[:len(stack)-1] - at := cur.bundle - - // if it's a root delegation, we found a valid proof - if at.Decoded.Issuer() == at.Decoded.Subject() { - if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize { - bestProof = append([]cid.Cid{}, cur.path...) // make a copy - bestSize = cur.size - continue + return FindProof(func() iter.Seq[*delegation.Bundle] { + return func(yield func(*delegation.Bundle) bool) { + for _, bundle := range p.dlgs { + if !yield(bundle) { + return } } - - // find parent delegation for our current delegation - for _, candidate := range p.dlgs { - // The Subject of each delegation must equal the invocation's Audience field. - 4f - if candidate.Decoded.Subject() != aud { - continue - } - // 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 candidate.Decoded.Audience() != at.Decoded.Issuer() { - continue - } - // The command of each delegation must "allow" the one before it. - 4g - if !candidate.Decoded.Command().Covers(at.Decoded.Command()) { - continue - } - // Time bound - 3b, 3c - if !candidate.Decoded.IsValidNow() { - continue - } - - newPath := append([]cid.Cid{}, cur.path...) // make copy - newPath = append(newPath, candidate.Cid) - stack = append(stack, state{bundle: candidate, path: newPath, size: cur.size + len(candidate.Sealed)}) - } } - } - - return bestProof + }, cmd, iss, aud) } // trim removes expired tokens func (p *Pool) trim() { + p.mu.Lock() + defer p.mu.Unlock() + now := time.Now() for c, bundle := range p.dlgs { if bundle.Decoded.Expiration() != nil && bundle.Decoded.Expiration().Before(now) { diff --git a/toolkit/client/pool_test.go b/toolkit/client/pool_test.go index a8f756b..5ad0d74 100644 --- a/toolkit/client/pool_test.go +++ b/toolkit/client/pool_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" ) @@ -16,14 +17,16 @@ func TestFindProof(t *testing.T) { } require.Equal(t, delegationtest.ProofAliceBob, - p.FindProof(didtest.PersonaBob.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + p.FindProof(delegationtest.NominalCommand, didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarol, - p.FindProof(didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + p.FindProof(delegationtest.NominalCommand, didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDan, - p.FindProof(didtest.PersonaDan.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + p.FindProof(delegationtest.NominalCommand, didtest.PersonaDan.DID(), didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, - p.FindProof(didtest.PersonaErin.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + p.FindProof(delegationtest.NominalCommand, didtest.PersonaErin.DID(), didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, - p.FindProof(didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID(), delegationtest.NominalCommand)) + p.FindProof(delegationtest.NominalCommand, didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID())) + // wrong command + require.Empty(t, p.FindProof(command.New("foo"), didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) } diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go new file mode 100644 index 0000000..e296581 --- /dev/null +++ b/toolkit/client/proof.go @@ -0,0 +1,103 @@ +package client + +import ( + "iter" + "math" + + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +// FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. +// - cmd: the command to execute +// - issuer: the DID of the client, also the issuer of the invocation token +// - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. +// Note: the implemented algorithm won't perform well with a large number of delegations. +func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { + // Find the possible leaf delegations, directly matching the invocation parameters + var candidateLeaf []*delegation.Bundle + + for bundle := range dlgs() { + dlg := bundle.Decoded + + // The Subject of each delegation must equal the invocation's Audience field. - 4f + if dlg.Subject() != aud { + continue + } + // 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 { + continue + } + // The command of each delegation must "allow" the one before it. - 4g + if !dlg.Command().Covers(cmd) { + continue + } + // Time bound - 3b, 3c + if !dlg.IsValidNow() { + continue + } + + candidateLeaf = append(candidateLeaf, bundle) + } + + type state struct { + bundle *delegation.Bundle + path []cid.Cid + size int + } + + var bestSize = math.MaxInt + var bestProof []cid.Cid + + // Perform a depth-first search on the DAG of connected delegations, for each of our candidates + for _, leaf := range candidateLeaf { + var stack = []state{{bundle: leaf, path: []cid.Cid{leaf.Cid}, size: len(leaf.Sealed)}} + + for len(stack) > 0 { + // dequeue a delegation + cur := stack[len(stack)-1] + stack = stack[:len(stack)-1] + at := cur.bundle + + // if it's a root delegation, we found a valid proof + if at.Decoded.Issuer() == at.Decoded.Subject() { + if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize { + bestProof = append([]cid.Cid{}, cur.path...) // make a copy + bestSize = cur.size + continue + } + } + + // find parent delegation for our current delegation + for candidate := range dlgs() { + // The Subject of each delegation must equal the invocation's Audience field. - 4f + if candidate.Decoded.Subject() != aud { + continue + } + // 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 candidate.Decoded.Audience() != at.Decoded.Issuer() { + continue + } + // The command of each delegation must "allow" the one before it. - 4g + if !candidate.Decoded.Command().Covers(at.Decoded.Command()) { + continue + } + // Time bound - 3b, 3c + if !candidate.Decoded.IsValidNow() { + continue + } + + newPath := append([]cid.Cid{}, cur.path...) // make a copy + newPath = append(newPath, candidate.Cid) + stack = append(stack, state{bundle: candidate, path: newPath, size: cur.size + len(candidate.Sealed)}) + } + } + } + + return bestProof +} From 4f4331b6775f9d87227fbd133f06d9f05221d2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 5 Dec 2024 17:11:18 +0100 Subject: [PATCH 07/38] client: move FindProof testing directly where the code is --- toolkit/client/pool_test.go | 32 -------------------------------- toolkit/client/proof_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 32 deletions(-) delete mode 100644 toolkit/client/pool_test.go create mode 100644 toolkit/client/proof_test.go diff --git a/toolkit/client/pool_test.go b/toolkit/client/pool_test.go deleted file mode 100644 index 5ad0d74..0000000 --- a/toolkit/client/pool_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "testing" - - "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/did/didtest" - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" -) - -func TestFindProof(t *testing.T) { - p := NewPool() - - for _, bundle := range delegationtest.AllBundles { - p.AddBundle(bundle) - } - - require.Equal(t, delegationtest.ProofAliceBob, - p.FindProof(delegationtest.NominalCommand, didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) - require.Equal(t, delegationtest.ProofAliceBobCarol, - p.FindProof(delegationtest.NominalCommand, didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID())) - require.Equal(t, delegationtest.ProofAliceBobCarolDan, - p.FindProof(delegationtest.NominalCommand, didtest.PersonaDan.DID(), didtest.PersonaAlice.DID())) - require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, - p.FindProof(delegationtest.NominalCommand, didtest.PersonaErin.DID(), didtest.PersonaAlice.DID())) - require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, - p.FindProof(delegationtest.NominalCommand, didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID())) - - // wrong command - require.Empty(t, p.FindProof(command.New("foo"), didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) -} diff --git a/toolkit/client/proof_test.go b/toolkit/client/proof_test.go new file mode 100644 index 0000000..cce0d55 --- /dev/null +++ b/toolkit/client/proof_test.go @@ -0,0 +1,33 @@ +package client + +import ( + "iter" + "slices" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" +) + +func TestFindProof(t *testing.T) { + dlgs := func() iter.Seq[*delegation.Bundle] { + return slices.Values(delegationtest.AllBundles) + } + + require.Equal(t, delegationtest.ProofAliceBob, + FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarol, + FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarolDan, + FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaDan.DID(), didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, + FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaErin.DID(), didtest.PersonaAlice.DID())) + require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, + FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID())) + + // wrong command + require.Empty(t, FindProof(dlgs, command.New("foo"), didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) +} From 4167bf44bd164d9886dafb3c8c4a0601659f715c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 28 Nov 2024 14:26:27 +0100 Subject: [PATCH 08/38] reorg packages, remove outdated server example --- toolkit/server/Readme.md | 6 - toolkit/server/bearer/Readme.md | 66 +++++++++ toolkit/server/bearer/http.go | 160 ++++++++++++++++++++++ toolkit/server/bearer/http_test.go | 185 ++++++++++++++++++++++++++ toolkit/server/bearer/jsonrpc.go | 168 +++++++++++++++++++++++ toolkit/server/bearer/jsonrpc_test.go | 171 ++++++++++++++++++++++++ toolkit/server/context.go | 27 ---- toolkit/server/exectx/ctxvalue.go | 16 +++ toolkit/server/exectx/ucanctx.go | 146 ++++++++++++++++++++ toolkit/server/exectx/ucanctx_test.go | 162 ++++++++++++++++++++++ toolkit/server/middleware.go | 80 ----------- 11 files changed, 1074 insertions(+), 113 deletions(-) delete mode 100644 toolkit/server/Readme.md create mode 100644 toolkit/server/bearer/Readme.md create mode 100644 toolkit/server/bearer/http.go create mode 100644 toolkit/server/bearer/http_test.go create mode 100644 toolkit/server/bearer/jsonrpc.go create mode 100644 toolkit/server/bearer/jsonrpc_test.go delete mode 100644 toolkit/server/context.go create mode 100644 toolkit/server/exectx/ctxvalue.go create mode 100644 toolkit/server/exectx/ucanctx.go create mode 100644 toolkit/server/exectx/ucanctx_test.go delete mode 100644 toolkit/server/middleware.go diff --git a/toolkit/server/Readme.md b/toolkit/server/Readme.md deleted file mode 100644 index 34f8d59..0000000 --- a/toolkit/server/Readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# HTTP Server middleware example - -This package shows an example of an HTTP middleware with the following features: -- decoding a UCAN delegation token from the `Authorization: Bearer` HTTP Header, according to the PriorityConnect Step0 way (which is not the fully correct UCAN way) -- minimal verifications, no policies or args -- retrieval of values passed as token metadata, and insertion in the go context \ No newline at end of file diff --git a/toolkit/server/bearer/Readme.md b/toolkit/server/bearer/Readme.md new file mode 100644 index 0000000..9875e33 --- /dev/null +++ b/toolkit/server/bearer/Readme.md @@ -0,0 +1,66 @@ +## Motivations + +UCAN is normally a pure RPC construct, when the entirety of the request's parameters are part of the invocation, in the form of `args`. Those `args` are evaluated against the delegation's [policy](https://github.com/ucan-wg/delegation/tree/v1_ipld?tab=readme-ov-file#policy) to determine if the request is allowed or not, then the request handling happens purely based on those args and the `command`. In that setup, the service would have a single entry point. + +Unfortunately, we live in a world of REST APIs, or JSON-RPC. Some adaptations or concessions need to be made. + +In this package, we cross the chasm of the pure UCAN world into our practical needs. This can, however, be done in a reasonable way. + +## Example + +Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request (header and JSONRPC body): + +```json +{ + "http": { + "scheme": "https", + "method": "POST", + "host": "mainnet.infura.io", + "path": "" + }, + "jsonrpc": { + "jsonrpc": "2.0", + "method": "eth_blockbynumber", + "params": [], + "id": 1 + } +} +``` +Those `args` can be evaluated against a delegation's policy, for example: +```json +{ + "cmd": "/infura/jsonrpc", + "pol": [ + ["==", ".http.host", "mainnet.infura.io"], + ["like", ".jsonrpc.method", "eth_*"] + ] +} +``` + +## Security implications + +UCAN essentially aims for perfect security. By having external args, we break that security perimeter, and we now need to arbitrate between security and practicality. + +First, what are we breaking exactly? Normally, the invocation has all the parameters and is signed by the invoker. This means that an attacker cannot intercept (MITM) a request and change the parameters when relaying it to the server. As they are signed, that would make the request invalid. + +If we have external args, now an attacker can intercept the request, change it, and pretend to be that person doing other things than intended. **That may of may not be an actual problem, depending on the situation.** + +There is a way to get around that, and have the best of both worlds, but **it comes with a client side complexity**: we can hash the external values and put them into the invocation's `args`. For example: + +```json +{ + "http": "zQmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D", + "jsonrpc": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9" +} +``` + +When the server receives the request, it can now reconstruct the values from the HTTP request, verify the hash and replace it with the real values for evaluation against the policies. **We are now back to the better security model**, but at the price of client-side complexity. + +## API design + +Therefore, the server-side logic is made to have this hashing optional: +- if present, the server honors the hash and enforces the security +- the client can opt out of passing that hash, and won't benefit from the enforced security + +The particular hash selected is SHA2-256 of the DAG-CBOR encoded argument, expressed in the form of a Multihash in raw bytes. +The arguments being hashed are the complete map of values, including the root key being replaced (for example `jsonrpc` or `http`). \ No newline at end of file diff --git a/toolkit/server/bearer/http.go b/toolkit/server/bearer/http.go new file mode 100644 index 0000000..70d0831 --- /dev/null +++ b/toolkit/server/bearer/http.go @@ -0,0 +1,160 @@ +package bearer + +import ( + "bytes" + "fmt" + "net/http" + "sync" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/multiformats/go-multihash" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +// HttpArgsKey is the key in the args, used for: +// - if it exists in the invocation, holds a hash of the args derived from the HTTP request +// - in the final args to be evaluated against the policies, holds the args derived from the HTTP request +const HttpArgsKey = "http" + +type HttpBearer struct { + pol policy.Policy + originalArgs args.ReadOnly + req *http.Request + + once sync.Once + args *args.Args + argsIpld ipld.Node +} + +func NewHttpBearer(pol policy.Policy, originalArgs args.ReadOnly, req *http.Request) *HttpBearer { + return &HttpBearer{pol: pol, originalArgs: originalArgs, req: req} +} + +func (hc *HttpBearer) Verify() error { + if err := hc.makeArgs(); err != nil { + return err + } + + if err := hc.verifyHash(); err != nil { + return err + } + + ok, leaf := hc.pol.PartialMatch(hc.argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) + } + return nil +} + +func (hc *HttpBearer) Args() (*args.Args, error) { + if err := hc.makeArgs(); err != nil { + return nil, err + } + return hc.args, nil +} + +func (hc *HttpBearer) makeArgs() error { + var outerErr error + hc.once.Do(func() { + var err error + hc.args, err = makeHttpArgs(hc.req) + if err != nil { + outerErr = err + return + } + + hc.argsIpld, err = hc.args.ToIPLD() + if err != nil { + outerErr = err + return + } + }) + return outerErr +} + +func (hc *HttpBearer) verifyHash() error { + n, err := hc.originalArgs.GetNode(HttpArgsKey) + if err != nil { + // no hash found, nothing to verify + return nil + } + + mhBytes, err := n.AsBytes() + if err != nil { + return fmt.Errorf("http args hash should be a string") + } + + data, err := ipld.Encode(hc.argsIpld, dagcbor.Encode) + if err != nil { + return fmt.Errorf("can't encode derived args in dag-cbor: %w", err) + } + + sum, err := multihash.Sum(data, multihash.SHA2_256, -1) + if err != nil { + return err + } + + if !bytes.Equal(mhBytes, sum) { + return fmt.Errorf("derived args from http request don't match the expected hash") + } + + return nil +} + +// MakeHttpHash compute the hash of the derived arguments from the HTTP request. +// If that hash is inserted at the HttpArgsKey key in the invocation arguments, +// this increases the security as the UCAN token cannot be used with a different +// HTTP request. +func MakeHttpHash(req *http.Request) ([]byte, error) { + computedArgs, err := makeHttpArgs(req) + if err != nil { + return nil, err + } + + n, err := computedArgs.ToIPLD() + if err != nil { + return nil, err + } + + data, err := ipld.Encode(n, dagcbor.Encode) + if err != nil { + return nil, err + } + + sum, err := multihash.Sum(data, multihash.SHA2_256, -1) + if err != nil { + return nil, err + } + + return sum, nil +} + +func makeHttpArgs(req *http.Request) (*args.Args, error) { + n, err := qp.BuildMap(basicnode.Prototype.Any, 4, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "scheme", qp.String(req.URL.Scheme)) // https + qp.MapEntry(ma, "method", qp.String(req.Method)) // GET + qp.MapEntry(ma, "host", qp.String(req.Host)) // example.com + qp.MapEntry(ma, "path", qp.String(req.URL.Path)) // /foo + + qp.MapEntry(ma, "headers", qp.Map(2, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "Origin", qp.String(req.Header.Get("Origin"))) + qp.MapEntry(ma, "User-Agent", qp.String(req.Header.Get("User-Agent"))) + })) + }) + if err != nil { + return nil, err + } + + res := args.New() + err = res.Add(HttpArgsKey, n) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/toolkit/server/bearer/http_test.go b/toolkit/server/bearer/http_test.go new file mode 100644 index 0000000..7849a26 --- /dev/null +++ b/toolkit/server/bearer/http_test.go @@ -0,0 +1,185 @@ +package bearer + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +func TestHttp(t *testing.T) { + pol := policy.MustConstruct( + policy.Equal(".http.scheme", literal.String("http")), + policy.Equal(".http.method", literal.String("GET")), + policy.Equal(".http.host", literal.String("example.com")), + policy.Equal(".http.path", literal.String("/foo")), + policy.Like(".http.headers.User-Agent", "*Mozilla*"), + policy.Equal(".http.headers.Origin", literal.String("dapps.com")), + ) + + tests := []struct { + name string + method string + target string + headers map[string]string + expected bool + }{ + { + name: "happy path", + method: http.MethodGet, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: true, + }, + { + name: "wrong scheme", + method: http.MethodGet, + target: "https://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong method", + method: http.MethodPost, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong host", + method: http.MethodGet, + target: "http://foo.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong path", + method: http.MethodGet, + target: "http://example.com/bar", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "dapps.com", + }, + expected: false, + }, + { + name: "wrong origin", + method: http.MethodGet, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0", + "Origin": "foo.com", + }, + expected: false, + }, + { + name: "wrong user-agent", + method: http.MethodGet, + target: "http://example.com/foo", + headers: map[string]string{ + "User-Agent": "Chrome/51.0.2704.103 Safari/537.36", + "Origin": "dapps.com", + }, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + handler := func(w http.ResponseWriter, r *http.Request) { + // we don't test the args hash here + emptyArgs := args.New().ReadOnly() + + ctx := NewHttpBearer(pol, emptyArgs, r) + + _, err := ctx.Args() + require.NoError(t, err) + + if tc.expected { + require.NoError(t, ctx.Verify()) + } else { + require.Error(t, ctx.Verify()) + } + } + + req := httptest.NewRequest(tc.method, tc.target, nil) + for k, v := range tc.headers { + req.Header.Set(k, v) + } + + w := httptest.NewRecorder() + handler(w, req) + }) + } +} + +func TestHttpHash(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "http://example.com/foo", nil) + require.NoError(t, err) + req.Header.Add("User-Agent", "Chrome/51.0.2704.103 Safari/537.36") + req.Header.Add("Origin", "dapps.com") + + pol := policy.MustConstruct( + policy.Equal(".http.scheme", literal.String("http")), + ) + + tests := []struct { + name string + hash []byte + expected bool + }{ + { + name: "correct hash", + hash: must(MakeHttpHash(req)), + expected: true, + }, + { + name: "non-matching hash", + hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.SHA2_256, -1)), + expected: false, + }, + { + name: "wrong type of hash", + hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.BLAKE3, -1)), + expected: false, + }, + { + name: "no hash", + hash: nil, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + invArgs := args.New() + err := invArgs.Add(HttpArgsKey, tc.hash) + require.NoError(t, err) + + ctx := NewHttpBearer(pol, invArgs.ReadOnly(), req) + + if tc.expected { + require.NoError(t, ctx.Verify()) + } else { + require.Error(t, ctx.Verify()) + } + }) + } +} diff --git a/toolkit/server/bearer/jsonrpc.go b/toolkit/server/bearer/jsonrpc.go new file mode 100644 index 0000000..27bbb8f --- /dev/null +++ b/toolkit/server/bearer/jsonrpc.go @@ -0,0 +1,168 @@ +package bearer + +import ( + "bytes" + "encoding/json" + "fmt" + "sync" + + "github.com/INFURA/go-ethlibs/jsonrpc" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/multiformats/go-multihash" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +// JsonRpcArgsKey is the key in the args, used for: +// - if it exists in the invocation, holds a hash of the args derived from the JsonRpc request +// - in the final args to be evaluated against the policies, holds the args derived from the JsonRpc request +const JsonRpcArgsKey = "jsonrpc" + +type JsonRpcBearer struct { + pol policy.Policy + originalArgs args.ReadOnly + req *jsonrpc.Request + + once sync.Once + args *args.Args + argsIpld ipld.Node +} + +func NewJsonRpcBearer(pol policy.Policy, originalArgs args.ReadOnly, req *jsonrpc.Request) *JsonRpcBearer { + return &JsonRpcBearer{pol: pol, originalArgs: originalArgs, req: req} +} + +func (jrc *JsonRpcBearer) Verify() error { + if err := jrc.makeArgs(); err != nil { + return err + } + + if err := jrc.verifyHash(); err != nil { + return err + } + + ok, leaf := jrc.pol.PartialMatch(jrc.argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) + } + return nil +} + +func (jrc *JsonRpcBearer) Args() (*args.Args, error) { + if err := jrc.makeArgs(); err != nil { + return nil, err + } + return jrc.args, nil +} + +func (jrc *JsonRpcBearer) makeArgs() error { + var outerErr error + jrc.once.Do(func() { + var err error + jrc.args, err = makeJsonRpcArgs(jrc.req) + if err != nil { + outerErr = err + return + } + + jrc.argsIpld, err = jrc.args.ToIPLD() + if err != nil { + outerErr = err + return + } + }) + return outerErr +} + +func (jrc *JsonRpcBearer) verifyHash() error { + n, err := jrc.originalArgs.GetNode(JsonRpcArgsKey) + if err != nil { + // no hash found, nothing to verify + return nil + } + + mhBytes, err := n.AsBytes() + if err != nil { + return fmt.Errorf("jsonrpc args hash should be a string") + } + + data, err := ipld.Encode(jrc.argsIpld, dagcbor.Encode) + if err != nil { + return fmt.Errorf("can't encode derived args in dag-cbor: %w", err) + } + + sum, err := multihash.Sum(data, multihash.SHA2_256, -1) + if err != nil { + return err + } + + if !bytes.Equal(mhBytes, sum) { + return fmt.Errorf("derived args from jsonrpc request don't match the expected hash") + } + + return nil +} + +// MakeJsonRpcHash compute the hash of the derived arguments from the JsonRPC request. +// If that hash is inserted at the JsonRpcArgsKey key in the invocation arguments, +// this increases the security as the UCAN token cannot be used with a different +// JsonRPC request. +func MakeJsonRpcHash(req *jsonrpc.Request) ([]byte, error) { + computedArgs, err := makeJsonRpcArgs(req) + if err != nil { + return nil, err + } + + n, err := computedArgs.ToIPLD() + if err != nil { + return nil, err + } + + data, err := ipld.Encode(n, dagcbor.Encode) + if err != nil { + return nil, err + } + + sum, err := multihash.Sum(data, multihash.SHA2_256, -1) + if err != nil { + return nil, err + } + + return sum, nil +} + +func makeJsonRpcArgs(req *jsonrpc.Request) (*args.Args, error) { + deserialized := make([]any, len(req.Params)) + for i, param := range req.Params { + err := json.Unmarshal(param, &deserialized[i]) + if err != nil { + return nil, err + } + } + + params, err := literal.List(deserialized) + if err != nil { + return nil, err + } + + n, err := qp.BuildMap(basicnode.Prototype.Any, 3, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "jsonrpc", qp.String(req.JSONRPC)) + qp.MapEntry(ma, "method", qp.String(req.Method)) + qp.MapEntry(ma, "params", qp.Node(params)) + }) + if err != nil { + return nil, err + } + + res := args.New() + err = res.Add(JsonRpcArgsKey, n) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/toolkit/server/bearer/jsonrpc_test.go b/toolkit/server/bearer/jsonrpc_test.go new file mode 100644 index 0000000..41791d8 --- /dev/null +++ b/toolkit/server/bearer/jsonrpc_test.go @@ -0,0 +1,171 @@ +package bearer + +import ( + "testing" + + "github.com/INFURA/go-ethlibs/jsonrpc" + "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +func TestJsonRpc(t *testing.T) { + tests := []struct { + name string + req *jsonrpc.Request + pol policy.Policy + expected bool + }{ + { + name: "or on method, not matching", + req: jsonrpc.MustRequest(1839673506133526, "eth_getBlockByNumber", + "0x599784", true, + ), + pol: policy.MustConstruct( + policy.Or( + policy.Equal(".jsonrpc.method", literal.String("eth_getCode")), + policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")), + policy.Equal(".jsonrpc.method", literal.String("eth_call")), + policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")), + ), + ), + expected: false, + }, + { + name: "or on method, matching", + req: jsonrpc.MustRequest(1839673506133526, "eth_call", + map[string]string{"to": "0xBADBADBADBADBADBADBADBADBADBADBADBADBAD1"}, + ), + pol: policy.MustConstruct( + policy.Or( + policy.Equal(".jsonrpc.method", literal.String("eth_getCode")), + policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")), + policy.Equal(".jsonrpc.method", literal.String("eth_call")), + policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")), + ), + ), + expected: true, + }, + { + name: "complex, optional parameter, matching", + req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall", + true, false, 1234, "callTracer", + ), + pol: policy.MustConstruct( + policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), + policy.Or( + policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")), + policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")), + ), + ), + expected: true, + }, + { + name: "complex, optional parameter, missing parameter", + req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall", + true, false, 1234, + ), + pol: policy.MustConstruct( + policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), + policy.Or( + policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")), + policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")), + ), + ), + expected: true, + }, + { + name: "complex, parameter not matching", + req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall", + true, false, 1234, "ho_no", + ), + pol: policy.MustConstruct( + policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), + policy.Or( + policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")), + policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")), + ), + ), + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // we don't test the args hash here + emptyArgs := args.New().ReadOnly() + + ctx := NewJsonRpcBearer(tc.pol, emptyArgs, tc.req) + + _, err := ctx.Args() + require.NoError(t, err) + + if tc.expected { + require.NoError(t, ctx.Verify()) + } else { + require.Error(t, ctx.Verify()) + } + }) + } +} + +func TestJsonRpcHash(t *testing.T) { + req := jsonrpc.MustRequest(1839673506133526, "debug_traceCall", + true, false, 1234, "ho_no", + ) + pol := policy.MustConstruct( + policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), + ) + + tests := []struct { + name string + hash []byte + expected bool + }{ + { + name: "correct hash", + hash: must(MakeJsonRpcHash(req)), + expected: true, + }, + { + name: "non-matching hash", + hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.SHA2_256, -1)), + expected: false, + }, + { + name: "wrong type of hash", + hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.BLAKE3, -1)), + expected: false, + }, + { + name: "no hash", + hash: nil, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + invArgs := args.New() + err := invArgs.Add(JsonRpcArgsKey, tc.hash) + require.NoError(t, err) + + ctx := NewJsonRpcBearer(pol, invArgs.ReadOnly(), req) + + if tc.expected { + require.NoError(t, ctx.Verify()) + } else { + require.Error(t, ctx.Verify()) + } + }) + } +} + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} diff --git a/toolkit/server/context.go b/toolkit/server/context.go deleted file mode 100644 index 701d203..0000000 --- a/toolkit/server/context.go +++ /dev/null @@ -1,27 +0,0 @@ -package server - -import "context" - -type ctxUserIdKey struct{} - -// ContextGetUserId return the UserId stored in the context, if it exists. -func ContextGetUserId(ctx context.Context) (string, bool) { - val, ok := ctx.Value(ctxUserIdKey{}).(string) - return val, ok -} - -func addUserIdToContext(ctx context.Context, userId string) context.Context { - return context.WithValue(ctx, ctxUserIdKey{}, userId) -} - -type ctxProjectIdKey struct{} - -// ContextGetProjectId return the ProjectID stored in the context, if it exists. -func ContextGetProjectId(ctx context.Context) (string, bool) { - val, ok := ctx.Value(ctxProjectIdKey{}).(string) - return val, ok -} - -func addProjectIdToContext(ctx context.Context, projectId string) context.Context { - return context.WithValue(ctx, ctxProjectIdKey{}, projectId) -} diff --git a/toolkit/server/exectx/ctxvalue.go b/toolkit/server/exectx/ctxvalue.go new file mode 100644 index 0000000..115289f --- /dev/null +++ b/toolkit/server/exectx/ctxvalue.go @@ -0,0 +1,16 @@ +package exectx + +import "context" + +type ctxKey struct{} + +// AddUcanCtxToContext insert a UcanCtx into a go context. +func AddUcanCtxToContext(ctx context.Context, ucanCtx *UcanCtx) context.Context { + return context.WithValue(ctx, ctxKey{}, ucanCtx) +} + +// FromContext retrieve a UcanCtx from a go context. +func FromContext(ctx context.Context) (*UcanCtx, bool) { + ucanCtx, ok := ctx.Value(ctxKey{}).(*UcanCtx) + return ucanCtx, ok +} diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go new file mode 100644 index 0000000..dec738d --- /dev/null +++ b/toolkit/server/exectx/ucanctx.go @@ -0,0 +1,146 @@ +package exectx + +import ( + "errors" + "fmt" + "net/http" + "slices" + + "github.com/INFURA/go-ethlibs/jsonrpc" + "github.com/ipfs/go-cid" + "github.com/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/meta" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" + + bearer2 "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +var _ delegation.Loader = UcanCtx{} + +// UcanCtx is a UCAN execution context meant to be inserted in the go context while handling a request. +// It allows to handle the control of the execution in multiple steps across different middlewares, +// as well as doing "bearer" types of controls, when arguments are derived from the request itself (HTTP, JsonRpc). +type UcanCtx struct { + inv *invocation.Token + dlgs map[cid.Cid]*delegation.Token + + policies policy.Policy // all policies combined + meta *meta.Meta // all meta combined, with no overwriting + + // argument sources + http *bearer2.HttpBearer + jsonrpc *bearer2.JsonRpcBearer +} + +func FromContainer(cont container.Reader) (*UcanCtx, error) { + inv, err := cont.GetInvocation() + if err != nil { + return nil, fmt.Errorf("no invocation: %w", err) + } + + ctx := &UcanCtx{ + inv: inv, + dlgs: make(map[cid.Cid]*delegation.Token, len(cont)-1), + meta: meta.NewMeta(), + } + + // iterate in reverse, from the root delegation to the leaf + for _, c := range slices.Backward(inv.Proof()) { + // make sure we have the delegation + dlg, err := cont.GetDelegation(c) + if errors.Is(err, delegation.ErrDelegationNotFound) { + return nil, fmt.Errorf("delegation proof %s is missing", c) + } + if err != nil { + return nil, err + } + ctx.dlgs[c] = dlg + // accumulate the policies + ctx.policies = append(ctx.policies, dlg.Policy()...) + // accumulate the meta values, with no overwriting + ctx.meta.Include(dlg.Meta()) + } + + return ctx, nil +} + +// Command returns the command triggered by the invocation. +func (ctn UcanCtx) Command() command.Command { + return ctn.inv.Command() +} + +// Invocation returns the invocation.Token. +func (ctn UcanCtx) Invocation() *invocation.Token { + return ctn.inv +} + +// GetDelegation returns the delegation.Token matching the given CID. +// If not found, delegation.ErrDelegationNotFound is returned. +func (ctn UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) { + if dlg, ok := ctn.dlgs[cid]; ok { + return dlg, nil + } + return nil, delegation.ErrDelegationNotFound +} + +// Policies return the full set of policy statements to satisfy. +func (ctn UcanCtx) Policies() policy.Policy { + return ctn.policies +} + +// Meta returns all the meta values from the delegations. +// They are accumulated from the root delegation to the leaf delegation, with no overwrite. +func (ctn UcanCtx) Meta() meta.ReadOnly { + return ctn.meta.ReadOnly() +} + +// VerifyHttp verify the delegation's policies against arguments constructed from the HTTP request. +// This function can only be called once per context. +// After being used, those constructed arguments will be used in ExecutionAllowed as well. +func (ctn UcanCtx) VerifyHttp(req *http.Request) error { + if ctn.http == nil { + panic("only use once per request context") + } + ctn.http = bearer2.NewHttpBearer(ctn.policies, ctn.inv.Arguments(), req) + return ctn.http.Verify() +} + +// VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request. +// This function can only be called once per context. +// After being used, those constructed arguments will be used in ExecutionAllowed as well. +func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { + if ctn.jsonrpc != nil { + panic("only use once per request context") + } + ctn.jsonrpc = bearer2.NewJsonRpcBearer(ctn.policies, ctn.inv.Arguments(), req) + return ctn.jsonrpc.Verify() +} + +// ExecutionAllowed does the final verification of the invocation. +// If VerifyHttp or VerifyJsonRpc has been used, those arguments are part of the verification. +func (ctn UcanCtx) ExecutionAllowed() error { + return ctn.inv.ExecutionAllowedWithArgsHook(ctn, func(args args.ReadOnly) (*args.Args, error) { + newArgs := args.WriteableClone() + + if ctn.http != nil { + httpArgs, err := ctn.http.Args() + if err != nil { + return nil, err + } + newArgs.Include(httpArgs) + } + if ctn.jsonrpc != nil { + jsonRpcArgs, err := ctn.jsonrpc.Args() + if err != nil { + return nil, err + } + newArgs.Include(jsonRpcArgs) + } + + return newArgs, nil + }) +} diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go new file mode 100644 index 0000000..731595e --- /dev/null +++ b/toolkit/server/exectx/ucanctx_test.go @@ -0,0 +1,162 @@ +package exectx_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/INFURA/go-ethlibs/jsonrpc" + "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/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" + + exectx2 "github.com/INFURA/go-ucan-toolkit/server/exectx" +) + +func ExampleContext() { + // let's use some pre-made DID+privkey. + // use go-ucan/did to generate or parse them. + service := didtest.PersonaAlice + user := didtest.PersonaBob + + // DELEGATION: the service gives some power to the user, in the form of a root UCAN token. + // The command defines the shape of the arguments on which the policies operate, it is specific to that service. + // Policies define what the user can do. + + cmd := command.New("foo") + pol := policy.MustConstruct( + // some basic HTTP constraints + policy.Equal(".http.method", literal.String("GET")), + policy.Like(".http.path", "/foo/*"), + // some JsonRpc constraints + policy.Or( + policy.Like(".jsonrpc.method", "eth_*"), + policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), + ), + ) + + dlg, _ := delegation.Root(service.DID(), user.DID(), cmd, pol, + delegation.WithExpirationIn(24*time.Hour), + ) + dlgBytes, dlgCid, _ := dlg.ToSealed(service.PrivKey()) + + // INVOCATION: the user leverages the delegation (power) to make a request. + + inv, _ := invocation.New(user.DID(), service.DID(), cmd, []cid.Cid{dlgCid}, + invocation.WithExpirationIn(10*time.Minute), + invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters + ) + invBytes, invCid, _ := inv.ToSealed(user.PrivKey()) + + // PACKAGING: no obligation for the transport, but the user needs to give the service the invocation + // and all the proof delegations. We can use a container for that. + cont := container.NewWriter() + cont.AddSealed(dlgCid, dlgBytes) + cont.AddSealed(invCid, invBytes) + contBytes, _ := cont.ToCborBase64() + + // MAKING A REQUEST: we pass the container in the Bearer HTTP header + + jrpc := jsonrpc.NewRequest() + jrpc.Method = "eth_call" + jrpc.Params = jsonrpc.MustParams("0x599784", true) + jrpcBytes, _ := jrpc.MarshalJSON() + req, _ := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes)) + req.Header.Set("Authorization", "Bearer "+string(contBytes)) + + // SERVER: Auth middleware + // - decode the container + // - create the context + + authMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Note: we obviously want something more robust, this is an example + // Note: if an error occur, we'll want to return an HTTP 401 Unauthorized + data := bytes.TrimPrefix([]byte(r.Header.Get("Authorization")), []byte("Bearer ")) + cont, _ := container.FromCborBase64(data) + ucanCtx, _ := exectx2.FromContainer(cont) + + // insert into the go context + req = req.WithContext(exectx2.AddUcanCtxToContext(req.Context(), ucanCtx)) + + next.ServeHTTP(w, req) + }) + } + + // SERVER: http checks + + httpMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, _ := exectx2.FromContext(req.Context()) + + err := ucanCtx.VerifyHttp(r) + if err != nil { + // This will print something like: + // `the following UCAN policy is not satisfied: ["==", ".http.path", "/foo"]` + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } + + // SERVER: JsonRpc checks + + jsonrpcMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, _ := exectx2.FromContext(req.Context()) + + var jrpc jsonrpc.Request + _ = json.NewDecoder(r.Body).Decode(&jrpc) + + err := ucanCtx.VerifyJsonRpc(&jrpc) + if err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) + } + + // SERVER: final handler + + handler := func(w http.ResponseWriter, r *http.Request) { + ucanCtx, _ := exectx2.FromContext(req.Context()) + + if err := ucanCtx.ExecutionAllowed(); err != nil { + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + + _, _ = fmt.Fprintln(w, "Success!") + } + + // Ready to go! + _ = http.ListenAndServe("", authMw(httpMw(jsonrpcMw(http.HandlerFunc(handler))))) +} + +func TestGoCtx(t *testing.T) { + ctx := context.Background() + + ucanCtx, ok := exectx2.FromContext(ctx) + require.False(t, ok) + require.Nil(t, ucanCtx) + + expected := &exectx2.UcanCtx{} + + ctx = exectx2.AddUcanCtxToContext(ctx, expected) + + got, ok := exectx2.FromContext(ctx) + require.True(t, ok) + require.Equal(t, expected, got) +} diff --git a/toolkit/server/middleware.go b/toolkit/server/middleware.go deleted file mode 100644 index a956d2d..0000000 --- a/toolkit/server/middleware.go +++ /dev/null @@ -1,80 +0,0 @@ -package server - -import ( - "encoding/base64" - "io" - "net/http" - "strings" - - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/token/delegation" -) - -type Middleware func(http.Handler) http.Handler - -func ExampleMiddleware(serviceDID did.DID) Middleware { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - tokenReader, ok := extractBearerToken(r) - if !ok { - http.Error(w, "missing or malformed UCAN HTTP Bearer token", http.StatusUnauthorized) - return - } - - // decode - // TODO: ultimately, this token should be a container with one invocation and 1+ delegations. - // We are doing something simpler for now. - dlg, err := delegation.FromDagCborReader(tokenReader) - if err != nil { - http.Error(w, "malformed UCAN delegation", http.StatusBadRequest) - return - } - - // optional: http-bearer - - // validate - if dlg.Subject() != serviceDID { - http.Error(w, "invalid UCAN delegation", http.StatusBadRequest) - return - } - // TODO: policies check + inject in context - - // extract values - userId, err := dlg.Meta().GetString("userId") - if err != nil { - http.Error(w, "missing or malformed userId", http.StatusBadRequest) - return - } - projectId, err := dlg.Meta().GetString("projectId") - if err != nil { - http.Error(w, "missing or malformed projectId", http.StatusBadRequest) - return - } - - // inject into context - ctx = addUserIdToContext(ctx, userId) - ctx = addProjectIdToContext(ctx, projectId) - - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - -func extractBearerToken(r *http.Request) (io.Reader, bool) { - header := r.Header.Get("Authorization") - if header == "" { - return nil, false - } - - if !strings.HasPrefix(header, "Bearer ") { - return nil, false - } - - // skip prefix - reader := strings.NewReader(header[len("Bearer "):]) - - // base64 decode - return base64.NewDecoder(base64.StdEncoding, reader), true -} From 174bf01c641ee99c7393a38bdb9990412b41e74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 9 Dec 2024 15:58:07 +0100 Subject: [PATCH 09/38] client: simpler FindProof() --- toolkit/client/proof.go | 43 ++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go index e296581..2b33fd8 100644 --- a/toolkit/client/proof.go +++ b/toolkit/client/proof.go @@ -17,33 +17,39 @@ import ( // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { - // Find the possible leaf delegations, directly matching the invocation parameters - var candidateLeaf []*delegation.Bundle - - for bundle := range dlgs() { - dlg := bundle.Decoded - + // TODO: maybe that should be part of delegation.Token directly? + dlgMatch := func(dlg *delegation.Token, cmd command.Command, aud, iss did.DID) bool { // The Subject of each delegation must equal the invocation's Audience field. - 4f if dlg.Subject() != aud { - continue + return false } // 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 { - continue + return false } // The command of each delegation must "allow" the one before it. - 4g if !dlg.Command().Covers(cmd) { - continue + return false } // Time bound - 3b, 3c if !dlg.IsValidNow() { + return false + } + return true + } + + // STEP 1: Find the possible leaf delegations, directly matching the invocation parameters + var candidateLeaf []*delegation.Bundle + + for bundle := range dlgs() { + if dlgMatch(bundle.Decoded, cmd, iss, aud) { continue } - candidateLeaf = append(candidateLeaf, bundle) } + // STEP 2: Perform a depth-first search on the DAG of connected delegations, for each of our candidates type state struct { bundle *delegation.Bundle path []cid.Cid @@ -53,7 +59,6 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, is var bestSize = math.MaxInt var bestProof []cid.Cid - // Perform a depth-first search on the DAG of connected delegations, for each of our candidates for _, leaf := range candidateLeaf { var stack = []state{{bundle: leaf, path: []cid.Cid{leaf.Cid}, size: len(leaf.Sealed)}} @@ -74,21 +79,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, is // find parent delegation for our current delegation for candidate := range dlgs() { - // The Subject of each delegation must equal the invocation's Audience field. - 4f - if candidate.Decoded.Subject() != aud { - continue - } - // 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 candidate.Decoded.Audience() != at.Decoded.Issuer() { - continue - } - // The command of each delegation must "allow" the one before it. - 4g - if !candidate.Decoded.Command().Covers(at.Decoded.Command()) { - continue - } - // Time bound - 3b, 3c - if !candidate.Decoded.IsValidNow() { + if !dlgMatch(candidate.Decoded, at.Decoded.Command(), aud, at.Decoded.Issuer()) { continue } From 4c08b22c61b29610b074c48e40eeb0126bdb83fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 9 Dec 2024 16:53:44 +0100 Subject: [PATCH 10/38] add an early version of a UCAN client --- toolkit/client/client.go | 78 +++++++++++++++++++++++++++++++++++++ toolkit/client/pool.go | 20 ++++++++++ toolkit/client/requester.go | 19 +++++++++ 3 files changed, 117 insertions(+) create mode 100644 toolkit/client/client.go create mode 100644 toolkit/client/requester.go diff --git a/toolkit/client/client.go b/toolkit/client/client.go new file mode 100644 index 0000000..3611ef2 --- /dev/null +++ b/toolkit/client/client.go @@ -0,0 +1,78 @@ +package client + +import ( + "context" + "fmt" + + "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/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +type Client struct { + did did.DID + privKey crypto.PrivKey + + pool *Pool + requester DelegationRequester +} + +func NewClient(privKey crypto.PrivKey, requester DelegationRequester) (*Client, error) { + d, err := did.FromPrivKey(privKey) + if err != nil { + return nil, err + } + return &Client{ + did: d, + privKey: privKey, + pool: NewPool(), + requester: requester, + }, nil +} + +// PrepareInvoke returns an invocation and the proof delegation, bundled in a container.Writer. +func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject did.DID, opts ...invocation.Option) (container.Writer, error) { + var proof []cid.Cid + + // do we already have a valid proof? + if proof = c.pool.FindProof(cmd, c.did, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := c.requester.RequestDelegation(ctx, cmd, c.did, subject) + if err != nil { + return nil, fmt.Errorf("requesting delegation: %w", err) + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + c.pool.AddBundle(bundle) + } + } + + inv, err := invocation.New(c.did, subject, cmd, proof, opts...) + if err != nil { + return nil, err + } + + invSealed, invCid, err := inv.ToSealed(c.privKey) + if err != nil { + return nil, err + } + + cont := container.NewWriter() + cont.AddSealed(invCid, invSealed) + for bundle, err := range c.pool.GetBundles(proof) { + if err != nil { + return nil, err + } + cont.AddSealed(bundle.Cid, bundle.Sealed) + } + + return cont, nil +} diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go index 6926eb7..de2b526 100644 --- a/toolkit/client/pool.go +++ b/toolkit/client/pool.go @@ -1,6 +1,7 @@ package client import ( + "fmt" "iter" "sync" "time" @@ -39,6 +40,7 @@ func (p *Pool) AddBundles(bundles iter.Seq[*delegation.Bundle]) { // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { + // TODO: move to some kind of background trim job? p.trim() p.mu.RLock() @@ -55,6 +57,24 @@ func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Ci }, cmd, iss, aud) } +func (p *Pool) GetBundles(cids []cid.Cid) iter.Seq2[*delegation.Bundle, error] { + p.mu.RLock() + defer p.mu.RUnlock() + + return func(yield func(*delegation.Bundle, error) bool) { + for _, c := range cids { + if b, ok := p.dlgs[c]; ok { + if !yield(b, nil) { + return + } + } else { + yield(nil, fmt.Errorf("bundle not found")) + return + } + } + } +} + // trim removes expired tokens func (p *Pool) trim() { p.mu.Lock() diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go new file mode 100644 index 0000000..cf28bc4 --- /dev/null +++ b/toolkit/client/requester.go @@ -0,0 +1,19 @@ +package client + +import ( + "context" + "iter" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +type DelegationRequester interface { + // RequestDelegation retrieve a delegation or chain of delegation for the given parameters. + // - cmd: the command to execute + // - issuer: the DID of the client, also the issuer of the invocation token + // - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. + RequestDelegation(ctx context.Context, cmd command.Command, audience did.DID, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) +} From 1187674a249307e763aea55cdc4f8bcf5138529d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 10 Dec 2024 14:48:58 +0100 Subject: [PATCH 11/38] server: minor code fixes --- toolkit/server/exectx/ucanctx.go | 10 +++++----- toolkit/server/exectx/ucanctx_test.go | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index dec738d..1b4851a 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -16,7 +16,7 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/invocation" - bearer2 "github.com/INFURA/go-ucan-toolkit/server/bearer" + "github.com/INFURA/go-ucan-toolkit/server/bearer" ) var _ delegation.Loader = UcanCtx{} @@ -32,8 +32,8 @@ type UcanCtx struct { meta *meta.Meta // all meta combined, with no overwriting // argument sources - http *bearer2.HttpBearer - jsonrpc *bearer2.JsonRpcBearer + http *bearer.HttpBearer + jsonrpc *bearer.JsonRpcBearer } func FromContainer(cont container.Reader) (*UcanCtx, error) { @@ -105,7 +105,7 @@ func (ctn UcanCtx) VerifyHttp(req *http.Request) error { if ctn.http == nil { panic("only use once per request context") } - ctn.http = bearer2.NewHttpBearer(ctn.policies, ctn.inv.Arguments(), req) + ctn.http = bearer.NewHttpBearer(ctn.policies, ctn.inv.Arguments(), req) return ctn.http.Verify() } @@ -116,7 +116,7 @@ func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { if ctn.jsonrpc != nil { panic("only use once per request context") } - ctn.jsonrpc = bearer2.NewJsonRpcBearer(ctn.policies, ctn.inv.Arguments(), req) + ctn.jsonrpc = bearer.NewJsonRpcBearer(ctn.policies, ctn.inv.Arguments(), req) return ctn.jsonrpc.Verify() } diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index 731595e..7e6fc8a 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -20,7 +20,7 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/invocation" - exectx2 "github.com/INFURA/go-ucan-toolkit/server/exectx" + "github.com/INFURA/go-ucan-toolkit/server/exectx" ) func ExampleContext() { @@ -52,7 +52,7 @@ func ExampleContext() { // INVOCATION: the user leverages the delegation (power) to make a request. - inv, _ := invocation.New(user.DID(), service.DID(), cmd, []cid.Cid{dlgCid}, + inv, _ := invocation.New(user.DID(), cmd, service.DID(), []cid.Cid{dlgCid}, invocation.WithExpirationIn(10*time.Minute), invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters ) @@ -84,10 +84,10 @@ func ExampleContext() { // Note: if an error occur, we'll want to return an HTTP 401 Unauthorized data := bytes.TrimPrefix([]byte(r.Header.Get("Authorization")), []byte("Bearer ")) cont, _ := container.FromCborBase64(data) - ucanCtx, _ := exectx2.FromContainer(cont) + ucanCtx, _ := exectx.FromContainer(cont) // insert into the go context - req = req.WithContext(exectx2.AddUcanCtxToContext(req.Context(), ucanCtx)) + req = req.WithContext(exectx.AddUcanCtxToContext(req.Context(), ucanCtx)) next.ServeHTTP(w, req) }) @@ -97,7 +97,7 @@ func ExampleContext() { httpMw := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ucanCtx, _ := exectx2.FromContext(req.Context()) + ucanCtx, _ := exectx.FromContext(req.Context()) err := ucanCtx.VerifyHttp(r) if err != nil { @@ -114,7 +114,7 @@ func ExampleContext() { jsonrpcMw := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ucanCtx, _ := exectx2.FromContext(req.Context()) + ucanCtx, _ := exectx.FromContext(req.Context()) var jrpc jsonrpc.Request _ = json.NewDecoder(r.Body).Decode(&jrpc) @@ -131,7 +131,7 @@ func ExampleContext() { // SERVER: final handler handler := func(w http.ResponseWriter, r *http.Request) { - ucanCtx, _ := exectx2.FromContext(req.Context()) + ucanCtx, _ := exectx.FromContext(req.Context()) if err := ucanCtx.ExecutionAllowed(); err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) @@ -148,15 +148,15 @@ func ExampleContext() { func TestGoCtx(t *testing.T) { ctx := context.Background() - ucanCtx, ok := exectx2.FromContext(ctx) + ucanCtx, ok := exectx.FromContext(ctx) require.False(t, ok) require.Nil(t, ucanCtx) - expected := &exectx2.UcanCtx{} + expected := &exectx.UcanCtx{} - ctx = exectx2.AddUcanCtxToContext(ctx, expected) + ctx = exectx.AddUcanCtxToContext(ctx, expected) - got, ok := exectx2.FromContext(ctx) + got, ok := exectx.FromContext(ctx) require.True(t, ok) require.Equal(t, expected, got) } From 2eeaaccc6d58859cb08df649a1695e4635a6e20e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 10 Dec 2024 14:50:29 +0100 Subject: [PATCH 12/38] client: follow go-ucan changes, improvements, example --- toolkit/client/client.go | 6 +-- toolkit/client/client_test.go | 76 +++++++++++++++++++++++++++++++++++ toolkit/client/pool.go | 9 +++-- toolkit/client/proof.go | 19 ++++----- toolkit/client/proof_test.go | 12 +++--- toolkit/client/requester.go | 37 +++++++++++++++-- 6 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 toolkit/client/client_test.go diff --git a/toolkit/client/client.go b/toolkit/client/client.go index 3611ef2..d783605 100644 --- a/toolkit/client/client.go +++ b/toolkit/client/client.go @@ -38,9 +38,9 @@ func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject var proof []cid.Cid // do we already have a valid proof? - if proof = c.pool.FindProof(cmd, c.did, subject); len(proof) == 0 { + if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { // we need to request a new proof - proofBundles, err := c.requester.RequestDelegation(ctx, cmd, c.did, subject) + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) if err != nil { return nil, fmt.Errorf("requesting delegation: %w", err) } @@ -55,7 +55,7 @@ func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject } } - inv, err := invocation.New(c.did, subject, cmd, proof, opts...) + inv, err := invocation.New(c.did, cmd, subject, proof, opts...) if err != nil { return nil, err } diff --git a/toolkit/client/client_test.go b/toolkit/client/client_test.go new file mode 100644 index 0000000..0e0d25f --- /dev/null +++ b/toolkit/client/client_test.go @@ -0,0 +1,76 @@ +package client + +import ( + "context" + "fmt" + "iter" + "time" + + "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" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +func ExampleNewClient() { + servicePersona := didtest.PersonaAlice + clientPersona := didtest.PersonaBob + + // requester is an adaptor for a real world issuer, we use a mock in that example + requester := &requesterMock{persona: servicePersona} + + client, err := NewClient(clientPersona.PrivKey(), requester) + handleError(err) + + cont, err := client.PrepareInvoke( + context.Background(), + command.New("crud", "add"), + servicePersona.DID(), + // extra invocation parameters: + invocation.WithExpirationIn(10*time.Minute), + invocation.WithArgument("foo", "bar"), + invocation.WithMeta("baz", 1234), + ) + handleError(err) + + // this container holds the invocation and all the delegation proofs + b64, err := cont.ToCborBase64() + handleError(err) + + fmt.Println(string(b64)) +} + +func handleError(err error) { + if err != nil { + panic(err) + } +} + +type requesterMock struct { + persona didtest.Persona +} + +func (r requesterMock) RequestDelegation(_ context.Context, audience did.DID, cmd command.Command, _ did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + // the mock issue whatever the client asks: + dlg, err := delegation.Root(r.persona.DID(), audience, cmd, policy.Policy{}) + if err != nil { + return nil, err + } + + dlgBytes, dlgCid, err := dlg.ToSealed(r.persona.PrivKey()) + if err != nil { + return nil, err + } + + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + return func(yield func(*delegation.Bundle, error) bool) { + yield(bundle, nil) + }, nil +} diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go index de2b526..13603cc 100644 --- a/toolkit/client/pool.go +++ b/toolkit/client/pool.go @@ -34,12 +34,13 @@ func (p *Pool) AddBundles(bundles iter.Seq[*delegation.Bundle]) { } // FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. -// - cmd: the command to execute // - issuer: the DID of the client, also the issuer of the invocation token -// - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(issuer) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. -func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { +func (p *Pool) FindProof(issuer did.DID, cmd command.Command, subject did.DID) []cid.Cid { // TODO: move to some kind of background trim job? p.trim() @@ -54,7 +55,7 @@ func (p *Pool) FindProof(cmd command.Command, iss did.DID, aud did.DID) []cid.Ci } } } - }, cmd, iss, aud) + }, issuer, cmd, subject) } func (p *Pool) GetBundles(cids []cid.Cid) iter.Seq2[*delegation.Bundle, error] { diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go index 2b33fd8..2b933d4 100644 --- a/toolkit/client/proof.go +++ b/toolkit/client/proof.go @@ -11,21 +11,22 @@ import ( ) // FindProof find in the pool the best (shortest, smallest in bytes) chain of delegation(s) matching the given invocation parameters. -// - cmd: the command to execute // - issuer: the DID of the client, also the issuer of the invocation token -// - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(issuer) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. -func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, iss did.DID, aud did.DID) []cid.Cid { +func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd command.Command, subject did.DID) []cid.Cid { // TODO: maybe that should be part of delegation.Token directly? - dlgMatch := func(dlg *delegation.Token, cmd command.Command, aud, iss did.DID) bool { - // The Subject of each delegation must equal the invocation's Audience field. - 4f - if dlg.Subject() != aud { + dlgMatch := func(dlg *delegation.Token, issuer did.DID, cmd command.Command, subject did.DID) bool { + // The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f + if dlg.Subject() != subject { return false } // 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 { + if dlg.Audience() != issuer { return false } // The command of each delegation must "allow" the one before it. - 4g @@ -43,7 +44,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, is var candidateLeaf []*delegation.Bundle for bundle := range dlgs() { - if dlgMatch(bundle.Decoded, cmd, iss, aud) { + if dlgMatch(bundle.Decoded, issuer, cmd, subject) { continue } candidateLeaf = append(candidateLeaf, bundle) @@ -79,7 +80,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], cmd command.Command, is // find parent delegation for our current delegation for candidate := range dlgs() { - if !dlgMatch(candidate.Decoded, at.Decoded.Command(), aud, at.Decoded.Issuer()) { + if !dlgMatch(candidate.Decoded, at.Decoded.Issuer(), at.Decoded.Command(), subject) { continue } diff --git a/toolkit/client/proof_test.go b/toolkit/client/proof_test.go index cce0d55..5f0738c 100644 --- a/toolkit/client/proof_test.go +++ b/toolkit/client/proof_test.go @@ -18,16 +18,16 @@ func TestFindProof(t *testing.T) { } require.Equal(t, delegationtest.ProofAliceBob, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaBob.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarol, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaCarol.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaCarol.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDan, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaDan.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaDan.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDanErin, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaErin.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaErin.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) require.Equal(t, delegationtest.ProofAliceBobCarolDanErinFrank, - FindProof(dlgs, delegationtest.NominalCommand, didtest.PersonaFrank.DID(), didtest.PersonaAlice.DID())) + FindProof(dlgs, didtest.PersonaFrank.DID(), delegationtest.NominalCommand, didtest.PersonaAlice.DID())) // wrong command - require.Empty(t, FindProof(dlgs, command.New("foo"), didtest.PersonaBob.DID(), didtest.PersonaAlice.DID())) + require.Empty(t, FindProof(dlgs, didtest.PersonaBob.DID(), command.New("foo"), didtest.PersonaAlice.DID())) } diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go index cf28bc4..8420a49 100644 --- a/toolkit/client/requester.go +++ b/toolkit/client/requester.go @@ -3,7 +3,9 @@ package client import ( "context" "iter" + "time" + "github.com/avast/retry-go/v4" "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" @@ -12,8 +14,37 @@ import ( type DelegationRequester interface { // RequestDelegation retrieve a delegation or chain of delegation for the given parameters. // - cmd: the command to execute - // - issuer: the DID of the client, also the issuer of the invocation token - // - audience: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // - audience: the DID of the client, also the issuer of the invocation token + // - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // Note: you can read it as "(audience) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. - RequestDelegation(ctx context.Context, cmd command.Command, audience did.DID, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) + RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) +} + +var _ DelegationRequester = &withRetry{} + +type withRetry struct { + requester DelegationRequester + initialDelay time.Duration + maxAttempts uint +} + +// RequesterWithRetry wraps a DelegationRequester to perform exponential backoff, +// with an initial delay and a maximum attempt count. +func RequesterWithRetry(requester DelegationRequester, initialDelay time.Duration, maxAttempt uint) DelegationRequester { + return &withRetry{ + requester: requester, + initialDelay: initialDelay, + maxAttempts: maxAttempt, + } +} + +func (w withRetry) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + return retry.DoWithData(func() (iter.Seq2[*delegation.Bundle, error], error) { + return w.requester.RequestDelegation(ctx, audience, cmd, subject) + }, + retry.Context(ctx), + retry.Delay(w.initialDelay), + retry.Attempts(w.maxAttempts), + ) } From b0783bf4a4d0853e9c362c3958df63c36fdcad54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 10 Dec 2024 14:52:39 +0100 Subject: [PATCH 13/38] add a "core" implementation of an issuer --- toolkit/issuer/issuer.go | 112 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 toolkit/issuer/issuer.go diff --git a/toolkit/issuer/issuer.go b/toolkit/issuer/issuer.go new file mode 100644 index 0000000..6403d47 --- /dev/null +++ b/toolkit/issuer/issuer.go @@ -0,0 +1,112 @@ +package issuer + +import ( + "context" + "iter" + "time" + + "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/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + + "github.com/INFURA/go-ucan-toolkit/client" +) + +var _ client.DelegationRequester = &Issuer{} + +type Issuer struct { + did did.DID + privKey crypto.PrivKey + + pool *client.Pool + requester client.DelegationRequester + logic IssuingLogic +} + +func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic IssuingLogic) (*Issuer, error) { + d, err := did.FromPrivKey(privKey) + if err != nil { + return nil, err + } + return &Issuer{ + did: d, + privKey: privKey, + pool: client.NewPool(), + requester: client.RequesterWithRetry(requester, time.Second, 3), + logic: logic, + }, nil +} + +// IssuingLogic is a function that decides what powers are given to a client. +// - issuer: the DID of our issuer +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(audience) wants to do (cmd) on (subject)". +// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the +// expected action. If you don't want to give that power, return an error instead. +type IssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) + +// RequestDelegation retrieve chain of delegation for the given parameters. +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(audience) does (cmd) on (subject)". +// Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. +func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + var proof []cid.Cid + + // is there already a valid proof chain? + if proof = i.pool.FindProof(audience, cmd, subject); len(proof) > 0 { + return i.pool.GetBundles(proof), nil + } + + // do we have the power to delegate this? + if proof = i.pool.FindProof(i.did, cmd, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := i.requester.RequestDelegation(ctx, i.did, cmd, subject) + if err != nil { + return nil, err + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + i.pool.AddBundle(bundle) + } + } + + // run the custom logic to get what we actually issue + dlg, err := i.logic(i.did, audience, cmd, subject) + if err != nil { + return nil, err + } + + // sign and cache the new token + dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey) + if err != nil { + return nil, err + } + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + // output the relevant delegations + return func(yield func(*delegation.Bundle, error) bool) { + if !yield(bundle, nil) { + return + } + for b, err := range i.pool.GetBundles(proof) { + if !yield(b, err) { + return + } + } + }, nil +} From 11b4352063dcf2452fce647074f56e92fb0d8614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 12 Dec 2024 16:58:02 +0100 Subject: [PATCH 14/38] client: fix FindProof() --- toolkit/client/proof.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go index 2b933d4..019a0be 100644 --- a/toolkit/client/proof.go +++ b/toolkit/client/proof.go @@ -44,7 +44,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd com var candidateLeaf []*delegation.Bundle for bundle := range dlgs() { - if dlgMatch(bundle.Decoded, issuer, cmd, subject) { + if !dlgMatch(bundle.Decoded, issuer, cmd, subject) { continue } candidateLeaf = append(candidateLeaf, bundle) From 9c8e9f17fa506fe97cc06b342bb5a1248ad5e675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 12 Dec 2024 16:41:02 +0100 Subject: [PATCH 15/38] cli: update following go-ucan changes --- toolkit/server/exectx/ucanctx_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index 7e6fc8a..54b8dff 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "testing" "time" @@ -82,7 +83,7 @@ func ExampleContext() { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Note: we obviously want something more robust, this is an example // Note: if an error occur, we'll want to return an HTTP 401 Unauthorized - data := bytes.TrimPrefix([]byte(r.Header.Get("Authorization")), []byte("Bearer ")) + data := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") cont, _ := container.FromCborBase64(data) ucanCtx, _ := exectx.FromContainer(cont) From ad02aa8d4fc26393cb1fb453bf66c4119348d03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 9 Jan 2025 13:22:22 +0100 Subject: [PATCH 16/38] server: rename "bearer" package to "extargs" --- toolkit/server/exectx/ucanctx.go | 10 +++--- toolkit/server/{bearer => extargs}/Readme.md | 2 +- toolkit/server/{bearer => extargs}/http.go | 36 +++++++++---------- .../server/{bearer => extargs}/http_test.go | 6 ++-- toolkit/server/{bearer => extargs}/jsonrpc.go | 36 +++++++++---------- .../{bearer => extargs}/jsonrpc_test.go | 6 ++-- 6 files changed, 48 insertions(+), 48 deletions(-) rename toolkit/server/{bearer => extargs}/Readme.md (87%) rename toolkit/server/{bearer => extargs}/http.go (79%) rename toolkit/server/{bearer => extargs}/http_test.go (97%) rename toolkit/server/{bearer => extargs}/jsonrpc.go (78%) rename toolkit/server/{bearer => extargs}/jsonrpc_test.go (97%) diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index 1b4851a..e2a8999 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -16,7 +16,7 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/invocation" - "github.com/INFURA/go-ucan-toolkit/server/bearer" + "github.com/INFURA/go-ucan-toolkit/server/extargs" ) var _ delegation.Loader = UcanCtx{} @@ -32,8 +32,8 @@ type UcanCtx struct { meta *meta.Meta // all meta combined, with no overwriting // argument sources - http *bearer.HttpBearer - jsonrpc *bearer.JsonRpcBearer + http *extargs.HttpExtArgs + jsonrpc *extargs.JsonRpcExtArgs } func FromContainer(cont container.Reader) (*UcanCtx, error) { @@ -105,7 +105,7 @@ func (ctn UcanCtx) VerifyHttp(req *http.Request) error { if ctn.http == nil { panic("only use once per request context") } - ctn.http = bearer.NewHttpBearer(ctn.policies, ctn.inv.Arguments(), req) + ctn.http = extargs.NewHttpExtArgs(ctn.policies, ctn.inv.Arguments(), req) return ctn.http.Verify() } @@ -116,7 +116,7 @@ func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { if ctn.jsonrpc != nil { panic("only use once per request context") } - ctn.jsonrpc = bearer.NewJsonRpcBearer(ctn.policies, ctn.inv.Arguments(), req) + ctn.jsonrpc = extargs.NewJsonRpcExtArgs(ctn.policies, ctn.inv.Arguments(), req) return ctn.jsonrpc.Verify() } diff --git a/toolkit/server/bearer/Readme.md b/toolkit/server/extargs/Readme.md similarity index 87% rename from toolkit/server/bearer/Readme.md rename to toolkit/server/extargs/Readme.md index 9875e33..ecaedf7 100644 --- a/toolkit/server/bearer/Readme.md +++ b/toolkit/server/extargs/Readme.md @@ -1,6 +1,6 @@ ## Motivations -UCAN is normally a pure RPC construct, when the entirety of the request's parameters are part of the invocation, in the form of `args`. Those `args` are evaluated against the delegation's [policy](https://github.com/ucan-wg/delegation/tree/v1_ipld?tab=readme-ov-file#policy) to determine if the request is allowed or not, then the request handling happens purely based on those args and the `command`. In that setup, the service would have a single entry point. +UCAN is normally a pure RPC construct, when the entirety of the request's parameters is part of the invocation, in the form of `args`. Those `args` are evaluated against the delegation's [policy](https://github.com/ucan-wg/delegation/tree/v1_ipld?tab=readme-ov-file#policy) to determine if the request is allowed or not, then the request handling happens purely based on those args and the `command`. In that setup, the service would have a single entry point. Unfortunately, we live in a world of REST APIs, or JSON-RPC. Some adaptations or concessions need to be made. diff --git a/toolkit/server/bearer/http.go b/toolkit/server/extargs/http.go similarity index 79% rename from toolkit/server/bearer/http.go rename to toolkit/server/extargs/http.go index 70d0831..cb33498 100644 --- a/toolkit/server/bearer/http.go +++ b/toolkit/server/extargs/http.go @@ -1,4 +1,4 @@ -package bearer +package extargs import ( "bytes" @@ -21,7 +21,7 @@ import ( // - in the final args to be evaluated against the policies, holds the args derived from the HTTP request const HttpArgsKey = "http" -type HttpBearer struct { +type HttpExtArgs struct { pol policy.Policy originalArgs args.ReadOnly req *http.Request @@ -31,44 +31,44 @@ type HttpBearer struct { argsIpld ipld.Node } -func NewHttpBearer(pol policy.Policy, originalArgs args.ReadOnly, req *http.Request) *HttpBearer { - return &HttpBearer{pol: pol, originalArgs: originalArgs, req: req} +func NewHttpExtArgs(pol policy.Policy, originalArgs args.ReadOnly, req *http.Request) *HttpExtArgs { + return &HttpExtArgs{pol: pol, originalArgs: originalArgs, req: req} } -func (hc *HttpBearer) Verify() error { - if err := hc.makeArgs(); err != nil { +func (hea *HttpExtArgs) Verify() error { + if err := hea.makeArgs(); err != nil { return err } - if err := hc.verifyHash(); err != nil { + if err := hea.verifyHash(); err != nil { return err } - ok, leaf := hc.pol.PartialMatch(hc.argsIpld) + ok, leaf := hea.pol.PartialMatch(hea.argsIpld) if !ok { return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) } return nil } -func (hc *HttpBearer) Args() (*args.Args, error) { - if err := hc.makeArgs(); err != nil { +func (hea *HttpExtArgs) Args() (*args.Args, error) { + if err := hea.makeArgs(); err != nil { return nil, err } - return hc.args, nil + return hea.args, nil } -func (hc *HttpBearer) makeArgs() error { +func (hea *HttpExtArgs) makeArgs() error { var outerErr error - hc.once.Do(func() { + hea.once.Do(func() { var err error - hc.args, err = makeHttpArgs(hc.req) + hea.args, err = makeHttpArgs(hea.req) if err != nil { outerErr = err return } - hc.argsIpld, err = hc.args.ToIPLD() + hea.argsIpld, err = hea.args.ToIPLD() if err != nil { outerErr = err return @@ -77,8 +77,8 @@ func (hc *HttpBearer) makeArgs() error { return outerErr } -func (hc *HttpBearer) verifyHash() error { - n, err := hc.originalArgs.GetNode(HttpArgsKey) +func (hea *HttpExtArgs) verifyHash() error { + n, err := hea.originalArgs.GetNode(HttpArgsKey) if err != nil { // no hash found, nothing to verify return nil @@ -89,7 +89,7 @@ func (hc *HttpBearer) verifyHash() error { return fmt.Errorf("http args hash should be a string") } - data, err := ipld.Encode(hc.argsIpld, dagcbor.Encode) + data, err := ipld.Encode(hea.argsIpld, dagcbor.Encode) if err != nil { return fmt.Errorf("can't encode derived args in dag-cbor: %w", err) } diff --git a/toolkit/server/bearer/http_test.go b/toolkit/server/extargs/http_test.go similarity index 97% rename from toolkit/server/bearer/http_test.go rename to toolkit/server/extargs/http_test.go index 7849a26..401bd3a 100644 --- a/toolkit/server/bearer/http_test.go +++ b/toolkit/server/extargs/http_test.go @@ -1,4 +1,4 @@ -package bearer +package extargs import ( "net/http" @@ -107,7 +107,7 @@ func TestHttp(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - ctx := NewHttpBearer(pol, emptyArgs, r) + ctx := NewHttpExtArgs(pol, emptyArgs, r) _, err := ctx.Args() require.NoError(t, err) @@ -173,7 +173,7 @@ func TestHttpHash(t *testing.T) { err := invArgs.Add(HttpArgsKey, tc.hash) require.NoError(t, err) - ctx := NewHttpBearer(pol, invArgs.ReadOnly(), req) + ctx := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { require.NoError(t, ctx.Verify()) diff --git a/toolkit/server/bearer/jsonrpc.go b/toolkit/server/extargs/jsonrpc.go similarity index 78% rename from toolkit/server/bearer/jsonrpc.go rename to toolkit/server/extargs/jsonrpc.go index 27bbb8f..418f844 100644 --- a/toolkit/server/bearer/jsonrpc.go +++ b/toolkit/server/extargs/jsonrpc.go @@ -1,4 +1,4 @@ -package bearer +package extargs import ( "bytes" @@ -23,7 +23,7 @@ import ( // - in the final args to be evaluated against the policies, holds the args derived from the JsonRpc request const JsonRpcArgsKey = "jsonrpc" -type JsonRpcBearer struct { +type JsonRpcExtArgs struct { pol policy.Policy originalArgs args.ReadOnly req *jsonrpc.Request @@ -33,44 +33,44 @@ type JsonRpcBearer struct { argsIpld ipld.Node } -func NewJsonRpcBearer(pol policy.Policy, originalArgs args.ReadOnly, req *jsonrpc.Request) *JsonRpcBearer { - return &JsonRpcBearer{pol: pol, originalArgs: originalArgs, req: req} +func NewJsonRpcExtArgs(pol policy.Policy, originalArgs args.ReadOnly, req *jsonrpc.Request) *JsonRpcExtArgs { + return &JsonRpcExtArgs{pol: pol, originalArgs: originalArgs, req: req} } -func (jrc *JsonRpcBearer) Verify() error { - if err := jrc.makeArgs(); err != nil { +func (jrea *JsonRpcExtArgs) Verify() error { + if err := jrea.makeArgs(); err != nil { return err } - if err := jrc.verifyHash(); err != nil { + if err := jrea.verifyHash(); err != nil { return err } - ok, leaf := jrc.pol.PartialMatch(jrc.argsIpld) + ok, leaf := jrea.pol.PartialMatch(jrea.argsIpld) if !ok { return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) } return nil } -func (jrc *JsonRpcBearer) Args() (*args.Args, error) { - if err := jrc.makeArgs(); err != nil { +func (jrea *JsonRpcExtArgs) Args() (*args.Args, error) { + if err := jrea.makeArgs(); err != nil { return nil, err } - return jrc.args, nil + return jrea.args, nil } -func (jrc *JsonRpcBearer) makeArgs() error { +func (jrea *JsonRpcExtArgs) makeArgs() error { var outerErr error - jrc.once.Do(func() { + jrea.once.Do(func() { var err error - jrc.args, err = makeJsonRpcArgs(jrc.req) + jrea.args, err = makeJsonRpcArgs(jrea.req) if err != nil { outerErr = err return } - jrc.argsIpld, err = jrc.args.ToIPLD() + jrea.argsIpld, err = jrea.args.ToIPLD() if err != nil { outerErr = err return @@ -79,8 +79,8 @@ func (jrc *JsonRpcBearer) makeArgs() error { return outerErr } -func (jrc *JsonRpcBearer) verifyHash() error { - n, err := jrc.originalArgs.GetNode(JsonRpcArgsKey) +func (jrea *JsonRpcExtArgs) verifyHash() error { + n, err := jrea.originalArgs.GetNode(JsonRpcArgsKey) if err != nil { // no hash found, nothing to verify return nil @@ -91,7 +91,7 @@ func (jrc *JsonRpcBearer) verifyHash() error { return fmt.Errorf("jsonrpc args hash should be a string") } - data, err := ipld.Encode(jrc.argsIpld, dagcbor.Encode) + data, err := ipld.Encode(jrea.argsIpld, dagcbor.Encode) if err != nil { return fmt.Errorf("can't encode derived args in dag-cbor: %w", err) } diff --git a/toolkit/server/bearer/jsonrpc_test.go b/toolkit/server/extargs/jsonrpc_test.go similarity index 97% rename from toolkit/server/bearer/jsonrpc_test.go rename to toolkit/server/extargs/jsonrpc_test.go index 41791d8..1346f1d 100644 --- a/toolkit/server/bearer/jsonrpc_test.go +++ b/toolkit/server/extargs/jsonrpc_test.go @@ -1,4 +1,4 @@ -package bearer +package extargs import ( "testing" @@ -97,7 +97,7 @@ func TestJsonRpc(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - ctx := NewJsonRpcBearer(tc.pol, emptyArgs, tc.req) + ctx := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) _, err := ctx.Args() require.NoError(t, err) @@ -152,7 +152,7 @@ func TestJsonRpcHash(t *testing.T) { err := invArgs.Add(JsonRpcArgsKey, tc.hash) require.NoError(t, err) - ctx := NewJsonRpcBearer(pol, invArgs.ReadOnly(), req) + ctx := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { require.NoError(t, ctx.Verify()) From f18ae547ab3c09af48a89a9652c29bfdd6dba60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 9 Jan 2025 13:41:09 +0100 Subject: [PATCH 17/38] Update server/extargs/http.go Co-authored-by: Steve Moyer --- toolkit/server/extargs/http.go | 1 + 1 file changed, 1 insertion(+) diff --git a/toolkit/server/extargs/http.go b/toolkit/server/extargs/http.go index cb33498..743ec0d 100644 --- a/toolkit/server/extargs/http.go +++ b/toolkit/server/extargs/http.go @@ -1,3 +1,4 @@ +// Package extargs adds external arguments to the invocation's arguments before the policy is evaluated. package extargs import ( From 09c8815755ec41843543c3c8855a5e38ad54af1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 9 Jan 2025 13:34:53 +0100 Subject: [PATCH 18/38] update go-ucan, with the now spec defined container https://github.com/ucan-wg/go-ucan/blob/main/pkg/container/SPEC.md --- toolkit/client/client_test.go | 4 ++-- toolkit/server/exectx/ucanctx_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/toolkit/client/client_test.go b/toolkit/client/client_test.go index 0e0d25f..5a310f5 100644 --- a/toolkit/client/client_test.go +++ b/toolkit/client/client_test.go @@ -36,10 +36,10 @@ func ExampleNewClient() { handleError(err) // this container holds the invocation and all the delegation proofs - b64, err := cont.ToCborBase64() + b64, err := cont.ToBase64StdPadding() handleError(err) - fmt.Println(string(b64)) + fmt.Println(b64) } func handleError(err error) { diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index 54b8dff..d891fdd 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -64,7 +64,7 @@ func ExampleContext() { cont := container.NewWriter() cont.AddSealed(dlgCid, dlgBytes) cont.AddSealed(invCid, invBytes) - contBytes, _ := cont.ToCborBase64() + contBytes, _ := cont.ToBase64StdPadding() // MAKING A REQUEST: we pass the container in the Bearer HTTP header @@ -84,7 +84,7 @@ func ExampleContext() { // Note: we obviously want something more robust, this is an example // Note: if an error occur, we'll want to return an HTTP 401 Unauthorized data := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - cont, _ := container.FromCborBase64(data) + cont, _ := container.FromString(data) ucanCtx, _ := exectx.FromContainer(cont) // insert into the go context From 3b6d70f47a3d3143d22d3373f61011e7719a3936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 16 Jan 2025 13:36:18 +0100 Subject: [PATCH 19/38] extargs: add ".inf" external args for arbitrary Infura args --- toolkit/server/exectx/ucanctx.go | 23 +++++ toolkit/server/extargs/http_test.go | 14 +-- toolkit/server/extargs/infura.go | 85 ++++++++++++++++++ toolkit/server/extargs/infura_test.go | 114 +++++++++++++++++++++++++ toolkit/server/extargs/jsonrpc_test.go | 14 +-- 5 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 toolkit/server/extargs/infura.go create mode 100644 toolkit/server/extargs/infura_test.go diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index e2a8999..6f87e55 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -8,6 +8,7 @@ import ( "github.com/INFURA/go-ethlibs/jsonrpc" "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" @@ -34,6 +35,7 @@ type UcanCtx struct { // argument sources http *extargs.HttpExtArgs jsonrpc *extargs.JsonRpcExtArgs + infura *extargs.InfuraExtArgs } func FromContainer(cont container.Reader) (*UcanCtx, error) { @@ -99,6 +101,7 @@ func (ctn UcanCtx) Meta() meta.ReadOnly { } // VerifyHttp verify the delegation's policies against arguments constructed from the HTTP request. +// These arguments will be set in the `.http` argument key, at the root. // This function can only be called once per context. // After being used, those constructed arguments will be used in ExecutionAllowed as well. func (ctn UcanCtx) VerifyHttp(req *http.Request) error { @@ -110,6 +113,7 @@ func (ctn UcanCtx) VerifyHttp(req *http.Request) error { } // VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request. +// These arguments will be set in the `.jsonrpc` argument key, at the root. // This function can only be called once per context. // After being used, those constructed arguments will be used in ExecutionAllowed as well. func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { @@ -120,6 +124,18 @@ func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { return ctn.jsonrpc.Verify() } +// VerifyInfura verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler. +// These arguments will be set in the `.inf` argument key, at the root. +// This function can only be called once per context. +// After being used, those constructed arguments will be used in ExecutionAllowed as well. +func (ctn UcanCtx) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error { + if ctn.infura != nil { + panic("only use once per request context") + } + ctn.infura = extargs.NewInfuraExtArgs(ctn.policies, assembler) + return ctn.infura.Verify() +} + // ExecutionAllowed does the final verification of the invocation. // If VerifyHttp or VerifyJsonRpc has been used, those arguments are part of the verification. func (ctn UcanCtx) ExecutionAllowed() error { @@ -140,6 +156,13 @@ func (ctn UcanCtx) ExecutionAllowed() error { } newArgs.Include(jsonRpcArgs) } + if ctn.infura != nil { + infuraArgs, err := ctn.infura.Args() + if err != nil { + return nil, err + } + newArgs.Include(infuraArgs) + } return newArgs, nil }) diff --git a/toolkit/server/extargs/http_test.go b/toolkit/server/extargs/http_test.go index 401bd3a..8ea454a 100644 --- a/toolkit/server/extargs/http_test.go +++ b/toolkit/server/extargs/http_test.go @@ -107,15 +107,15 @@ func TestHttp(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - ctx := NewHttpExtArgs(pol, emptyArgs, r) + extArgs := NewHttpExtArgs(pol, emptyArgs, r) - _, err := ctx.Args() + _, err := extArgs.Args() require.NoError(t, err) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } } @@ -173,12 +173,12 @@ func TestHttpHash(t *testing.T) { err := invArgs.Add(HttpArgsKey, tc.hash) require.NoError(t, err) - ctx := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) + extArgs := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } }) } diff --git a/toolkit/server/extargs/infura.go b/toolkit/server/extargs/infura.go new file mode 100644 index 0000000..ff6c066 --- /dev/null +++ b/toolkit/server/extargs/infura.go @@ -0,0 +1,85 @@ +package extargs + +import ( + "fmt" + "sync" + + "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/ucan-wg/go-ucan/pkg/args" + "github.com/ucan-wg/go-ucan/pkg/policy" +) + +const InfuraArgsKey = "inf" + +type InfuraExtArgs struct { + pol policy.Policy + originalArgs args.ReadOnly + assembler func(ma datamodel.MapAssembler) + + once sync.Once + args *args.Args + argsIpld ipld.Node +} + +func NewInfuraExtArgs(pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *InfuraExtArgs { + return &InfuraExtArgs{pol: pol, assembler: assembler} +} + +func (ia *InfuraExtArgs) Verify() error { + if err := ia.makeArgs(); err != nil { + return err + } + + // Note: InfuraExtArgs doesn't support verifying a hash computed client-side like the other + // external args, as the arguments are by nature dynamic. The client can't generate a meaningful hash. + + ok, leaf := ia.pol.PartialMatch(ia.argsIpld) + if !ok { + return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) + } + return nil +} + +func (ia *InfuraExtArgs) Args() (*args.Args, error) { + if err := ia.makeArgs(); err != nil { + return nil, err + } + return ia.args, nil +} + +func (ia *InfuraExtArgs) makeArgs() error { + var outerErr error + ia.once.Do(func() { + var err error + + ia.args, err = makeInfuraArgs(ia.assembler) + if err != nil { + outerErr = err + return + } + + ia.argsIpld, err = ia.args.ToIPLD() + if err != nil { + outerErr = err + return + } + }) + return outerErr +} + +func makeInfuraArgs(assembler func(ma datamodel.MapAssembler)) (*args.Args, error) { + n, err := qp.BuildMap(basicnode.Prototype.Any, -1, assembler) + if err != nil { + return nil, err + } + + res := args.New() + err = res.Add(InfuraArgsKey, n) + if err != nil { + return nil, err + } + return res, nil +} diff --git a/toolkit/server/extargs/infura_test.go b/toolkit/server/extargs/infura_test.go new file mode 100644 index 0000000..b8de085 --- /dev/null +++ b/toolkit/server/extargs/infura_test.go @@ -0,0 +1,114 @@ +package extargs + +import ( + "fmt" + "testing" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" +) + +func ExampleInfuraExtArgs() { + // Note: this is an example for how to build arguments, but you likely want to use InfuraExtArgs + // through UcanCtx. + + pol := policy.Policy{} // policies from the delegations + + // We will construct the following args: + // { + // "ntwk":"eth-mainnet", + // "quota":{ + // "ur":1234, + // "udc":1234, + // "arch":1234, + // "down":1234, + // "store":1234, + // "up":1234 + // } + // } + infArgs := NewInfuraExtArgs(pol, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) + qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + qp.MapEntry(ma, "udc", qp.Int(1234)) + qp.MapEntry(ma, "arch", qp.Int(1234)) + qp.MapEntry(ma, "down", qp.Int(1234)) + qp.MapEntry(ma, "store", qp.Int(1234)) + qp.MapEntry(ma, "up", qp.Int(1234)) + })) + }) + + err := infArgs.Verify() + fmt.Println(err) + + // Output: + // +} + +func TestInfura(t *testing.T) { + assembler := func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) + qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + qp.MapEntry(ma, "udc", qp.Int(1234)) + qp.MapEntry(ma, "arch", qp.Int(1234)) + qp.MapEntry(ma, "down", qp.Int(1234)) + qp.MapEntry(ma, "store", qp.Int(1234)) + qp.MapEntry(ma, "up", qp.Int(1234)) + })) + } + + tests := []struct { + name string + pol policy.Policy + expected bool + }{ + { + name: "no policies", + pol: policy.Policy{}, + expected: true, + }, + { + name: "matching args", + pol: policy.MustConstruct( + policy.Equal(".inf.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + ), + expected: true, + }, + { + name: "wrong network", + pol: policy.MustConstruct( + policy.Equal(".inf.ntwk", literal.String("avalanche-fuji")), + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + ), + expected: false, + }, + { + name: "unrespected quota", + pol: policy.MustConstruct( + policy.Equal(".inf.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(100)), + ), + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + extArgs := NewInfuraExtArgs(tc.pol, assembler) + + _, err := extArgs.Args() + require.NoError(t, err) + + if tc.expected { + require.NoError(t, extArgs.Verify()) + } else { + require.Error(t, extArgs.Verify()) + } + }) + } +} diff --git a/toolkit/server/extargs/jsonrpc_test.go b/toolkit/server/extargs/jsonrpc_test.go index 1346f1d..80a444d 100644 --- a/toolkit/server/extargs/jsonrpc_test.go +++ b/toolkit/server/extargs/jsonrpc_test.go @@ -97,15 +97,15 @@ func TestJsonRpc(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - ctx := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) + extArgs := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) - _, err := ctx.Args() + _, err := extArgs.Args() require.NoError(t, err) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } }) } @@ -152,12 +152,12 @@ func TestJsonRpcHash(t *testing.T) { err := invArgs.Add(JsonRpcArgsKey, tc.hash) require.NoError(t, err) - ctx := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) + extArgs := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { - require.NoError(t, ctx.Verify()) + require.NoError(t, extArgs.Verify()) } else { - require.Error(t, ctx.Verify()) + require.Error(t, extArgs.Verify()) } }) } From cf3eb1b3f73f57adff47e5e0a62060059cfa9482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 16 Jan 2025 15:16:01 +0100 Subject: [PATCH 20/38] bump go-ucan --- toolkit/client/client.go | 6 +++--- toolkit/client/proof.go | 2 ++ toolkit/client/requester.go | 2 ++ toolkit/server/exectx/ucanctx.go | 5 ++++- toolkit/server/exectx/ucanctx_test.go | 6 +++--- toolkit/server/extargs/http_test.go | 14 +++++++------- toolkit/server/extargs/jsonrpc_test.go | 14 +++++++------- 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/toolkit/client/client.go b/toolkit/client/client.go index d783605..f9cca09 100644 --- a/toolkit/client/client.go +++ b/toolkit/client/client.go @@ -60,18 +60,18 @@ func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject return nil, err } - invSealed, invCid, err := inv.ToSealed(c.privKey) + invSealed, _, err := inv.ToSealed(c.privKey) if err != nil { return nil, err } cont := container.NewWriter() - cont.AddSealed(invCid, invSealed) + cont.AddSealed(invSealed) for bundle, err := range c.pool.GetBundles(proof) { if err != nil { return nil, err } - cont.AddSealed(bundle.Cid, bundle.Sealed) + cont.AddSealed(bundle.Sealed) } return cont, nil diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go index 019a0be..fa6c701 100644 --- a/toolkit/client/proof.go +++ b/toolkit/client/proof.go @@ -14,6 +14,8 @@ import ( // - issuer: the DID of the client, also the issuer of the invocation token // - cmd: the command to execute // - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// The returned delegation chain is ordered starting from the leaf (the one matching the invocation) to the root +// (the one given by the service). // Note: you can read it as "(issuer) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. // Note: the implemented algorithm won't perform well with a large number of delegations. diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go index 8420a49..9cb3855 100644 --- a/toolkit/client/requester.go +++ b/toolkit/client/requester.go @@ -16,6 +16,8 @@ type DelegationRequester interface { // - cmd: the command to execute // - audience: the DID of the client, also the issuer of the invocation token // - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token + // The returned delegations MUST be ordered starting from the leaf (the one matching the invocation) to the root + // (the one given by the service). // Note: you can read it as "(audience) wants to do (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index 6f87e55..1f6a622 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -38,6 +38,9 @@ type UcanCtx struct { infura *extargs.InfuraExtArgs } +// FromContainer prepare a UcanCtx from a UCAN container, for further evaluation in a server pipeline. +// It is expected that the container holds a single invocation and the matching delegations. If not, +// an error is returned. func FromContainer(cont container.Reader) (*UcanCtx, error) { inv, err := cont.GetInvocation() if err != nil { @@ -95,7 +98,7 @@ func (ctn UcanCtx) Policies() policy.Policy { } // Meta returns all the meta values from the delegations. -// They are accumulated from the root delegation to the leaf delegation, with no overwrite. +// They are accumulated from the root delegation to the leaf delegation, with no overwriting. func (ctn UcanCtx) Meta() meta.ReadOnly { return ctn.meta.ReadOnly() } diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index d891fdd..29f3186 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -57,13 +57,13 @@ func ExampleContext() { invocation.WithExpirationIn(10*time.Minute), invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters ) - invBytes, invCid, _ := inv.ToSealed(user.PrivKey()) + invBytes, _, _ := inv.ToSealed(user.PrivKey()) // PACKAGING: no obligation for the transport, but the user needs to give the service the invocation // and all the proof delegations. We can use a container for that. cont := container.NewWriter() - cont.AddSealed(dlgCid, dlgBytes) - cont.AddSealed(invCid, invBytes) + cont.AddSealed(dlgBytes) + cont.AddSealed(invBytes) contBytes, _ := cont.ToBase64StdPadding() // MAKING A REQUEST: we pass the container in the Bearer HTTP header diff --git a/toolkit/server/extargs/http_test.go b/toolkit/server/extargs/http_test.go index 8ea454a..401bd3a 100644 --- a/toolkit/server/extargs/http_test.go +++ b/toolkit/server/extargs/http_test.go @@ -107,15 +107,15 @@ func TestHttp(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - extArgs := NewHttpExtArgs(pol, emptyArgs, r) + ctx := NewHttpExtArgs(pol, emptyArgs, r) - _, err := extArgs.Args() + _, err := ctx.Args() require.NoError(t, err) if tc.expected { - require.NoError(t, extArgs.Verify()) + require.NoError(t, ctx.Verify()) } else { - require.Error(t, extArgs.Verify()) + require.Error(t, ctx.Verify()) } } @@ -173,12 +173,12 @@ func TestHttpHash(t *testing.T) { err := invArgs.Add(HttpArgsKey, tc.hash) require.NoError(t, err) - extArgs := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) + ctx := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { - require.NoError(t, extArgs.Verify()) + require.NoError(t, ctx.Verify()) } else { - require.Error(t, extArgs.Verify()) + require.Error(t, ctx.Verify()) } }) } diff --git a/toolkit/server/extargs/jsonrpc_test.go b/toolkit/server/extargs/jsonrpc_test.go index 80a444d..1346f1d 100644 --- a/toolkit/server/extargs/jsonrpc_test.go +++ b/toolkit/server/extargs/jsonrpc_test.go @@ -97,15 +97,15 @@ func TestJsonRpc(t *testing.T) { // we don't test the args hash here emptyArgs := args.New().ReadOnly() - extArgs := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) + ctx := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) - _, err := extArgs.Args() + _, err := ctx.Args() require.NoError(t, err) if tc.expected { - require.NoError(t, extArgs.Verify()) + require.NoError(t, ctx.Verify()) } else { - require.Error(t, extArgs.Verify()) + require.Error(t, ctx.Verify()) } }) } @@ -152,12 +152,12 @@ func TestJsonRpcHash(t *testing.T) { err := invArgs.Add(JsonRpcArgsKey, tc.hash) require.NoError(t, err) - extArgs := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) + ctx := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) if tc.expected { - require.NoError(t, extArgs.Verify()) + require.NoError(t, ctx.Verify()) } else { - require.Error(t, extArgs.Verify()) + require.Error(t, ctx.Verify()) } }) } From 9e062b0cc76d62f3d6ea0537a70650fb72b87c94 Mon Sep 17 00:00:00 2001 From: rarquevaux Date: Fri, 17 Jan 2025 11:37:58 +0100 Subject: [PATCH 21/38] fix(ucanCtx): pointer receiver and fix to verifyHttp --- toolkit/server/exectx/ucanctx.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index 1f6a622..cc9ae9e 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -20,7 +20,7 @@ import ( "github.com/INFURA/go-ucan-toolkit/server/extargs" ) -var _ delegation.Loader = UcanCtx{} +var _ delegation.Loader = &UcanCtx{} // UcanCtx is a UCAN execution context meant to be inserted in the go context while handling a request. // It allows to handle the control of the execution in multiple steps across different middlewares, @@ -74,18 +74,18 @@ func FromContainer(cont container.Reader) (*UcanCtx, error) { } // Command returns the command triggered by the invocation. -func (ctn UcanCtx) Command() command.Command { +func (ctn *UcanCtx) Command() command.Command { return ctn.inv.Command() } // Invocation returns the invocation.Token. -func (ctn UcanCtx) Invocation() *invocation.Token { +func (ctn *UcanCtx) Invocation() *invocation.Token { return ctn.inv } // GetDelegation returns the delegation.Token matching the given CID. // If not found, delegation.ErrDelegationNotFound is returned. -func (ctn UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) { +func (ctn *UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) { if dlg, ok := ctn.dlgs[cid]; ok { return dlg, nil } @@ -93,13 +93,13 @@ func (ctn UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) { } // Policies return the full set of policy statements to satisfy. -func (ctn UcanCtx) Policies() policy.Policy { +func (ctn *UcanCtx) Policies() policy.Policy { return ctn.policies } // Meta returns all the meta values from the delegations. // They are accumulated from the root delegation to the leaf delegation, with no overwriting. -func (ctn UcanCtx) Meta() meta.ReadOnly { +func (ctn *UcanCtx) Meta() meta.ReadOnly { return ctn.meta.ReadOnly() } @@ -107,8 +107,8 @@ func (ctn UcanCtx) Meta() meta.ReadOnly { // These arguments will be set in the `.http` argument key, at the root. // This function can only be called once per context. // After being used, those constructed arguments will be used in ExecutionAllowed as well. -func (ctn UcanCtx) VerifyHttp(req *http.Request) error { - if ctn.http == nil { +func (ctn *UcanCtx) VerifyHttp(req *http.Request) error { + if ctn.http != nil { panic("only use once per request context") } ctn.http = extargs.NewHttpExtArgs(ctn.policies, ctn.inv.Arguments(), req) @@ -119,7 +119,7 @@ func (ctn UcanCtx) VerifyHttp(req *http.Request) error { // These arguments will be set in the `.jsonrpc` argument key, at the root. // This function can only be called once per context. // After being used, those constructed arguments will be used in ExecutionAllowed as well. -func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { +func (ctn *UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { if ctn.jsonrpc != nil { panic("only use once per request context") } @@ -131,7 +131,7 @@ func (ctn UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { // These arguments will be set in the `.inf` argument key, at the root. // This function can only be called once per context. // After being used, those constructed arguments will be used in ExecutionAllowed as well. -func (ctn UcanCtx) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error { +func (ctn *UcanCtx) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error { if ctn.infura != nil { panic("only use once per request context") } @@ -141,7 +141,7 @@ func (ctn UcanCtx) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error // ExecutionAllowed does the final verification of the invocation. // If VerifyHttp or VerifyJsonRpc has been used, those arguments are part of the verification. -func (ctn UcanCtx) ExecutionAllowed() error { +func (ctn *UcanCtx) ExecutionAllowed() error { return ctn.inv.ExecutionAllowedWithArgsHook(ctn, func(args args.ReadOnly) (*args.Args, error) { newArgs := args.WriteableClone() From cd9ee535ad0d670199241e6aca934403e36b2c14 Mon Sep 17 00:00:00 2001 From: rarquevaux Date: Fri, 17 Jan 2025 11:38:21 +0100 Subject: [PATCH 22/38] fix(ucanCtx): update test --- toolkit/server/exectx/ucanctx_test.go | 86 ++++++++++++++++++++------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index 29f3186..5011ae2 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -4,14 +4,16 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" + "net/http/httptest" "strings" "testing" "time" "github.com/INFURA/go-ethlibs/jsonrpc" "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/did/didtest" "github.com/ucan-wg/go-ucan/pkg/command" @@ -24,7 +26,11 @@ import ( "github.com/INFURA/go-ucan-toolkit/server/exectx" ) -func ExampleContext() { +const ( + network = "eth-mainnet" +) + +func TestCtx(t *testing.T) { // let's use some pre-made DID+privkey. // use go-ucan/did to generate or parse them. service := didtest.PersonaAlice @@ -44,35 +50,47 @@ func ExampleContext() { policy.Like(".jsonrpc.method", "eth_*"), policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), ), + // some infura constraints + // Network + policy.Equal(".inf.ntwk", literal.String(network)), + // Quota + policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), ) - dlg, _ := delegation.Root(service.DID(), user.DID(), cmd, pol, + dlg, err := delegation.Root(service.DID(), user.DID(), cmd, pol, delegation.WithExpirationIn(24*time.Hour), ) - dlgBytes, dlgCid, _ := dlg.ToSealed(service.PrivKey()) + require.NoError(t, err) + dlgBytes, dlgCid, err := dlg.ToSealed(service.PrivKey()) + require.NoError(t, err) // INVOCATION: the user leverages the delegation (power) to make a request. - inv, _ := invocation.New(user.DID(), cmd, service.DID(), []cid.Cid{dlgCid}, + inv, err := invocation.New(user.DID(), cmd, service.DID(), []cid.Cid{dlgCid}, invocation.WithExpirationIn(10*time.Minute), invocation.WithArgument("myarg", "hello"), // we can specify invocation parameters ) - invBytes, _, _ := inv.ToSealed(user.PrivKey()) + require.NoError(t, err) + invBytes, _, err := inv.ToSealed(user.PrivKey()) + require.NoError(t, err) // PACKAGING: no obligation for the transport, but the user needs to give the service the invocation // and all the proof delegations. We can use a container for that. cont := container.NewWriter() cont.AddSealed(dlgBytes) cont.AddSealed(invBytes) - contBytes, _ := cont.ToBase64StdPadding() + contBytes, err := cont.ToBase64StdPadding() + require.NoError(t, err) // MAKING A REQUEST: we pass the container in the Bearer HTTP header jrpc := jsonrpc.NewRequest() jrpc.Method = "eth_call" jrpc.Params = jsonrpc.MustParams("0x599784", true) - jrpcBytes, _ := jrpc.MarshalJSON() - req, _ := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes)) + jrpcBytes, err := jrpc.MarshalJSON() + require.NoError(t, err) + req, err := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes)) + require.NoError(t, err) req.Header.Set("Authorization", "Bearer "+string(contBytes)) // SERVER: Auth middleware @@ -84,13 +102,15 @@ func ExampleContext() { // Note: we obviously want something more robust, this is an example // Note: if an error occur, we'll want to return an HTTP 401 Unauthorized data := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") - cont, _ := container.FromString(data) - ucanCtx, _ := exectx.FromContainer(cont) + cont, err := container.FromString(data) + require.NoError(t, err) + ucanCtx, err := exectx.FromContainer(cont) + require.NoError(t, err) // insert into the go context - req = req.WithContext(exectx.AddUcanCtxToContext(req.Context(), ucanCtx)) + r = r.WithContext(exectx.AddUcanCtxToContext(r.Context(), ucanCtx)) - next.ServeHTTP(w, req) + next.ServeHTTP(w, r) }) } @@ -98,7 +118,8 @@ func ExampleContext() { httpMw := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ucanCtx, _ := exectx.FromContext(req.Context()) + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) err := ucanCtx.VerifyHttp(r) if err != nil { @@ -115,12 +136,14 @@ func ExampleContext() { jsonrpcMw := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ucanCtx, _ := exectx.FromContext(req.Context()) + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) var jrpc jsonrpc.Request - _ = json.NewDecoder(r.Body).Decode(&jrpc) + err := json.NewDecoder(r.Body).Decode(&jrpc) + require.NoError(t, err) - err := ucanCtx.VerifyJsonRpc(&jrpc) + err = ucanCtx.VerifyJsonRpc(&jrpc) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return @@ -129,21 +152,42 @@ func ExampleContext() { }) } + // SERVER: custom infura checks + + infuraMw := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) + err := ucanCtx.VerifyInfura(func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ntwk", qp.String(network)) + qp.MapEntry(ma, "quota", qp.Map(1, func(ma datamodel.MapAssembler) { + qp.MapEntry(ma, "ur", qp.Int(1234)) + })) + }) + require.NoError(t, err) + next.ServeHTTP(w, r) + }) + } + // SERVER: final handler handler := func(w http.ResponseWriter, r *http.Request) { - ucanCtx, _ := exectx.FromContext(req.Context()) + ucanCtx, ok := exectx.FromContext(r.Context()) + require.True(t, ok) if err := ucanCtx.ExecutionAllowed(); err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } - _, _ = fmt.Fprintln(w, "Success!") + w.WriteHeader(http.StatusOK) } - // Ready to go! - _ = http.ListenAndServe("", authMw(httpMw(jsonrpcMw(http.HandlerFunc(handler))))) + sut := authMw(httpMw(jsonrpcMw(infuraMw(http.HandlerFunc(handler))))) + + rec := httptest.NewRecorder() + sut.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) } func TestGoCtx(t *testing.T) { From 07d27459669405f884659301d7681def591cfd2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 15 Jan 2025 13:35:26 +0100 Subject: [PATCH 23/38] client,step1: add a KS compatible client, and a script to package tokens for PC step1 --- toolkit/client/requester_infura.go | 90 ++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 toolkit/client/requester_infura.go diff --git a/toolkit/client/requester_infura.go b/toolkit/client/requester_infura.go new file mode 100644 index 0000000..cae67a3 --- /dev/null +++ b/toolkit/client/requester_infura.go @@ -0,0 +1,90 @@ +package client + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "iter" + "net/http" + "net/url" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" +) + +var _ DelegationRequester = &InfuraRequester{} + +type InfuraRequester struct { + baseURL string +} + +// dev: http://ucan-issuer-api.commercial-dev.eks-dev.infura.org/ +// prod: +func NewInfuraRequester(baseURL string) *InfuraRequester { + return &InfuraRequester{baseURL: baseURL} +} + +func (i InfuraRequester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + p, err := url.JoinPath(i.baseURL, "v1/token/generate-with-did") + if err != nil { + return nil, err + } + + payload := struct { + Cmd string `json:"cmd"` + Aud string `json:"aud"` + }{ + Cmd: cmd.String(), + Aud: audience.String(), + } + + body, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + r, err := http.NewRequest(http.MethodPost, p, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + res, err := http.DefaultClient.Do(r.WithContext(ctx)) + if err != nil { + return nil, err + } + + resp := struct { + Cid string `json:"cid"` + Content string `json:"content"` + }{} + if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { + return nil, err + } + + raw, err := base64.StdEncoding.DecodeString(resp.Content) + if err != nil { + return nil, err + } + + tkn, c, err := delegation.FromSealed(raw) + if err != nil { + return nil, err + } + + // For sanity, we verify that the delegation we got matches the expected subject, + // meaning that we are talking to the expected issuer. + if tkn.Subject() != subject { + return nil, fmt.Errorf("received token has unexpected subject: expected %s, got %s", subject, tkn.Subject()) + } + + return func(yield func(*delegation.Bundle, error) bool) { + yield(&delegation.Bundle{ + Cid: c, + Decoded: tkn, + Sealed: raw, + }, nil) + }, nil +} From cc661f39360aed3c8935ada6cc4c6d1c477f7519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 22 Jan 2025 17:28:37 +0100 Subject: [PATCH 24/38] infura requester: properly handle failure --- toolkit/client/requester_infura.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/toolkit/client/requester_infura.go b/toolkit/client/requester_infura.go index cae67a3..b60f64b 100644 --- a/toolkit/client/requester_infura.go +++ b/toolkit/client/requester_infura.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "iter" "net/http" "net/url" @@ -21,8 +22,9 @@ type InfuraRequester struct { baseURL string } -// dev: http://ucan-issuer-api.commercial-dev.eks-dev.infura.org/ -// prod: +// NewInfuraRequester create a requester client for the Infura UCAN token issuer. +// dev: http://ucan-issuer-api.commercial-dev.eks-dev.infura.org +// prod: http://ucan-issuer-api.commercial-prod.eks.infura.org func NewInfuraRequester(baseURL string) *InfuraRequester { return &InfuraRequester{baseURL: baseURL} } @@ -55,6 +57,13 @@ func (i InfuraRequester) RequestDelegation(ctx context.Context, audience did.DID if err != nil { return nil, err } + if res.StatusCode != http.StatusOK { + msg, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("request failed with status %d, then failed to read response body: %w", res.StatusCode, err) + } + return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, msg) + } resp := struct { Cid string `json:"cid"` From 41d679dfab35deb1647ddb02ab95889db3480a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 23 Jan 2025 14:36:34 +0100 Subject: [PATCH 25/38] extargs: make the hash more convenient to use by directly returning the invocation.Option --- toolkit/server/extargs/http.go | 7 +++- toolkit/server/extargs/http_test.go | 53 +++++++++++++++++--------- toolkit/server/extargs/jsonrpc.go | 7 +++- toolkit/server/extargs/jsonrpc_test.go | 53 +++++++++++++++++--------- 4 files changed, 80 insertions(+), 40 deletions(-) diff --git a/toolkit/server/extargs/http.go b/toolkit/server/extargs/http.go index 743ec0d..e2062f6 100644 --- a/toolkit/server/extargs/http.go +++ b/toolkit/server/extargs/http.go @@ -15,6 +15,7 @@ import ( "github.com/multiformats/go-multihash" "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/invocation" ) // HttpArgsKey is the key in the args, used for: @@ -111,7 +112,9 @@ func (hea *HttpExtArgs) verifyHash() error { // If that hash is inserted at the HttpArgsKey key in the invocation arguments, // this increases the security as the UCAN token cannot be used with a different // HTTP request. -func MakeHttpHash(req *http.Request) ([]byte, error) { +// For convenience, the hash is returned as a read to use invocation argument. +func MakeHttpHash(req *http.Request) (invocation.Option, error) { + // Note: the hash is computed on the full IPLD args, including HttpArgsKey computedArgs, err := makeHttpArgs(req) if err != nil { return nil, err @@ -132,7 +135,7 @@ func MakeHttpHash(req *http.Request) ([]byte, error) { return nil, err } - return sum, nil + return invocation.WithArgument(HttpArgsKey, []byte(sum)), nil } func makeHttpArgs(req *http.Request) (*args.Args, error) { diff --git a/toolkit/server/extargs/http_test.go b/toolkit/server/extargs/http_test.go index 401bd3a..ebb20f7 100644 --- a/toolkit/server/extargs/http_test.go +++ b/toolkit/server/extargs/http_test.go @@ -7,9 +7,12 @@ import ( "github.com/multiformats/go-multihash" "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/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/invocation" ) func TestHttp(t *testing.T) { @@ -131,6 +134,9 @@ func TestHttp(t *testing.T) { } func TestHttpHash(t *testing.T) { + servicePersona := didtest.PersonaAlice + clientPersona := didtest.PersonaBob + req, err := http.NewRequest(http.MethodGet, "http://example.com/foo", nil) require.NoError(t, err) req.Header.Add("User-Agent", "Chrome/51.0.2704.103 Safari/537.36") @@ -140,40 +146,51 @@ func TestHttpHash(t *testing.T) { policy.Equal(".http.scheme", literal.String("http")), ) + makeArg := func(data []byte, code uint64) invocation.Option { + mh, err := multihash.Sum(data, code, -1) + require.NoError(t, err) + return invocation.WithArgument(HttpArgsKey, []byte(mh)) + } + tests := []struct { - name string - hash []byte - expected bool + name string + argOptions []invocation.Option + expected bool }{ { - name: "correct hash", - hash: must(MakeHttpHash(req)), - expected: true, + name: "correct hash", + argOptions: []invocation.Option{must(MakeHttpHash(req))}, + expected: true, }, { - name: "non-matching hash", - hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.SHA2_256, -1)), - expected: false, + name: "non-matching hash", + argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.SHA2_256)}, + expected: false, }, { - name: "wrong type of hash", - hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.BLAKE3, -1)), - expected: false, + name: "wrong type of hash", + argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.BLAKE3)}, + expected: false, }, { - name: "no hash", - hash: nil, - expected: false, + name: "no hash", + argOptions: nil, + expected: true, // having a hash is not enforced }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - invArgs := args.New() - err := invArgs.Add(HttpArgsKey, tc.hash) + inv, err := invocation.New( + clientPersona.DID(), + command.MustParse("/foo"), + servicePersona.DID(), + nil, + tc.argOptions..., // inject hash argument, if any + ) require.NoError(t, err) - ctx := NewHttpExtArgs(pol, invArgs.ReadOnly(), req) + ctx := NewHttpExtArgs(pol, inv.Arguments(), req) if tc.expected { require.NoError(t, ctx.Verify()) diff --git a/toolkit/server/extargs/jsonrpc.go b/toolkit/server/extargs/jsonrpc.go index 418f844..51e911e 100644 --- a/toolkit/server/extargs/jsonrpc.go +++ b/toolkit/server/extargs/jsonrpc.go @@ -16,6 +16,7 @@ import ( "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/invocation" ) // JsonRpcArgsKey is the key in the args, used for: @@ -112,7 +113,9 @@ func (jrea *JsonRpcExtArgs) verifyHash() error { // If that hash is inserted at the JsonRpcArgsKey key in the invocation arguments, // this increases the security as the UCAN token cannot be used with a different // JsonRPC request. -func MakeJsonRpcHash(req *jsonrpc.Request) ([]byte, error) { +// For convenience, the hash is returned as a read to use invocation argument. +func MakeJsonRpcHash(req *jsonrpc.Request) (invocation.Option, error) { + // Note: the hash is computed on the full IPLD args, including JsonRpcArgsKey computedArgs, err := makeJsonRpcArgs(req) if err != nil { return nil, err @@ -133,7 +136,7 @@ func MakeJsonRpcHash(req *jsonrpc.Request) ([]byte, error) { return nil, err } - return sum, nil + return invocation.WithArgument(JsonRpcArgsKey, []byte(sum)), nil } func makeJsonRpcArgs(req *jsonrpc.Request) (*args.Args, error) { diff --git a/toolkit/server/extargs/jsonrpc_test.go b/toolkit/server/extargs/jsonrpc_test.go index 1346f1d..ceb715d 100644 --- a/toolkit/server/extargs/jsonrpc_test.go +++ b/toolkit/server/extargs/jsonrpc_test.go @@ -6,9 +6,12 @@ import ( "github.com/INFURA/go-ethlibs/jsonrpc" "github.com/multiformats/go-multihash" "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/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/invocation" ) func TestJsonRpc(t *testing.T) { @@ -112,6 +115,9 @@ func TestJsonRpc(t *testing.T) { } func TestJsonRpcHash(t *testing.T) { + servicePersona := didtest.PersonaAlice + clientPersona := didtest.PersonaBob + req := jsonrpc.MustRequest(1839673506133526, "debug_traceCall", true, false, 1234, "ho_no", ) @@ -119,40 +125,51 @@ func TestJsonRpcHash(t *testing.T) { policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), ) + makeArg := func(data []byte, code uint64) invocation.Option { + mh, err := multihash.Sum(data, code, -1) + require.NoError(t, err) + return invocation.WithArgument(JsonRpcArgsKey, []byte(mh)) + } + tests := []struct { - name string - hash []byte - expected bool + name string + argOptions []invocation.Option + expected bool }{ { - name: "correct hash", - hash: must(MakeJsonRpcHash(req)), - expected: true, + name: "correct hash", + argOptions: []invocation.Option{must(MakeJsonRpcHash(req))}, + expected: true, }, { - name: "non-matching hash", - hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.SHA2_256, -1)), - expected: false, + name: "non-matching hash", + argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.SHA2_256)}, + expected: false, }, { - name: "wrong type of hash", - hash: must(multihash.Sum([]byte{1, 2, 3, 4}, multihash.BLAKE3, -1)), - expected: false, + name: "wrong type of hash", + argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.BLAKE3)}, + expected: false, }, { - name: "no hash", - hash: nil, - expected: false, + name: "no hash", + argOptions: nil, + expected: true, // having a hash is not enforced }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - invArgs := args.New() - err := invArgs.Add(JsonRpcArgsKey, tc.hash) + inv, err := invocation.New( + clientPersona.DID(), + command.MustParse("/foo"), + servicePersona.DID(), + nil, + tc.argOptions..., // inject hash argument, if any + ) require.NoError(t, err) - ctx := NewJsonRpcExtArgs(pol, invArgs.ReadOnly(), req) + ctx := NewJsonRpcExtArgs(pol, inv.Arguments(), req) if tc.expected { require.NoError(t, ctx.Verify()) From cc207aa202d983eeeb02fc378d57ca05fe5a7e4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 23 Jan 2025 14:39:53 +0100 Subject: [PATCH 26/38] bearer: add a package to add/extract a HTTP Authorisation bearer header --- toolkit/server/bearer/bearer.go | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 toolkit/server/bearer/bearer.go diff --git a/toolkit/server/bearer/bearer.go b/toolkit/server/bearer/bearer.go new file mode 100644 index 0000000..8328330 --- /dev/null +++ b/toolkit/server/bearer/bearer.go @@ -0,0 +1,64 @@ +package bearer + +import ( + "fmt" + "net/http" + "strings" + + "github.com/ucan-wg/go-ucan/pkg/container" +) + +var ErrNoUcan = fmt.Errorf("no ucan") +var ErrContainerMalformed = fmt.Errorf("malformed container") + +// ExtractBearerContainer extract a full UCAN container from an HTTP "Authorization: Bearer" header. +// It can return: +// - ErrNoUcan if no such HTTP header is found +// - ErrContainerMalformed if the container or token can't be decoded or if a token is invalid (bad signature ...) +func ExtractBearerContainer(h http.Header) (container.Reader, error) { + header := h.Get("Authorization") + if header == "" { + return nil, ErrNoUcan + } + + if !strings.HasPrefix(header, "Bearer ") { + return nil, ErrNoUcan + } + + // skip prefix + reader := strings.NewReader(header[len("Bearer "):]) + + // read container from any supported format + ctn, err := container.FromReader(reader) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrContainerMalformed, err) + } + + return ctn, nil +} + +// AddBearerContainer adds the given UCAN container into an `Authorization: Bearer` HTTP header. +// +// You should use this with HTTP/2 or HTTP/3, as both of those already compress their header. +func AddBearerContainer(h http.Header, container container.Writer) error { + str, err := container.ToBase64StdPadding() + if err != nil { + return err + } + + h.Set("Authorization", fmt.Sprintf("Bearer %s", str)) + return nil +} + +// AddBearerContainerCompressed adds the given UCAN container into an `Authorization: Bearer` HTTP header. +// +// You should use this with HTTP/1, as it doesn't compress its headers. +func AddBearerContainerCompressed(h http.Header, container container.Writer) error { + str, err := container.ToBase64StdPaddingGzipped() + if err != nil { + return err + } + + h.Set("Authorization", fmt.Sprintf("Bearer %s", str)) + return nil +} From b95e525cfbdb3a04fa22caaf016e50840e959613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 23 Jan 2025 14:40:09 +0100 Subject: [PATCH 27/38] exectx: add a minimal middleware to extract the HTTP bearer, and make the UCAN ctx --- toolkit/server/exectx/middleware.go | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 toolkit/server/exectx/middleware.go diff --git a/toolkit/server/exectx/middleware.go b/toolkit/server/exectx/middleware.go new file mode 100644 index 0000000..8b5dd2f --- /dev/null +++ b/toolkit/server/exectx/middleware.go @@ -0,0 +1,43 @@ +package exectx + +import ( + "errors" + "net/http" + + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +// Middleware returns an HTTP middleware tasked with: +// - extracting UCAN credentials from the `Authorization: Bearer ` HTTP header +// - performing basic checks, and returning HTTP errors if necessary +// - exposing those credentials in the go context as a UcanCtx for further usage +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctn, err := bearer.ExtractBearerContainer(r.Header) + if errors.Is(err, bearer.ErrNoUcan) { + http.Error(w, "no UCAN auth", http.StatusBadRequest) + return + } + if errors.Is(err, bearer.ErrContainerMalformed) { + http.Error(w, "malformed token", http.StatusBadRequest) + return + } + if err != nil { + // should not happen, defensive programming + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // prepare a UcanCtx from the container, for further evaluation in the server pipeline + ucanCtx, err := FromContainer(ctn) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // insert into the go context + r = r.WithContext(AddUcanCtxToContext(r.Context(), ucanCtx)) + + next.ServeHTTP(w, r) + }) +} From 1098a834fb41102b0ba5c8b9c6020955f2af0c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 29 Jan 2025 16:22:41 +0100 Subject: [PATCH 28/38] bearer: add some tests --- toolkit/client/proof_test.go | 9 ++- toolkit/server/bearer/bearer_test.go | 32 ++++++++++ toolkit/server/exectx/middleware.go | 43 -------------- toolkit/server/exectx/middlewares.go | 87 ++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 45 deletions(-) create mode 100644 toolkit/server/bearer/bearer_test.go delete mode 100644 toolkit/server/exectx/middleware.go create mode 100644 toolkit/server/exectx/middlewares.go diff --git a/toolkit/client/proof_test.go b/toolkit/client/proof_test.go index 5f0738c..cfca2c2 100644 --- a/toolkit/client/proof_test.go +++ b/toolkit/client/proof_test.go @@ -2,7 +2,6 @@ package client import ( "iter" - "slices" "testing" "github.com/stretchr/testify/require" @@ -14,7 +13,13 @@ import ( func TestFindProof(t *testing.T) { dlgs := func() iter.Seq[*delegation.Bundle] { - return slices.Values(delegationtest.AllBundles) + return func(yield func(*delegation.Bundle) bool) { + for _, bundle := range delegationtest.AllBundles { + if !yield(&bundle) { + return + } + } + } } require.Equal(t, delegationtest.ProofAliceBob, diff --git a/toolkit/server/bearer/bearer_test.go b/toolkit/server/bearer/bearer_test.go new file mode 100644 index 0000000..0070159 --- /dev/null +++ b/toolkit/server/bearer/bearer_test.go @@ -0,0 +1,32 @@ +package bearer + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/container/containertest" +) + +func TestHTTPBearer(t *testing.T) { + for _, fn := range []func(h http.Header, container container.Writer) error{ + AddBearerContainer, + AddBearerContainerCompressed, + } { + r, err := http.NewRequest(http.MethodPost, "/foo/bar", nil) + require.NoError(t, err) + + cont, err := container.FromBytes(containertest.Bytes) + require.NoError(t, err) + + err = fn(r.Header, cont.ToWriter()) + require.NoError(t, err) + + contRead, err := ExtractBearerContainer(r.Header) + require.NoError(t, err) + + require.NotEmpty(t, contRead) + require.Equal(t, cont, contRead) + } +} diff --git a/toolkit/server/exectx/middleware.go b/toolkit/server/exectx/middleware.go deleted file mode 100644 index 8b5dd2f..0000000 --- a/toolkit/server/exectx/middleware.go +++ /dev/null @@ -1,43 +0,0 @@ -package exectx - -import ( - "errors" - "net/http" - - "github.com/INFURA/go-ucan-toolkit/server/bearer" -) - -// Middleware returns an HTTP middleware tasked with: -// - extracting UCAN credentials from the `Authorization: Bearer ` HTTP header -// - performing basic checks, and returning HTTP errors if necessary -// - exposing those credentials in the go context as a UcanCtx for further usage -func Middleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctn, err := bearer.ExtractBearerContainer(r.Header) - if errors.Is(err, bearer.ErrNoUcan) { - http.Error(w, "no UCAN auth", http.StatusBadRequest) - return - } - if errors.Is(err, bearer.ErrContainerMalformed) { - http.Error(w, "malformed token", http.StatusBadRequest) - return - } - if err != nil { - // should not happen, defensive programming - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // prepare a UcanCtx from the container, for further evaluation in the server pipeline - ucanCtx, err := FromContainer(ctn) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // insert into the go context - r = r.WithContext(AddUcanCtxToContext(r.Context(), ucanCtx)) - - next.ServeHTTP(w, r) - }) -} diff --git a/toolkit/server/exectx/middlewares.go b/toolkit/server/exectx/middlewares.go new file mode 100644 index 0000000..c2d96c4 --- /dev/null +++ b/toolkit/server/exectx/middlewares.go @@ -0,0 +1,87 @@ +package exectx + +import ( + "errors" + "net/http" + + "github.com/ucan-wg/go-ucan/did" + + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +// ExtractMW returns an HTTP middleware tasked with: +// - extracting UCAN credentials from the `Authorization: Bearer ` HTTP header +// - performing basic checks, and returning HTTP errors if necessary +// - verify that the invocation targets our service +// - exposing those credentials in the go context as a UcanCtx for further usage +func ExtractMW(next http.Handler, serviceDID did.DID) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctn, err := bearer.ExtractBearerContainer(r.Header) + if errors.Is(err, bearer.ErrNoUcan) { + http.Error(w, "no UCAN auth", http.StatusBadRequest) + return + } + if errors.Is(err, bearer.ErrContainerMalformed) { + http.Error(w, "malformed token", http.StatusBadRequest) + return + } + if err != nil { + // should not happen, defensive programming + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // prepare a UcanCtx from the container, for further evaluation in the server pipeline + ucanCtx, err := FromContainer(ctn) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if ucanCtx.Invocation().Subject() != serviceDID { + http.Error(w, "UCAN delegation doesn't match the service DID", http.StatusUnauthorized) + return + } + + // insert into the go context + r = r.WithContext(AddUcanCtxToContext(r.Context(), ucanCtx)) + + next.ServeHTTP(w, r) + }) +} + +// HttpExtArgsVerify returns an HTTP middleware tasked with verifying the UCAN policies applying on the HTTP request. +func HttpExtArgsVerify(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := FromContext(r.Context()) + if !ok { + http.Error(w, "no ucan-ctx found", http.StatusInternalServerError) + return + } + + if err := ucanCtx.VerifyHttp(r); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} + +// EnforceMW returns an HTTP middleware tasked with the final verification of the UCAN policies. +func EnforceMW(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := FromContext(r.Context()) + if !ok { + http.Error(w, "no ucan-ctx found", http.StatusInternalServerError) + return + } + + if err := ucanCtx.ExecutionAllowed(); err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + }) +} From 55f38fef4a628b6cc57f408951b57cda3ae95704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 3 Feb 2025 16:17:30 +0100 Subject: [PATCH 29/38] add server/issuer/client examples, and lots of sanding --- toolkit/_example/client/client.go | 151 ++++++++++++++++++++ toolkit/_example/issuer/issuer.go | 119 +++++++++++++++ toolkit/_example/server/server.go | 85 +++++++++++ toolkit/_example/shared_values.go | 24 ++++ toolkit/issuer/{issuer.go => dlg_issuer.go} | 35 +++-- toolkit/issuer/http_wrapper.go | 83 +++++++++++ toolkit/issuer/root_issuer.go | 78 ++++++++++ 7 files changed, 563 insertions(+), 12 deletions(-) create mode 100644 toolkit/_example/client/client.go create mode 100644 toolkit/_example/issuer/issuer.go create mode 100644 toolkit/_example/server/server.go create mode 100644 toolkit/_example/shared_values.go rename toolkit/issuer/{issuer.go => dlg_issuer.go} (81%) create mode 100644 toolkit/issuer/http_wrapper.go create mode 100644 toolkit/issuer/root_issuer.go diff --git a/toolkit/_example/client/client.go b/toolkit/_example/client/client.go new file mode 100644 index 0000000..7db6c7d --- /dev/null +++ b/toolkit/_example/client/client.go @@ -0,0 +1,151 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "iter" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/token/delegation" + + example "github.com/INFURA/go-ucan-toolkit/_example" + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/issuer" + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.IssuerUrl, example.ServerUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +var _ client.DelegationRequester = &requester{} + +type requester struct { + issuerURL string +} + +func (r requester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + log.Printf("requesting delegation for %s on %s", cmd, subject) + + // we match the simple json protocol of the issuer + data := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{ + Audience: audience.String(), + Cmd: cmd.String(), + Subject: subject.String(), + } + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(data) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, "http://"+r.issuerURL, buf) + if err != nil { + return nil, err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + + return issuer.DecodeResponse(res) +} + +func run(ctx context.Context, issuerUrl string, serverUrl string, serviceDid did.DID) error { + // Let's generate a keypair for our client: + priv, d, err := did.GenerateEd25519() + if err != nil { + return err + } + + log.Printf("client DID is %s", d.String()) + + cli, err := client.NewClient(priv, requester{issuerURL: issuerUrl}) + if err != nil { + return err + } + + for { + select { + case <-ctx.Done(): + case <-time.After(5 * time.Second): + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serverUrl, proofs) + if err != nil { + log.Println(err) + } + } + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serverUrl string, proofs container.Writer) error { + // we construct a URL that include the client DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serverUrl, clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/issuer/issuer.go b/toolkit/_example/issuer/issuer.go new file mode 100644 index 0000000..2cc4890 --- /dev/null +++ b/toolkit/_example/issuer/issuer.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + + example "github.com/INFURA/go-ucan-toolkit/_example" + "github.com/INFURA/go-ucan-toolkit/issuer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.IssuerUrl, example.ServicePrivKey) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) error { + issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) { + log.Printf("issuing delegation to %v for %v", aud, cmd) + + // We construct an arbitrary policy. + // Here, we enforce that the caller uses its own DID endpoint (an arbitrary construct for this example). + // You will notice that the server doesn't need to know about this logic to enforce it. + policies, err := policy.Construct( + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s", aud.String()))), + ) + if err != nil { + return nil, err + } + + return delegation.Root(iss, aud, cmd, policies, + // let's add an expiration, this will force the client to renew its token. + delegation.WithExpirationIn(10*time.Second), + ) + } + + rootIssuer, err := issuer.NewRootIssuer(servicePrivKey, issuingLogic) + if err != nil { + return err + } + + handler := issuer.HttpWrapper(rootIssuer, func(r *http.Request) (*issuer.ResolvedRequest, error) { + // Let's make up a simple json protocol + req := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return nil, err + } + aud, err := did.Parse(req.Audience) + if err != nil { + return nil, err + } + cmd, err := command.Parse(req.Cmd) + if err != nil { + return nil, err + } + sub, err := did.Parse(req.Subject) + if err != nil { + return nil, err + } + return &issuer.ResolvedRequest{ + Audience: aud, + Cmd: cmd, + Subject: sub, + }, nil + }) + + srv := &http.Server{ + Addr: issuerUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + return err + } + return nil +} diff --git a/toolkit/_example/server/server.go b/toolkit/_example/server/server.go new file mode 100644 index 0000000..726042b --- /dev/null +++ b/toolkit/_example/server/server.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "errors" + "log" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/ucan-wg/go-ucan/did" + + example "github.com/INFURA/go-ucan-toolkit/_example" + "github.com/INFURA/go-ucan-toolkit/server/exectx" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.ServerUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { + // we'll make a simple handling pipeline: + // - exectx.ExtractMW to extract and decode the UCAN context, verify the service DID + // - exectx.HttpExtArgsVerify to verify the HTTP policies + // - exectx.EnforceMW to perform the final UCAN checks + // - our handler to execute the commands + + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ucanCtx, ok := exectx.FromContext(r.Context()) + if !ok { + http.Error(w, "no ucan-ctx found", http.StatusInternalServerError) + return + } + + switch ucanCtx.Command().String() { + case "/foo/bar": + log.Printf("handled command %v for %v", ucanCtx.Command(), ucanCtx.Invocation().Issuer()) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + default: + http.Error(w, "unknown UCAN commmand", http.StatusBadRequest) + return + } + }) + + handler = exectx.EnforceMW(handler) + handler = exectx.HttpExtArgsVerify(handler) + handler = exectx.ExtractMW(handler, serviceDID) + + srv := &http.Server{ + Addr: serverUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + return err + } + return nil +} diff --git a/toolkit/_example/shared_values.go b/toolkit/_example/shared_values.go new file mode 100644 index 0000000..d71f24d --- /dev/null +++ b/toolkit/_example/shared_values.go @@ -0,0 +1,24 @@ +package example + +import ( + "encoding/base64" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" +) + +// Endpoints + +var ServerUrl = ":8080" +var IssuerUrl = ":8081" + +// Service + +var ServicePrivKey crypto.PrivKey +var ServiceDid did.DID + +func init() { + privRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=") + ServicePrivKey, _ = crypto.UnmarshalPrivateKey(privRaw) + ServiceDid, _ = did.FromPrivKey(ServicePrivKey) +} diff --git a/toolkit/issuer/issuer.go b/toolkit/issuer/dlg_issuer.go similarity index 81% rename from toolkit/issuer/issuer.go rename to toolkit/issuer/dlg_issuer.go index 6403d47..00ac364 100644 --- a/toolkit/issuer/issuer.go +++ b/toolkit/issuer/dlg_issuer.go @@ -2,6 +2,7 @@ package issuer import ( "context" + "fmt" "iter" "time" @@ -14,18 +15,31 @@ import ( "github.com/INFURA/go-ucan-toolkit/client" ) +// DlgIssuingLogic is a function that decides what powers are given to a client. +// - issuer: the DID of our issuer +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token +// Note: you can read it as "(audience) wants to do (cmd) on (subject)". +// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the +// expected action. If you don't want to give that power, return an error instead. +type DlgIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) + var _ client.DelegationRequester = &Issuer{} +// Issuer is an implementation of a re-delegating issuer. +// Note: Your actual needs for an issuer can easily be different (caching...) than the choices made here. +// Feel free to replace this component with your own flavor. type Issuer struct { did did.DID privKey crypto.PrivKey pool *client.Pool requester client.DelegationRequester - logic IssuingLogic + logic DlgIssuingLogic } -func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic IssuingLogic) (*Issuer, error) { +func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic DlgIssuingLogic) (*Issuer, error) { d, err := did.FromPrivKey(privKey) if err != nil { return nil, err @@ -39,16 +53,6 @@ func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, log }, nil } -// IssuingLogic is a function that decides what powers are given to a client. -// - issuer: the DID of our issuer -// - audience: the DID of the client, also the issuer of the invocation token -// - cmd: the command to execute -// - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token -// Note: you can read it as "(audience) wants to do (cmd) on (subject)". -// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the -// expected action. If you don't want to give that power, return an error instead. -type IssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) - // RequestDelegation retrieve chain of delegation for the given parameters. // - audience: the DID of the client, also the issuer of the invocation token // - cmd: the command to execute @@ -56,6 +60,10 @@ type IssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject di // Note: you can read it as "(audience) does (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + if subject != i.did { + return nil, fmt.Errorf("subject DID doesn't match the issuer DID") + } + var proof []cid.Cid // is there already a valid proof chain? @@ -86,6 +94,9 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co if err != nil { return nil, err } + if dlg.IsRoot() { + return nil, fmt.Errorf("issuing logic should return a non-root delegation") + } // sign and cache the new token dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey) diff --git a/toolkit/issuer/http_wrapper.go b/toolkit/issuer/http_wrapper.go new file mode 100644 index 0000000..a0a095f --- /dev/null +++ b/toolkit/issuer/http_wrapper.go @@ -0,0 +1,83 @@ +package issuer + +import ( + "fmt" + "io" + "iter" + "net/http" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/token/delegation" + + "github.com/INFURA/go-ucan-toolkit/client" +) + +type RequestResolver func(r *http.Request) (*ResolvedRequest, error) + +type ResolvedRequest struct { + Audience did.DID + Cmd command.Command + Subject did.DID +} + +// HttpWrapper implements an HTTP transport for a UCAN issuer. +// It provides a common response shape, while allowing customisation of the request. +func HttpWrapper(requester client.DelegationRequester, resolver RequestResolver) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + resolved, err := resolver(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + dlgs, err := requester.RequestDelegation(r.Context(), resolved.Audience, resolved.Cmd, resolved.Subject) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + cont := container.NewWriter() + for bundle, err := range dlgs { + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + cont.AddSealed(bundle.Sealed) + } + + err = cont.ToBytesGzippedWriter(w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} + +func DecodeResponse(res *http.Response) (iter.Seq2[*delegation.Bundle, error], error) { + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + msg, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("request failed with status %d, then failed to read response body: %w", res.StatusCode, err) + } + return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, msg) + } + cont, err := container.FromReader(res.Body) + if err != nil { + return nil, err + } + return func(yield func(*delegation.Bundle, error) bool) { + for bundle := range cont.GetAllDelegations() { + if !yield(&bundle, nil) { + return + } + } + }, nil +} diff --git a/toolkit/issuer/root_issuer.go b/toolkit/issuer/root_issuer.go new file mode 100644 index 0000000..5723a8c --- /dev/null +++ b/toolkit/issuer/root_issuer.go @@ -0,0 +1,78 @@ +package issuer + +import ( + "context" + "fmt" + "iter" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + + "github.com/INFURA/go-ucan-toolkit/client" +) + +// RootIssuingLogic is a function that decides what powers are given to a client. +// - issuer: the DID of our issuer +// - audience: the DID of the client, also the issuer of the invocation token +// - cmd: the command to execute +// Note: you can read it as "(audience) wants to do (cmd) on (subject)". +// Note: you can decide to match the input parameters exactly or issue a broader power, as long as it allows the +// expected action. If you don't want to give that power, return an error instead. +type RootIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) + +var _ client.DelegationRequester = &RootIssuer{} + +// RootIssuer is an implementation of a root delegation issuer. +// Note: Your actual needs for an issuer can easily be different (caching...) than the choices made here. +// Feel free to replace this component with your own flavor. +type RootIssuer struct { + did did.DID + privKey crypto.PrivKey + + logic RootIssuingLogic +} + +func NewRootIssuer(privKey crypto.PrivKey, logic RootIssuingLogic) (*RootIssuer, error) { + d, err := did.FromPrivKey(privKey) + if err != nil { + return nil, err + } + return &RootIssuer{ + did: d, + privKey: privKey, + logic: logic, + }, nil +} + +func (r *RootIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + if subject != r.did { + return nil, fmt.Errorf("subject DID doesn't match the issuer DID") + } + + // run the custom logic to get what we actually issue + dlg, err := r.logic(r.did, audience, cmd) + if err != nil { + return nil, err + } + if !dlg.IsRoot() { + return nil, fmt.Errorf("issuing logic should return a root delegation") + } + + // sign and cache the new token + dlgBytes, dlgCid, err := dlg.ToSealed(r.privKey) + if err != nil { + return nil, err + } + bundle := &delegation.Bundle{ + Cid: dlgCid, + Decoded: dlg, + Sealed: dlgBytes, + } + + // output the root delegation + return func(yield func(*delegation.Bundle, error) bool) { + yield(bundle, nil) + }, nil +} From c670433335c96f30aed834c2368ceb5d26912321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 6 Feb 2025 14:19:30 +0100 Subject: [PATCH 30/38] example: add sub-delegation --- toolkit/_example/Readme.md | 9 ++ .../_protocol-issuer/request-resolver.go | 41 +++++ .../_example/_protocol-issuer/requester.go | 59 +++++++ toolkit/_example/alice-client-issuer/alice.go | 153 ++++++++++++++++++ toolkit/_example/bob-client/bob.go | 111 +++++++++++++ toolkit/_example/client/client.go | 151 ----------------- toolkit/_example/diagram.puml | 57 +++++++ toolkit/_example/scenario1.png | Bin 0 -> 21304 bytes toolkit/_example/scenario2.png | Bin 0 -> 38186 bytes .../{issuer => service-issuer}/issuer.go | 41 ++--- .../{server/server.go => service/service.go} | 11 +- toolkit/_example/shared_values.go | 19 ++- .../dlg_issuer.go => client/clientissuer.go} | 54 ++----- 13 files changed, 478 insertions(+), 228 deletions(-) create mode 100644 toolkit/_example/Readme.md create mode 100644 toolkit/_example/_protocol-issuer/request-resolver.go create mode 100644 toolkit/_example/_protocol-issuer/requester.go create mode 100644 toolkit/_example/alice-client-issuer/alice.go create mode 100644 toolkit/_example/bob-client/bob.go delete mode 100644 toolkit/_example/client/client.go create mode 100644 toolkit/_example/diagram.puml create mode 100644 toolkit/_example/scenario1.png create mode 100644 toolkit/_example/scenario2.png rename toolkit/_example/{issuer => service-issuer}/issuer.go (70%) rename toolkit/_example/{server/server.go => service/service.go} (82%) rename toolkit/{issuer/dlg_issuer.go => client/clientissuer.go} (61%) diff --git a/toolkit/_example/Readme.md b/toolkit/_example/Readme.md new file mode 100644 index 0000000..ce99267 --- /dev/null +++ b/toolkit/_example/Readme.md @@ -0,0 +1,9 @@ +![scenario 1](scenario1.png) +![scenario 2](scenario2.png) + +TODO + +- differences with a real system + - issuer protocol + token exchange + - opinionated with HTTP +- toolkit is helpers, you can change or write your own thing \ No newline at end of file diff --git a/toolkit/_example/_protocol-issuer/request-resolver.go b/toolkit/_example/_protocol-issuer/request-resolver.go new file mode 100644 index 0000000..ff7497a --- /dev/null +++ b/toolkit/_example/_protocol-issuer/request-resolver.go @@ -0,0 +1,41 @@ +package protocol + +import ( + "encoding/json" + "net/http" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + + "github.com/INFURA/go-ucan-toolkit/issuer" +) + +func RequestResolver(r *http.Request) (*issuer.ResolvedRequest, error) { + // Let's make up a simple json protocol + req := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + return nil, err + } + aud, err := did.Parse(req.Audience) + if err != nil { + return nil, err + } + cmd, err := command.Parse(req.Cmd) + if err != nil { + return nil, err + } + sub, err := did.Parse(req.Subject) + if err != nil { + return nil, err + } + return &issuer.ResolvedRequest{ + Audience: aud, + Cmd: cmd, + Subject: sub, + }, nil +} diff --git a/toolkit/_example/_protocol-issuer/requester.go b/toolkit/_example/_protocol-issuer/requester.go new file mode 100644 index 0000000..fa98c79 --- /dev/null +++ b/toolkit/_example/_protocol-issuer/requester.go @@ -0,0 +1,59 @@ +package protocol + +import ( + "bytes" + "context" + "encoding/json" + "iter" + "log" + "net/http" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/token/delegation" + + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/issuer" +) + +var _ client.DelegationRequester = &Requester{} + +type Requester struct { + issuerURL string +} + +func NewRequester(issuerURL string) *Requester { + return &Requester{issuerURL: issuerURL} +} + +func (r Requester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { + log.Printf("requesting delegation for %s on %s", cmd, subject) + + // we match the simple json protocol of the issuer + data := struct { + Audience string `json:"aud"` + Cmd string `json:"cmd"` + Subject string `json:"sub"` + }{ + Audience: audience.String(), + Cmd: cmd.String(), + Subject: subject.String(), + } + buf := &bytes.Buffer{} + err := json.NewEncoder(buf).Encode(data) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, "http://"+r.issuerURL, buf) + if err != nil { + return nil, err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + + return issuer.DecodeResponse(res) +} diff --git a/toolkit/_example/alice-client-issuer/alice.go b/toolkit/_example/alice-client-issuer/alice.go new file mode 100644 index 0000000..85e2190 --- /dev/null +++ b/toolkit/_example/alice-client-issuer/alice.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/pkg/policy/literal" + "github.com/ucan-wg/go-ucan/token/delegation" + + example "github.com/INFURA/go-ucan-toolkit/_example" + protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/issuer" + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, + example.AliceIssuerUrl, example.AlicePrivKey, example.AliceDid, + example.ServiceIssuerUrl, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DID, + serviceIssuerUrl string, serviceUrl string, serviceDid did.DID) error { + log.Printf("Alice DID is %s", d.String()) + + issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) { + log.Printf("issuing delegation to %v for %v to operate on %v", aud, cmd, subject) + + // As another example, we'll force Bob to use a specific HTTP sub-path + policies, err := policy.Construct( + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s/%s", iss.String(), aud.String()))), + ) + if err != nil { + return nil, err + } + + return delegation.New(iss, aud, cmd, policies, subject) + } + + cli, err := client.NewWithIssuer(priv, protocol.NewRequester(serviceIssuerUrl), issuingLogic) + if err != nil { + return err + } + + go startIssuerHttp(ctx, ownIssuerUrl, cli) + + for { + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serviceUrl, proofs) + if err != nil { + log.Println(err) + } + + select { + case <-ctx.Done(): + return nil + case <-time.After(20 * time.Second): + } + } +} + +func startIssuerHttp(ctx context.Context, issuerUrl string, cli *client.WithIssuer) { + handler := issuer.HttpWrapper(cli, protocol.RequestResolver) + + srv := &http.Server{ + Addr: issuerUrl, + Handler: handler, + } + + go func() { + if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("listen: %s\n", err) + } + }() + + log.Printf("issuer listening on %s\n", srv.Addr) + + <-ctx.Done() + + if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) { + log.Printf("issuer error: %v\n", err) + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, proofs container.Writer) error { + // we construct a URL that include the client DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serviceUrl, clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/bob-client/bob.go b/toolkit/_example/bob-client/bob.go new file mode 100644 index 0000000..9b7ae0f --- /dev/null +++ b/toolkit/_example/bob-client/bob.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "syscall" + "time" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/pkg/container" + + example "github.com/INFURA/go-ucan-toolkit/_example" + protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" + "github.com/INFURA/go-ucan-toolkit/client" + "github.com/INFURA/go-ucan-toolkit/server/bearer" +) + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + + // register as handler of the interrupt signal to trigger the teardown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + + go func() { + <-quit + cancel() + }() + + err := run(ctx, example.AliceIssuerUrl, example.AliceDid, example.ServiceUrl, example.ServiceDid) + if err != nil { + log.Println(err) + os.Exit(1) + } +} + +func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl string, serviceDid did.DID) error { + // Let's generate a keypair for our client: + priv, d, err := did.GenerateEd25519() + if err != nil { + return err + } + + log.Printf("Bob DID is %s", d.String()) + + cli, err := client.NewClient(priv, protocol.NewRequester(aliceUrl)) + if err != nil { + return err + } + + for { + proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) + if err != nil { + return err + } + + err = makeRequest(ctx, d, serverUrl, aliceDid, proofs) + if err != nil { + log.Println(err) + } + + select { + case <-ctx.Done(): + return nil + case <-time.After(5 * time.Second): + } + } +} + +func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, aliceDid did.DID, proofs container.Writer) error { + // we construct a URL that include the our DID and Alice DID as path, as requested by the UCAN policy we get issued + u, err := url.JoinPath("http://"+serviceUrl, aliceDid.String(), clientDid.String()) + if err != nil { + return err + } + + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return err + } + + err = bearer.AddBearerContainerCompressed(req.Header, proofs) + if err != nil { + return err + } + + res, err := http.DefaultClient.Do(req.WithContext(ctx)) + if err != nil { + return err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) + } + return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) + } + + log.Printf("response status code: %d", res.StatusCode) + + return nil +} diff --git a/toolkit/_example/client/client.go b/toolkit/_example/client/client.go deleted file mode 100644 index 7db6c7d..0000000 --- a/toolkit/_example/client/client.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "iter" - "log" - "net/http" - "net/url" - "os" - "os/signal" - "syscall" - "time" - - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/pkg/container" - "github.com/ucan-wg/go-ucan/token/delegation" - - example "github.com/INFURA/go-ucan-toolkit/_example" - "github.com/INFURA/go-ucan-toolkit/client" - "github.com/INFURA/go-ucan-toolkit/issuer" - "github.com/INFURA/go-ucan-toolkit/server/bearer" -) - -func main() { - ctx, cancel := context.WithCancel(context.Background()) - - // register as handler of the interrupt signal to trigger the teardown - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) - - go func() { - <-quit - cancel() - }() - - err := run(ctx, example.IssuerUrl, example.ServerUrl, example.ServiceDid) - if err != nil { - log.Println(err) - os.Exit(1) - } -} - -var _ client.DelegationRequester = &requester{} - -type requester struct { - issuerURL string -} - -func (r requester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { - log.Printf("requesting delegation for %s on %s", cmd, subject) - - // we match the simple json protocol of the issuer - data := struct { - Audience string `json:"aud"` - Cmd string `json:"cmd"` - Subject string `json:"sub"` - }{ - Audience: audience.String(), - Cmd: cmd.String(), - Subject: subject.String(), - } - buf := &bytes.Buffer{} - err := json.NewEncoder(buf).Encode(data) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, "http://"+r.issuerURL, buf) - if err != nil { - return nil, err - } - - res, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return nil, err - } - - return issuer.DecodeResponse(res) -} - -func run(ctx context.Context, issuerUrl string, serverUrl string, serviceDid did.DID) error { - // Let's generate a keypair for our client: - priv, d, err := did.GenerateEd25519() - if err != nil { - return err - } - - log.Printf("client DID is %s", d.String()) - - cli, err := client.NewClient(priv, requester{issuerURL: issuerUrl}) - if err != nil { - return err - } - - for { - select { - case <-ctx.Done(): - case <-time.After(5 * time.Second): - proofs, err := cli.PrepareInvoke(ctx, command.MustParse("/foo/bar"), serviceDid) - if err != nil { - return err - } - - err = makeRequest(ctx, d, serverUrl, proofs) - if err != nil { - log.Println(err) - } - } - } -} - -func makeRequest(ctx context.Context, clientDid did.DID, serverUrl string, proofs container.Writer) error { - // we construct a URL that include the client DID as path, as requested by the UCAN policy we get issued - u, err := url.JoinPath("http://"+serverUrl, clientDid.String()) - if err != nil { - return err - } - - req, err := http.NewRequest(http.MethodGet, u, nil) - if err != nil { - return err - } - - err = bearer.AddBearerContainerCompressed(req.Header, proofs) - if err != nil { - return err - } - - res, err := http.DefaultClient.Do(req.WithContext(ctx)) - if err != nil { - return err - } - - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - body, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("unexpected status code: %d, error reading body: %w", res.StatusCode, err) - } - return fmt.Errorf("unexpected status code: %d, body: %v", res.StatusCode, string(body)) - } - - log.Printf("response status code: %d", res.StatusCode) - - return nil -} diff --git a/toolkit/_example/diagram.puml b/toolkit/_example/diagram.puml new file mode 100644 index 0000000..9891a39 --- /dev/null +++ b/toolkit/_example/diagram.puml @@ -0,0 +1,57 @@ +@startuml +left to right direction + +rectangle Service as owner { + rectangle Issuer as issuer + rectangle Executor as exec +} + +node resource as res + +owner --> res : Controls +exec --> res : Allow access to + +rectangle "Alice" as alice { + rectangle Client as aliceclient + +} + +aliceclient --> issuer : [1] request delegation +aliceclient <-- issuer : [2] issue delegation A + +aliceclient --> exec : [3] make request with A +@enduml + + +@startuml +left to right direction + +rectangle Service as owner { + rectangle Issuer as issuer + rectangle Executor as exec +} + +node resource as res + +owner --> res : Controls +exec --> res : Allow access to + +rectangle "Alice" as alice { + rectangle Client as aliceclient + rectangle Issuer as aliceissuer +} + +aliceclient --> issuer : [1] request delegation +aliceclient <-- issuer : [2] issue delegation A + +aliceclient --> exec : [3] make request with A + +rectangle "Bob" as bob { + rectangle Client as bobclient +} + +bobclient --> aliceissuer : [4] request delegation +bobclient <-- aliceissuer : [5] issue delegation B\nalso returns A + +bobclient -down-> exec : [6] make request with A+B +@enduml \ No newline at end of file diff --git a/toolkit/_example/scenario1.png b/toolkit/_example/scenario1.png new file mode 100644 index 0000000000000000000000000000000000000000..91851be6939065f753e2c995c0e718b99e5bf33b GIT binary patch literal 21304 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 literal 0 HcmV?d00001 diff --git a/toolkit/_example/issuer/issuer.go b/toolkit/_example/service-issuer/issuer.go similarity index 70% rename from toolkit/_example/issuer/issuer.go rename to toolkit/_example/service-issuer/issuer.go index 2cc4890..cac7894 100644 --- a/toolkit/_example/issuer/issuer.go +++ b/toolkit/_example/service-issuer/issuer.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "log" @@ -20,6 +19,7 @@ import ( "github.com/ucan-wg/go-ucan/token/delegation" example "github.com/INFURA/go-ucan-toolkit/_example" + protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" "github.com/INFURA/go-ucan-toolkit/issuer" ) @@ -35,7 +35,7 @@ func main() { cancel() }() - err := run(ctx, example.IssuerUrl, example.ServicePrivKey) + err := run(ctx, example.ServiceIssuerUrl, example.ServicePrivKey) if err != nil { log.Println(err) os.Exit(1) @@ -50,7 +50,12 @@ func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) e // Here, we enforce that the caller uses its own DID endpoint (an arbitrary construct for this example). // You will notice that the server doesn't need to know about this logic to enforce it. policies, err := policy.Construct( - policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s", aud.String()))), + policy.Or( + // allow exact path + policy.Equal(".http.path", literal.String(fmt.Sprintf("/%s", aud.String()))), + // allow sub-path + policy.Like(".http.path", fmt.Sprintf("/%s/*", aud.String())), + ), ) if err != nil { return nil, err @@ -67,35 +72,7 @@ func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) e return err } - handler := issuer.HttpWrapper(rootIssuer, func(r *http.Request) (*issuer.ResolvedRequest, error) { - // Let's make up a simple json protocol - req := struct { - Audience string `json:"aud"` - Cmd string `json:"cmd"` - Subject string `json:"sub"` - }{} - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - return nil, err - } - aud, err := did.Parse(req.Audience) - if err != nil { - return nil, err - } - cmd, err := command.Parse(req.Cmd) - if err != nil { - return nil, err - } - sub, err := did.Parse(req.Subject) - if err != nil { - return nil, err - } - return &issuer.ResolvedRequest{ - Audience: aud, - Cmd: cmd, - Subject: sub, - }, nil - }) + handler := issuer.HttpWrapper(rootIssuer, protocol.RequestResolver) srv := &http.Server{ Addr: issuerUrl, diff --git a/toolkit/_example/server/server.go b/toolkit/_example/service/service.go similarity index 82% rename from toolkit/_example/server/server.go rename to toolkit/_example/service/service.go index 726042b..f09d532 100644 --- a/toolkit/_example/server/server.go +++ b/toolkit/_example/service/service.go @@ -27,14 +27,16 @@ func main() { cancel() }() - err := run(ctx, example.ServerUrl, example.ServiceDid) + err := run(ctx, example.ServiceUrl, example.ServiceDid) if err != nil { log.Println(err) os.Exit(1) } } -func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { +func run(ctx context.Context, serviceUrl string, serviceDID did.DID) error { + log.Printf("service DID is %s\n", serviceDID.String()) + // we'll make a simple handling pipeline: // - exectx.ExtractMW to extract and decode the UCAN context, verify the service DID // - exectx.HttpExtArgsVerify to verify the HTTP policies @@ -50,7 +52,8 @@ func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { switch ucanCtx.Command().String() { case "/foo/bar": - log.Printf("handled command %v for %v", ucanCtx.Command(), ucanCtx.Invocation().Issuer()) + log.Printf("handled command %v at %v for %v", ucanCtx.Command(), r.URL.Path, ucanCtx.Invocation().Issuer()) + log.Printf("proof is %v", ucanCtx.Invocation().Proof()) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) default: @@ -64,7 +67,7 @@ func run(ctx context.Context, serverUrl string, serviceDID did.DID) error { handler = exectx.ExtractMW(handler, serviceDID) srv := &http.Server{ - Addr: serverUrl, + Addr: serviceUrl, Handler: handler, } diff --git a/toolkit/_example/shared_values.go b/toolkit/_example/shared_values.go index d71f24d..befa737 100644 --- a/toolkit/_example/shared_values.go +++ b/toolkit/_example/shared_values.go @@ -9,16 +9,27 @@ import ( // Endpoints -var ServerUrl = ":8080" -var IssuerUrl = ":8081" +var ServiceUrl = ":8080" +var ServiceIssuerUrl = ":8081" + +var AliceIssuerUrl = ":8082" // Service var ServicePrivKey crypto.PrivKey var ServiceDid did.DID +// Alice + +var AlicePrivKey crypto.PrivKey +var AliceDid did.DID + func init() { - privRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=") - ServicePrivKey, _ = crypto.UnmarshalPrivateKey(privRaw) + servPrivRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=") + ServicePrivKey, _ = crypto.UnmarshalPrivateKey(servPrivRaw) ServiceDid, _ = did.FromPrivKey(ServicePrivKey) + + alicePrivRaw, _ := base64.StdEncoding.DecodeString("CAESQFESA31nDYUhXXwbCNSFvg7M+TOFgyxy0tVX6o+TkJAKqAwDvtGxZeGyUjibGd/op+xOLvzE6BrTIOw62K3yLp8=") + AlicePrivKey, _ = crypto.UnmarshalPrivateKey(alicePrivRaw) + AliceDid, _ = did.FromPrivKey(AlicePrivKey) } diff --git a/toolkit/issuer/dlg_issuer.go b/toolkit/client/clientissuer.go similarity index 61% rename from toolkit/issuer/dlg_issuer.go rename to toolkit/client/clientissuer.go index 00ac364..57afc75 100644 --- a/toolkit/issuer/dlg_issuer.go +++ b/toolkit/client/clientissuer.go @@ -1,18 +1,15 @@ -package issuer +package client import ( "context" "fmt" "iter" - "time" "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/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" - - "github.com/INFURA/go-ucan-toolkit/client" ) // DlgIssuingLogic is a function that decides what powers are given to a client. @@ -25,32 +22,19 @@ import ( // expected action. If you don't want to give that power, return an error instead. type DlgIssuingLogic func(iss did.DID, aud did.DID, cmd command.Command, subject did.DID) (*delegation.Token, error) -var _ client.DelegationRequester = &Issuer{} +var _ DelegationRequester = &WithIssuer{} -// Issuer is an implementation of a re-delegating issuer. -// Note: Your actual needs for an issuer can easily be different (caching...) than the choices made here. -// Feel free to replace this component with your own flavor. -type Issuer struct { - did did.DID - privKey crypto.PrivKey - - pool *client.Pool - requester client.DelegationRequester - logic DlgIssuingLogic +type WithIssuer struct { + *Client + logic DlgIssuingLogic } -func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, logic DlgIssuingLogic) (*Issuer, error) { - d, err := did.FromPrivKey(privKey) +func NewWithIssuer(privKey crypto.PrivKey, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) { + client, err := NewClient(privKey, requester) if err != nil { return nil, err } - return &Issuer{ - did: d, - privKey: privKey, - pool: client.NewPool(), - requester: client.RequesterWithRetry(requester, time.Second, 3), - logic: logic, - }, nil + return &WithIssuer{Client: client, logic: logic}, nil } // RequestDelegation retrieve chain of delegation for the given parameters. @@ -59,22 +43,18 @@ func NewIssuer(privKey crypto.PrivKey, requester client.DelegationRequester, log // - subject: the DID of the resource to operate on, also the subject (or audience if defined) of the invocation token // Note: you can read it as "(audience) does (cmd) on (subject)". // Note: the returned delegation(s) don't have to match exactly the parameters, as long as they allow them. -func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { - if subject != i.did { - return nil, fmt.Errorf("subject DID doesn't match the issuer DID") - } - +func (c *WithIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { var proof []cid.Cid // is there already a valid proof chain? - if proof = i.pool.FindProof(audience, cmd, subject); len(proof) > 0 { - return i.pool.GetBundles(proof), nil + if proof = c.pool.FindProof(audience, cmd, subject); len(proof) > 0 { + return c.pool.GetBundles(proof), nil } // do we have the power to delegate this? - if proof = i.pool.FindProof(i.did, cmd, subject); len(proof) == 0 { + if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { // we need to request a new proof - proofBundles, err := i.requester.RequestDelegation(ctx, i.did, cmd, subject) + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) if err != nil { return nil, err } @@ -85,12 +65,12 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co return nil, err } proof = append(proof, bundle.Cid) - i.pool.AddBundle(bundle) + c.pool.AddBundle(bundle) } } // run the custom logic to get what we actually issue - dlg, err := i.logic(i.did, audience, cmd, subject) + dlg, err := c.logic(c.did, audience, cmd, subject) if err != nil { return nil, err } @@ -99,7 +79,7 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co } // sign and cache the new token - dlgBytes, dlgCid, err := dlg.ToSealed(i.privKey) + dlgBytes, dlgCid, err := dlg.ToSealed(c.privKey) if err != nil { return nil, err } @@ -114,7 +94,7 @@ func (i *Issuer) RequestDelegation(ctx context.Context, audience did.DID, cmd co if !yield(bundle, nil) { return } - for b, err := range i.pool.GetBundles(proof) { + for b, err := range c.pool.GetBundles(proof) { if !yield(b, err) { return } From df6dfee210d6301d9e9c2d559de8824a1a97b414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 25 Feb 2025 13:16:53 +0100 Subject: [PATCH 31/38] example: finish readme, small tuning --- toolkit/_example/Readme.md | 37 ++++++++++++++++--- .../_example/_protocol-issuer/requester.go | 2 +- toolkit/_example/alice-client-issuer/alice.go | 2 +- toolkit/_example/bob-client/bob.go | 2 +- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/toolkit/_example/Readme.md b/toolkit/_example/Readme.md index ce99267..54bfa45 100644 --- a/toolkit/_example/Readme.md +++ b/toolkit/_example/Readme.md @@ -1,9 +1,36 @@ +## UCAN examples + +This directory contains an example of UCAN usage across multiple agents, and their respective implementations. + +Please note that UCAN in itself doesn't enforce any protocol, topology or transport, and as such what you have here is one possibility among many others. In particular: +- this example is really geared towards using UCAN for an HTTP API +- it uses a particular flavor of issuer protocol and token exchange. In particular, that issuer gives delegation tokens to anyone asking. + +Your situation may be different from this, and would call for a different setup. + +Remember that everything in `go-ucan-toolkit` is essentially helpers, pre-made building blocks. You can use them, change them or make your own. + +## Scenario 1 + +Starting simple, if we run `service`, `service-issuer` and `alice-client-server`, we have the following scenario: + ![scenario 1](scenario1.png) + +- `service` controls the access to the resource (it's the `Executor` in that diagram). You can think about it as a proxy with authentication. +- `service-issuer` gives a delegation tokens to clients. `service` and `service-issuer` share the same DID and keypair. +- `alice-client-server` ask for a token, and periodically makes request + +## Scenario 2 + +Building on the previous scenario, we are adding sub-delegation. + ![scenario 2](scenario2.png) -TODO +- `alice-client-server` still do the same thing, but also expose a similar token issuer with the same protocol (for simplicity in that example) +- `bob-client` request a delegation from Alice, and make **direct** request to the service -- differences with a real system - - issuer protocol + token exchange - - opinionated with HTTP -- toolkit is helpers, you can change or write your own thing \ No newline at end of file +Note a few things: +- Alice can finely tune what Bob can do +- Bob receives **two** delegations: the original one Alice got and a second one delegating some of that original power to him +- Bob can make direct calls to the service without having to be proxied somewhere +- The service doesn't have to know beforehand about Bob or what power is given to him diff --git a/toolkit/_example/_protocol-issuer/requester.go b/toolkit/_example/_protocol-issuer/requester.go index fa98c79..fa0db0e 100644 --- a/toolkit/_example/_protocol-issuer/requester.go +++ b/toolkit/_example/_protocol-issuer/requester.go @@ -45,7 +45,7 @@ func (r Requester) RequestDelegation(ctx context.Context, audience did.DID, cmd return nil, err } - req, err := http.NewRequest(http.MethodPost, "http://"+r.issuerURL, buf) + req, err := http.NewRequest(http.MethodPost, r.issuerURL, buf) if err != nil { return nil, err } diff --git a/toolkit/_example/alice-client-issuer/alice.go b/toolkit/_example/alice-client-issuer/alice.go index 85e2190..a6bee86 100644 --- a/toolkit/_example/alice-client-issuer/alice.go +++ b/toolkit/_example/alice-client-issuer/alice.go @@ -67,7 +67,7 @@ func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DI return delegation.New(iss, aud, cmd, policies, subject) } - cli, err := client.NewWithIssuer(priv, protocol.NewRequester(serviceIssuerUrl), issuingLogic) + cli, err := client.NewWithIssuer(priv, protocol.NewRequester("http://"+serviceIssuerUrl), issuingLogic) if err != nil { return err } diff --git a/toolkit/_example/bob-client/bob.go b/toolkit/_example/bob-client/bob.go index 9b7ae0f..99b786d 100644 --- a/toolkit/_example/bob-client/bob.go +++ b/toolkit/_example/bob-client/bob.go @@ -50,7 +50,7 @@ func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl strin log.Printf("Bob DID is %s", d.String()) - cli, err := client.NewClient(priv, protocol.NewRequester(aliceUrl)) + cli, err := client.NewClient(priv, protocol.NewRequester("http://"+aliceUrl)) if err != nil { return err } From 1c4a0a9c8160fc984db3eb2c6e9bbc580a861aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 26 Feb 2025 16:07:24 +0100 Subject: [PATCH 32/38] exectx: add test for the main middleware, adjust some HTTP status --- toolkit/server/exectx/middlewares.go | 4 +- toolkit/server/exectx/middlewares_test.go | 128 ++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 toolkit/server/exectx/middlewares_test.go diff --git a/toolkit/server/exectx/middlewares.go b/toolkit/server/exectx/middlewares.go index c2d96c4..c431905 100644 --- a/toolkit/server/exectx/middlewares.go +++ b/toolkit/server/exectx/middlewares.go @@ -18,7 +18,7 @@ func ExtractMW(next http.Handler, serviceDID did.DID) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctn, err := bearer.ExtractBearerContainer(r.Header) if errors.Is(err, bearer.ErrNoUcan) { - http.Error(w, "no UCAN auth", http.StatusBadRequest) + http.Error(w, "no UCAN auth", http.StatusUnauthorized) return } if errors.Is(err, bearer.ErrContainerMalformed) { @@ -34,7 +34,7 @@ func ExtractMW(next http.Handler, serviceDID did.DID) http.Handler { // prepare a UcanCtx from the container, for further evaluation in the server pipeline ucanCtx, err := FromContainer(ctn) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusUnauthorized) return } diff --git a/toolkit/server/exectx/middlewares_test.go b/toolkit/server/exectx/middlewares_test.go new file mode 100644 index 0000000..00dd652 --- /dev/null +++ b/toolkit/server/exectx/middlewares_test.go @@ -0,0 +1,128 @@ +package exectx + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + + "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/container" + "github.com/ucan-wg/go-ucan/token/delegation" + "github.com/ucan-wg/go-ucan/token/invocation" +) + +func TestExtractMW(t *testing.T) { + const service = didtest.PersonaAlice + const client = didtest.PersonaBob + const cmd = "/foo/bar" + + for _, tc := range []struct { + name string + addHeaderFn func(func(key string, value string)) + expectedStatusCode int + successful bool + }{ + { + name: "no auth", + addHeaderFn: func(f func(key string, value string)) {}, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "wrong kind of auth", + addHeaderFn: func(f func(key string, value string)) { + f("Authorization", "Basic foobar") + }, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "invalid container", + addHeaderFn: func(f func(key string, value string)) { + f("Authorization", "Bearer foobar") + }, + expectedStatusCode: http.StatusBadRequest, + successful: false, + }, + { + name: "valid containter, for incorrect service", + addHeaderFn: func(f func(key string, value string)) { + cont := container.NewWriter() + + const service2 = didtest.PersonaCarol + + dlg, _ := delegation.Root(service2.DID(), client.DID(), cmd, nil) + dlgByte, dlgCid, _ := dlg.ToSealed(service2.PrivKey()) + cont.AddSealed(dlgByte) + + inv, _ := invocation.New(client.DID(), cmd, service2.DID(), []cid.Cid{dlgCid}) + invBytes, _, _ := inv.ToSealed(client.PrivKey()) + cont.AddSealed(invBytes) + + contB64, _ := cont.ToBase64StdPadding() + + f("Authorization", "Bearer "+contB64) + }, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "valid containter, missing invocation", + addHeaderFn: func(f func(key string, value string)) { + cont := container.NewWriter() + + dlg, _ := delegation.Root(service.DID(), client.DID(), cmd, nil) + dlgByte, _, _ := dlg.ToSealed(service.PrivKey()) + cont.AddSealed(dlgByte) + + contB64, _ := cont.ToBase64StdPadding() + + f("Authorization", "Bearer "+contB64) + }, + expectedStatusCode: http.StatusUnauthorized, + successful: false, + }, + { + name: "valid containter, valid tokens", + addHeaderFn: func(f func(key string, value string)) { + cont := container.NewWriter() + + dlg, _ := delegation.Root(service.DID(), client.DID(), cmd, nil) + dlgByte, dlgCid, _ := dlg.ToSealed(service.PrivKey()) + cont.AddSealed(dlgByte) + + inv, _ := invocation.New(client.DID(), cmd, service.DID(), []cid.Cid{dlgCid}) + invBytes, _, _ := inv.ToSealed(client.PrivKey()) + cont.AddSealed(invBytes) + + contB64, _ := cont.ToBase64StdPadding() + + f("Authorization", "Bearer "+contB64) + }, + expectedStatusCode: http.StatusOK, + successful: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, has := FromContext(r.Context()) + require.Equal(t, tc.successful, has) + + _, _ = io.WriteString(w, "OK") + }) + handler = ExtractMW(handler, service.DID()) + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + tc.addHeaderFn(req.Header.Set) + + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + require.Equal(t, tc.expectedStatusCode, w.Code) + + }) + } +} From 2d031fdbdb374426bb4e1c493073f88d88637938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 26 Feb 2025 19:07:10 +0100 Subject: [PATCH 33/38] exectx: easy access to the root delegation --- toolkit/server/exectx/ucanctx.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index cc9ae9e..5bb1f9e 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -92,6 +92,13 @@ func (ctn *UcanCtx) GetDelegation(cid cid.Cid) (*delegation.Token, error) { return nil, delegation.ErrDelegationNotFound } +// GetRootDelegation returns the delegation.Token at the root of the proof chain. +func (ctn *UcanCtx) GetRootDelegation() *delegation.Token { + proofs := ctn.inv.Proof() + c := proofs[len(proofs)-1] + return ctn.dlgs[c] +} + // Policies return the full set of policy statements to satisfy. func (ctn *UcanCtx) Policies() policy.Policy { return ctn.policies From 29ccdb700e2b83e1395974d360ce697935cbd275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 27 Feb 2025 15:18:30 +0100 Subject: [PATCH 34/38] exectx: more tests --- toolkit/server/exectx/ucanctx_test.go | 62 ++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index 5011ae2..7d72309 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -30,7 +30,7 @@ const ( network = "eth-mainnet" ) -func TestCtx(t *testing.T) { +func TestUcanCtxFullFlow(t *testing.T) { // let's use some pre-made DID+privkey. // use go-ucan/did to generate or parse them. service := didtest.PersonaAlice @@ -205,3 +205,63 @@ func TestGoCtx(t *testing.T) { require.True(t, ok) require.Equal(t, expected, got) } + +func TestUcanCtx(t *testing.T) { + const service = didtest.PersonaAlice + const client1 = didtest.PersonaBob + const client2 = didtest.PersonaCarol + const cmd = "/foo/bar" + + cont := container.NewWriter() + + pol1 := policy.MustConstruct( + policy.Equal(".http.scheme", literal.String("https")), + ) + dlg1, err := delegation.Root(service.DID(), client1.DID(), cmd, pol1, + delegation.WithMeta("foo", "bar"), + ) + require.NoError(t, err) + dlg1Byte, dlg1Cid, err := dlg1.ToSealed(service.PrivKey()) + require.NoError(t, err) + cont.AddSealed(dlg1Byte) + + pol2 := policy.MustConstruct( + policy.Equal(".http.method", literal.String("GET")), + ) + dlg2, err := delegation.New(client1.DID(), client2.DID(), cmd, pol2, service.DID(), + delegation.WithMeta("foo", "foo"), // attempt to replace + ) + require.NoError(t, err) + dlg2Byte, dlg2Cid, err := dlg2.ToSealed(client1.PrivKey()) + require.NoError(t, err) + cont.AddSealed(dlg2Byte) + + inv, err := invocation.New(client2.DID(), cmd, service.DID(), []cid.Cid{dlg2Cid, dlg1Cid}) + require.NoError(t, err) + invBytes, _, err := inv.ToSealed(client2.PrivKey()) + require.NoError(t, err) + cont.AddSealed(invBytes) + + ctx, err := exectx.FromContainer(cont.ToReader()) + require.NoError(t, err) + + require.NotNil(t, ctx.Invocation()) + require.Equal(t, cmd, ctx.Command().String()) + require.Equal(t, 1, ctx.Meta().Len()) + require.Equal(t, "bar", must(ctx.Meta().GetString("foo"))) + require.Equal(t, service.DID(), ctx.GetRootDelegation().Issuer()) + require.Equal(t, append(pol1, pol2...), ctx.Policies()) + + require.ErrorContains(t, ctx.ExecutionAllowed(), `the following UCAN policy is not satisfied: ["==", ".http.method", "GET"]`) + + r := httptest.NewRequest(http.MethodGet, "https://foo/bar", nil) + require.NoError(t, ctx.VerifyHttp(r)) + require.NoError(t, ctx.ExecutionAllowed()) +} + +func must[T any](e T, err error) T { + if err != nil { + panic(err) + } + return e +} From 0fd71612d3a244fce96aa8bc6486b4b891a82a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 27 Feb 2025 15:23:58 +0100 Subject: [PATCH 35/38] exectx: DX improvement: catch proof chain issue early --- toolkit/server/exectx/ucanctx.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index 5bb1f9e..04250d3 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -70,6 +70,21 @@ func FromContainer(cont container.Reader) (*UcanCtx, error) { ctx.meta.Include(dlg.Meta()) } + // DX: As the invocation is created without the delegation, no check is done that the proof chain (CIDs only) + // is ordered properly and not broken. We don't check that in the container either as it doesn't make any assumption + // on what is being carried around. That UcanCtx is the first place where we enforce having a single invocation and + // only the matching delegation. + // For sanity, we verify that the proofs are ordered properly. This will be checked later anyway, but it's cheap to + // verify here and catch an easy mistake. + chainTo := inv.Issuer() + for _, c := range inv.Proof() { + dlg := ctx.dlgs[c] + if dlg.Audience() != chainTo { + return nil, fmt.Errorf("proof chain is broken or not ordered correctly") + } + chainTo = dlg.Issuer() + } + return ctx, nil } From 4aedc4de39cd74ad09b16071453131d648c67ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 18 Mar 2025 18:03:00 +0100 Subject: [PATCH 36/38] client: add a convenient PrepareDelegation function --- toolkit/client/client.go | 78 ++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/toolkit/client/client.go b/toolkit/client/client.go index f9cca09..346c9fe 100644 --- a/toolkit/client/client.go +++ b/toolkit/client/client.go @@ -9,6 +9,8 @@ import ( "github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" + "github.com/ucan-wg/go-ucan/pkg/policy" + "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/invocation" ) @@ -33,26 +35,11 @@ func NewClient(privKey crypto.PrivKey, requester DelegationRequester) (*Client, }, nil } -// PrepareInvoke returns an invocation and the proof delegation, bundled in a container.Writer. +// PrepareInvoke returns an invocation, bundled in a container.Writer with the necessary proofs. func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject did.DID, opts ...invocation.Option) (container.Writer, error) { - var proof []cid.Cid - - // do we already have a valid proof? - if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { - // we need to request a new proof - proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) - if err != nil { - return nil, fmt.Errorf("requesting delegation: %w", err) - } - - // cache the new proofs - for bundle, err := range proofBundles { - if err != nil { - return nil, err - } - proof = append(proof, bundle.Cid) - c.pool.AddBundle(bundle) - } + proof, err := c.findProof(ctx, cmd, subject) + if err != nil { + return nil, err } inv, err := invocation.New(c.did, cmd, subject, proof, opts...) @@ -76,3 +63,56 @@ func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject return cont, nil } + +// PrepareDelegation returns a new delegation for a third party DID, bundled in a container.Writer with the necessary proofs. +func (c *Client) PrepareDelegation(ctx context.Context, aud did.DID, cmd command.Command, subject did.DID, policies policy.Policy, opts ...delegation.Option) (container.Writer, error) { + proof, err := c.findProof(ctx, cmd, subject) + if err != nil { + return nil, err + } + + dlg, err := delegation.New(c.did, aud, cmd, policies, subject, opts...) + if err != nil { + return nil, err + } + + dlgSealed, _, err := dlg.ToSealed(c.privKey) + if err != nil { + return nil, err + } + + cont := container.NewWriter() + cont.AddSealed(dlgSealed) + for bundle, err := range c.pool.GetBundles(proof) { + if err != nil { + return nil, err + } + cont.AddSealed(bundle.Sealed) + } + + return cont, nil +} + +func (c *Client) findProof(ctx context.Context, cmd command.Command, subject did.DID) ([]cid.Cid, error) { + var proof []cid.Cid + + // do we already have a valid proof? + if proof = c.pool.FindProof(c.did, cmd, subject); len(proof) == 0 { + // we need to request a new proof + proofBundles, err := c.requester.RequestDelegation(ctx, c.did, cmd, subject) + if err != nil { + return nil, fmt.Errorf("requesting delegation: %w", err) + } + + // cache the new proofs + for bundle, err := range proofBundles { + if err != nil { + return nil, err + } + proof = append(proof, bundle.Cid) + c.pool.AddBundle(bundle) + } + } + + return proof, nil +} From 06f478b9c3b05c903be91a0b53ea2a7e47487157 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Tue, 18 Mar 2025 14:18:53 -0400 Subject: [PATCH 37/38] feat(client): add delegation bundle(s) to client.pool --- toolkit/client/client.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/toolkit/client/client.go b/toolkit/client/client.go index 346c9fe..470c12b 100644 --- a/toolkit/client/client.go +++ b/toolkit/client/client.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + "iter" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/crypto" @@ -35,6 +36,16 @@ func NewClient(privKey crypto.PrivKey, requester DelegationRequester) (*Client, }, nil } +// AddBundle adds a delegation.Bundle to the client's pool. +func (c *Client) AddBundle(bundle *delegation.Bundle) { + c.pool.AddBundle(bundle) +} + +// AddBundles adds a sequence of delegation.Bundles to the client's pool. +func (c *Client) AddBundles(bundles iter.Seq[*delegation.Bundle]) { + c.pool.AddBundles(bundles) +} + // PrepareInvoke returns an invocation, bundled in a container.Writer with the necessary proofs. func (c *Client) PrepareInvoke(ctx context.Context, cmd command.Command, subject did.DID, opts ...invocation.Option) (container.Writer, error) { proof, err := c.findProof(ctx, cmd, subject) From 0647e4ff8a2ff249d3f7c0b3a65a69b97b4d1664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Tue, 5 Aug 2025 12:11:20 +0200 Subject: [PATCH 38/38] adjust the toolkit to the new location --- go.mod | 1 + go.sum | 2 + pkg/container/containertest/Base64StdPadding | 2 +- .../containertest/Base64StdPaddingGzipped | 2 +- pkg/container/containertest/Base64URL | 2 +- pkg/container/containertest/Base64URLGzipped | 2 +- pkg/container/containertest/Bytes | Bin 4180 -> 4220 bytes pkg/container/containertest/BytesGzipped | Bin 2336 -> 2345 bytes pkg/container/reader.go | 23 ++- pkg/container/writer.go | 4 +- token/delegation/delegation_test.go | 2 +- .../delegationtest/generator/generator.go | 2 +- .../delegationtest/generator/main.go | 2 +- token/delegation/examples_test.go | 2 +- token/delegation/schema_test.go | 2 +- token/internal/didtest/crypto.go | 139 ------------- token/invocation/invocation_test.go | 2 +- toolkit/_example/Readme.md | 2 +- .../_protocol-issuer/request-resolver.go | 6 +- .../_example/_protocol-issuer/requester.go | 9 +- toolkit/_example/alice-client-issuer/alice.go | 19 +- toolkit/_example/bob-client/bob.go | 22 +- toolkit/_example/service-issuer/issuer.go | 13 +- toolkit/_example/service/service.go | 6 +- toolkit/_example/shared_values.go | 22 +- toolkit/client/client.go | 13 +- toolkit/client/client_test.go | 7 +- toolkit/client/clientissuer.go | 9 +- toolkit/client/pool.go | 3 +- toolkit/client/proof.go | 9 +- toolkit/client/proof_test.go | 3 +- toolkit/client/requester.go | 3 +- toolkit/client/requester_infura.go | 99 --------- toolkit/issuer/http_wrapper.go | 23 ++- toolkit/issuer/root_issuer.go | 20 +- toolkit/server/bearer/bearer_test.go | 2 + toolkit/server/exectx/middlewares.go | 6 +- toolkit/server/exectx/middlewares_test.go | 3 +- toolkit/server/exectx/ucanctx.go | 60 ++---- toolkit/server/exectx/ucanctx_test.go | 55 +---- toolkit/server/extargs/Readme.md | 21 +- .../server/extargs/{infura.go => custom.go} | 36 ++-- .../{infura_test.go => custom_test.go} | 27 +-- toolkit/server/extargs/http.go | 5 +- toolkit/server/extargs/http_test.go | 10 +- toolkit/server/extargs/jsonrpc.go | 171 ---------------- toolkit/server/extargs/jsonrpc_test.go | 188 ------------------ 47 files changed, 233 insertions(+), 828 deletions(-) delete mode 100644 token/internal/didtest/crypto.go delete mode 100644 toolkit/client/requester_infura.go rename toolkit/server/extargs/{infura.go => custom.go} (57%) rename toolkit/server/extargs/{infura_test.go => custom_test.go} (76%) delete mode 100644 toolkit/server/extargs/jsonrpc.go delete mode 100644 toolkit/server/extargs/jsonrpc_test.go diff --git a/go.mod b/go.mod index c8c2ab1..619f8e5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.24.5 require ( github.com/MetaMask/go-did-it v1.0.0-pre1 + github.com/avast/retry-go/v4 v4.6.1 github.com/ipfs/go-cid v0.5.0 github.com/ipld/go-ipld-prime v0.21.0 github.com/multiformats/go-multibase v0.2.0 diff --git a/go.sum b/go.sum index 5f9198d..71d4ac2 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MetaMask/go-did-it v1.0.0-pre1 h1:NTGAC7z52TwFegEF7c+csUr/6Al1nAo6ValAAxOsjto= github.com/MetaMask/go-did-it v1.0.0-pre1/go.mod h1:7m9syDnXFTg5GmUEcydpO4Rs3eYT4McFH7vCw5fp3A4= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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/container/containertest/Base64StdPadding b/pkg/container/containertest/Base64StdPadding index 58936c0..e912350 100644 --- a/pkg/container/containertest/Base64StdPadding +++ b/pkg/container/containertest/Base64StdPadding @@ -1 +1 @@ -BoWZjdG4tdjGKWQGeglhAa8vQAimsd6982rgPP0e1ccInV/4cjWzjR71iyXGc2qVOGyEEZMT+e/Zfaw1WzVS5Ybgtpjw5/845jAPNbJv1DqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdnF0eDhkWFZ3NzZkTnc2U0FZUXJ3cFEzQlI4S3NwZ2FUN0Rwd3F3OW1zbXhjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcDZmMng2TW1xY1BDVWJhRVBxM0VqYjc4cjI3TFVKTEJWYUxjZ2hZaFR6TXJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcDZmMng2TW1xY1BDVWJhRVBxM0VqYjc4cjI3TFVKTEJWYUxjZ2hZaFR6TXJkbWV0YaNoNTIxZDIzZDBqMzMwNTJkZjgzOGg3NDVlZWFlZGpkYzg2NDM2M2QxaDgzZjY5OGYzajNhMDA3ZWY4Mjllbm9uY2VMwB+HSucZ5j+7FG/wWQGeglhAFgW0vCDmL/dzrIIT5oVRXsA1b/Fk5AiKhFlAVXRSXFoKuCixvn17vePm8anPkPewtZS7QiekJYwt9a0Aoe/CBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcGNrS01FM3c3UWlvdWV3WkpLUFI0cDF5ak4yUDRjOXF3aDc1NVFqeTNyMURjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ramlzQmZ3RE5SSHlYZmljd1k1M2MxaVpHdnBqaWVlclBuc1JxSEN2emhvOXpjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ramlzQmZ3RE5SSHlYZmljd1k1M2MxaVpHdnBqaWVlclBuc1JxSEN2emhvOXpkbWV0YaNoMTllZGI5NWFqN2ZkNThkYjc5M2gyY2FhY2NiYmo0NWJkNmQ1ODY2aDg4ZmY0N2E3amU2YjgzNjZlNGRlbm9uY2VMCrOVvOHN5HLH2khEWQGeglhAf4mTe11QRPxtukpFKL6IaKRxrZPSytRgfOHYXGHLrImx0j0qbdPkQLaQeajQAG2vYnrjqG74v+lRhHIFTLsRCKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcWREc2FqdDhUQXBkeGtNbmRHQXoxa1FNY0w4bWtxYXNWR1ZoZm5MRnRIeFRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1raDlicHdRWjNycFZ0dlZXOFhtRUJEc01ucUJBZG00Q2MzczFWeTE3VUhDR1NjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raDlicHdRWjNycFZ0dlZXOFhtRUJEc01ucUJBZG00Q2MzczFWeTE3VUhDR1NkbWV0YaNoMDhjYTQ0MmNqMTBiNmU5Y2JjY2hjNTZmMjUwM2o1ZTJlNmU0ZDNiaGU3ODNhNTcxajY2OWVmZTI2Y2Nlbm9uY2VMm+hnFEzvWMyIwPWpWQGeglhAg2jLUqHQRlsIoBt7jaif9n1AFiuODDXx0lxtIhZLK52E9AjdPl+MRYaE4HrYXLUZdsACOovH6YrS8onV0E2ZA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rb1dIQXN2d2ZRWnZDamdCYWhlSjRMWVlXYWJ2MTVjcmpSVGJEWWpGM2ZHbWVjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcXFhcVdMZEVRWTVBUkV1c2U0UUduWUtDVTY5OHFleU5Lc0ZTZzRKdkVzWUxjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcXFhcVdMZEVRWTVBUkV1c2U0UUduWUtDVTY5OHFleU5Lc0ZTZzRKdkVzWUxkbWV0YaNoMmE5NmNjNmVqNGNlM2ZjMTdjZGg3ZmMzZTdkM2piMjkwZTMyNDE3aGUwMDFmYzdmajJjOTAwNWY0NmRlbm9uY2VMC3IjW2PprOmuLut7WQGeglhAviUPHHfU+n8H1vyKyatGMS2YvJUvSvBxjC9hQSw7wIVbYAUJIgRr54V6vAifE2O4CvldXb16Qkqh1w9LnhX+A6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra0o3TTNzUXdOR0hFdEZOZGd4UFV5V1lXdGRWdXhLVXFiWndGdTU0TkVkOThjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdkZVcGhKcGpaVFZHYzRqdjVjWnJWampuV2JQWUM1UHhvVWduenhuVVZzMUJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdkZVcGhKcGpaVFZHYzRqdjVjWnJWampuV2JQWUM1UHhvVWduenhuVVZzMUJkbWV0YaNoMGYzOTk5M2VqNWMwOGI2NzEwNWgzNmFmNTMzNmpiNGU0MWRlZTFmaGMyOWVjZWM1ajdlMGZjMzE4OWZlbm9uY2VMwsoostxej7C8KxTfWQGeglhAABlu2b+acxlXwvhl3Cvr1t6wTRgDZyhwui2K//dXhixO+IBslbAM9a4W7wssVcpaA1M/U91DwRH6JjG8lfwnA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rbW9iNGlDczdXRHdRNU1KNDNOZERzaEplbXF4ZjF4RHBrWnJ0NFFYdUNTaURjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcG1BY0dLdWtLQ281czlzQzhpUGVaajkyMWc3YzRWeVM3WEFNcjR2ZGZSN1BjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcG1BY0dLdWtLQ281czlzQzhpUGVaajkyMWc3YzRWeVM3WEFNcjR2ZGZSN1BkbWV0YaNoMjk3MTk1OTRqM2ZkYjI4MDA3Ymg4M2I4ZTQ2OGpiMDlhMDY3OWQxaGEwNjQzYmI4amE1ZWQ4NjcwYmRlbm9uY2VMBevInlpWjVqplZC8WQGeglhAVuW7Dme4SHo/P7+mB67OfD6DK6NBnZy1cyBAsbk76HL42MjYzPp8fi8+JkoZDs4g24iXi9vixQhfpPG8GfrUAKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcmJUbWRXaDZ2TUg2eDZFNlI3UnNpUEpIdFloM3hlSzdtTnFpSzJoekVxZ0VjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ralZtNUdqblhLU2I4d0s3VDF2dmU4eHRuWlJvaUVYUjJGSGdQYVF5M2lEeWhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ralZtNUdqblhLU2I4d0s3VDF2dmU4eHRuWlJvaUVYUjJGSGdQYVF5M2lEeWhkbWV0YaNoODU4MzY2ODhqNGZlNzI1Mzg2ZWg4NjNiMTIxOWo2MmY2Nzg1YWFmaGE5Y2M0MTEyajExMjgwZTM5NTBlbm9uY2VMUPFgyqtzEkRPXukGWQGeglhAIHB/6ndHOw4ruRy33I9yJej9A/ec3mGJsIEmNr33/rErTXlcNx+YH269sRCO9xOoRXFugTFDkAr92gjoUAeEAKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rb3U4ZkRxc0xjcXVNaWRlRzZGdlpteFJOdG9EOVU0S2dIWjgycVBxOHJLR25jY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rc0RrenVVMncyeFZocVFGTjJVdHhyWkE1QWlIdWpHeGdEWHBueXdoNGV0TGRjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rc0RrenVVMncyeFZocVFGTjJVdHhyWkE1QWlIdWpHeGdEWHBueXdoNGV0TGRkbWV0YaNoNzIwMzUzNGRqYTMwMDY2Nzk4Y2g5ZDM1ZmUwOWoxZTg0Mzc3MTU0aGE4ZWY4YjM3ajIzMDM5YTljMTdlbm9uY2VMVLWsemG3WIh8/QshWQGeglhAR0zlf5Hc4m8hEwf664w9Cim5PHJftx0Y/rno06f9lTth3JuvY8znsXvl7Ym+ShIjTk9H9uJcjpVaNABfkr2YCKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcUU3SkptM3pkdXoxV2hFenpRcFZQa01SODdRaG5jRlpDNmpnMlZOV2JvVHNjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbTN1ZWF5cXo5akJXa2NRcWI2RDJpbkc1cVk4OTNOVWtnTWdXWmtudU1vcVJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbTN1ZWF5cXo5akJXa2NRcWI2RDJpbkc1cVk4OTNOVWtnTWdXWmtudU1vcVJkbWV0YaNoNmUyNzYwOThqZDQwOGRhYmJjZmhiMmE5ZGVhZGo2MWFjZmNlOWMwaGVhNWRjY2QwamVjYWUyMDVkMmNlbm9uY2VMcrvruHcj5dHWBhjMWQGeglhARe/SIBfmxJzT9pT650Vr3xmTvOhB2vOO3SsxEk5emAbTD0msHSWiwROgpbcsVOczBAMpR3MDlv+KDWzU5vKyBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rZkhTbW12TEppM1R5QmF3emczbmlFWUxOQzZnREd1NW9xcnhueFo5U3FkYk5jY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rblhhaXl1cUdXdTFrWTh2Rm1tZ2lMRjR1YnB4bW8zZlg3RlNpdlhBcmFac1djcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rblhhaXl1cUdXdTFrWTh2Rm1tZ2lMRjR1YnB4bW8zZlg3RlNpdlhBcmFac1dkbWV0YaNoODg2NGQ5MmNqNTZhNjZhYjY5NWg4OGI3NjAwOGpmMjQwM2FkYTExaGZiY2Q1NDQ5amE4ZGVhNzhiMmZlbm9uY2VM2bLoKvbEKP43Vixd \ No newline at end of file +BoWZjdG4tdjGKWQGiglhABYakVJ3qMsyohPE5jEbaJ/o7OgvQT9dI/drXscOq+F6V8twJV6xRIXVYzWsoga7BYsf2XwTf+b+PEEQGRkOSAaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3JYd0hzYmtXSjg0ZkVpOUt3QjJSQzRwUFN0WG1QbkJXd2RQR3V0Tk56eXhBY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3ZWTlNMWlI1ZlFRMlpkeDhGZGRtcXoxTEx0dzFtMzVKc1VhVjdiSmszblpwY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3ZWTlNMWlI1ZlFRMlpkeDhGZGRtcXoxTEx0dzFtMzVKc1VhVjdiSmszblpwZG1ldGGjaDEzYjcyOWRiamJiYjZmZGNkOTJoMWY5M2U1YTBqZTA4MTk0MDdlOGg3M2Y4MWIzMWowNzQ1ZTQ0MzI3ZW5vbmNlTBGodc59dCiOB1O9lVkBooJYQPMo/EMhJhThi8Ab9zxcWlsBdJJf/WQnp7biGcrSOr/F5YrvumbUj31pd7s8mggD6BLSmi/Ch6wqQztRGD3Atw2iYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtmcmVKb2o4cGtqamE1OHJUTTVEc3k0OGd3MWhaU0hmY1pXSHREcTI4WVp0QmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtwZ1k1b3pUUWVMRnIxUENtV0UyU1lDWjZmNUZhckZERUF4Y1JmSEZQcFpycmNwb2yBg2NhbGxjLltdg2E+Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtwZ1k1b3pUUWVMRnIxUENtV0UyU1lDWjZmNUZhckZERUF4Y1JmSEZQcFpycmRtZXRho2g5NTkyYTVkNWo1OGM5ZTYyN2E3aGNlYjhhMDVmamRlZjI1ZGNiN2JoZGZjOWE4OGVqNmVhZjQ1YTllNGVub25jZUzWFKuHIJ8sGrSYiD5ZAaKCWEDh97Nhb9gfjBT5wMlUe/VKpKXtO5C/P9/DOngvpVrc2fjxLmU+izfdFIMfsZc4fqFWXRGa7mM/vqFKsK7tAxoPomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rd1JmN2VDczRBWkc1RGhuNXJmcEZvNzhRTEhpbnNCc01TeFZ1eHpOeUM5aW5jY21kaC9mb28vYmFyY2V4cBpoki+3Y2lzc3g4ZGlkOmtleTp6Nk1raVo1c1lrMjF3VFVkZjc0NGNZRDFMSlgyS1Z1UlNxc29mN05TcEJISjVSVm1jcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raVo1c1lrMjF3VFVkZjc0NGNZRDFMSlgyS1Z1UlNxc29mN05TcEJISjVSVm1kbWV0YaNoMDU2ZWE4NzlqMzY0MmIyZDE3MWgzNDI4ZmExY2pjNTE1ODlhNTFiaDc4ODQ2MDI2ajhiNGVlMzhmZTRlbm9uY2VMlJH6zxWeWLgrPFL8WQGiglhAodgbslQnxQ6LbUhJ2ZVE6YXZmJzsXpptz45Px8HX67seRXLboSiTpJGX7CWn2l/C3Bkvpc9wWr4os4UmAP8WCaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3Jpbng5WHNwVEdaMjlhSHBQdWkxUXhwUTFNRko3b2lnSERVbWdINWN4MUtZY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa2pRa3hFbUNVSHdVTEV0R1VZOTFLMUdlY2NScHJVdkI0QndXVUQ4Z0hGcVpUY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa2pRa3hFbUNVSHdVTEV0R1VZOTFLMUdlY2NScHJVdkI0QndXVUQ4Z0hGcVpUZG1ldGGjaDcxOTBjZmE5ajM4NmU0OGFiZmJoN2U1OTdjZTlqZDFiYmI2ZjYxNGg5ZTljYTc4OWpjZWM5Zjk5NTgxZW5vbmNlTL6XetkzghuY2J6Y51kBooJYQHoWRs/WJJro50YyRnRzlhpvn1JL6uD2z6bHo5F4Ke9ui0WIr6kSxQKcwbT1/6EkrFAL4n/FlxXG73ba31zgwwSiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWt3ZW9hbVp1VFBrVVFRUlhhaXIyb0hKem1QYzVFYnh6UkExeXZ2RjU2SFJ3YmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWttYWRLaFJqR2FOV2MzQzczbzUxb0E0QzY5U01jaHBBUFJYU1A2V2NYelFwWGNwb2yBg2NhbGxjLltdg2E+Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWttYWRLaFJqR2FOV2MzQzczbzUxb0E0QzY5U01jaHBBUFJYU1A2V2NYelFwWGRtZXRho2gxYzgxYmJjMGo4OWEyNjJhYjNlaDU4ZTVjZjc5ajk0NmZkZDcwOTNoNjUyYTFiOWRqNDQwZDg0YzNiY2Vub25jZUxV6CeWkqLbqyInfX1ZAaKCWECPCG7QlXAncItxluVoom9ExdZB36bJgVcC8atGlNkZztgwgi2qRn/PAPJ+izizRxPvXo2HeVl8oFMx+eVU9I0IomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ramFDdzNqNTRFcGN0SGFVdjNqeGVTQkRhd2RNTW5EYXNLcERLMVozQmtNYmRjY21kaC9mb28vYmFyY2V4cBpoki+3Y2lzc3g4ZGlkOmtleTp6Nk1rbUVBZVZYdVNncWhybzNZMUNETEtjN0pDbUhMNmM1eHFlM0ZIVkdhZU52UjljcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbUVBZVZYdVNncWhybzNZMUNETEtjN0pDbUhMNmM1eHFlM0ZIVkdhZU52UjlkbWV0YaNoN2U3NjA3NWNqZWYyNDEzMGMyM2g4ZGI3YTA0YWpmMDUwMDRiOGZiaGQwMTJkNTNiamMwYTk0ZTU2OGZlbm9uY2VM0A2enQKwnAQODN09WQGiglhAH7cY+u6guPL+h4Gl1MD99HMYxzauamvTl9BDhm+gf1qI5wyyJZz9s1jDn9DIV0kc+X4zOukFEc2mxgi3B3DmDqJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3FhNmNoU2ZMM2NHYWNFamVxalNnY3pLZmlYNmRWM0xRbWMzQ1JWVnhDVmg4Y2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3VKQWQ4b3BQUkxWNzJXZXp0UFFIcnhkZENwcTM3UDFQeDFRNXlhVEdhdjl2Y3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3VKQWQ4b3BQUkxWNzJXZXp0UFFIcnhkZENwcTM3UDFQeDFRNXlhVEdhdjl2ZG1ldGGjaDA2ODc4MmY3amRhMGJjNGUzN2RoNGEyY2U0YzFqNTJkNGQzMmVmN2hhZGU5MGE1OGphOTk1ODdhZTliZW5vbmNlTB8pysCUZIh8y43VnFkBooJYQLaRhdQ03p5fKqnz8nRjb9h+uFvrVK4Ke7JlGuhnEL8LUQzjbDckjicDfkZX//duNEV3qKyTHiSGvU1FQo9nyAiiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtxcWlienI5amYxTjkxQVY1eUVLYzRjSzRaMWlVMkpXRzdndWJLdWhoZTR6a2NjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtqeFIyVGFBUWNqRERXTjl5dloyY3BFYUVrbncydmkxNENLTmhBSzQ0TGZ1Y2Nwb2yBg2NhbGxjLltdg2E+Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtqeFIyVGFBUWNqRERXTjl5dloyY3BFYUVrbncydmkxNENLTmhBSzQ0TGZ1Y2RtZXRho2gyMDkyZjYwYmpiMzcwZjMwNzI3aDQ3OGI0Y2M1ajk4YjdkYjAxMzJoNmQ0YmFhZjVqYmVkNGUxYjJmM2Vub25jZUx+tmRsecAtdwXDqY1ZAaKCWEB3o5gA741QVQnxqMkgt7DwS/4WgoReEKdUCz4RpaM5LDeJNg4vi8rOUTaU4VhpowgDxn+lBGraJUbp2Y5L/q8PomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdHhRUjg5b0F4VG1kdjNTOFFhbVk2aXdGYlp6SDZ1QnlFQWZLOGNpY0RkU3JjY21kaC9mb28vYmFyY2V4cBpoki+3Y2lzc3g4ZGlkOmtleTp6Nk1raTgyc2dFa1ZrYnNjZEJ5ZDY4WVoyQ2ViSmh4QXlzSnpDZHluOTFhRHROdjhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raTgyc2dFa1ZrYnNjZEJ5ZDY4WVoyQ2ViSmh4QXlzSnpDZHluOTFhRHROdjhkbWV0YaNoMmI1MTllNzNqM2E3MTc5NmQ3MWg5YWE3NjQwMGoxM2FiZGYwNWQ2aDlmZmU1ZTliajY5YmU3NjI5ZDFlbm9uY2VMyL0L8CRxcMsMq5hbWQGiglhA3YnA0f5qejI6yGKfabYt1rojIQkTKSdLfMLSwhl9LR+aHXivxgKWzy/W3wbHOjG5JdEb7sl/yH0G4SFcpTSOCaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3VqVTVCTFhQZkt6OVlVUmNZY1IzNVN1eFV1UmNEajZOZmtpV1c0dEhReUJQY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3MydVJxUEV5akdBM3Z4eEVEZmNHSzdpZ2tWN1JwSGltNTVqamlKazYzcTJzY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3MydVJxUEV5akdBM3Z4eEVEZmNHSzdpZ2tWN1JwSGltNTVqamlKazYzcTJzZG1ldGGjaGNlOTk3ZWVjamFkZDA2NzBhMTdoZDczMjA4YTFqZDhmNTllZjc3N2hkYTg3MDQwMWpiNzVhNTRlNTYzZW5vbmNlTBZguMsZADREIeYq5g== \ No newline at end of file diff --git a/pkg/container/containertest/Base64StdPaddingGzipped b/pkg/container/containertest/Base64StdPaddingGzipped index d619ebd..1fdd6b3 100644 --- a/pkg/container/containertest/Base64StdPaddingGzipped +++ b/pkg/container/containertest/Base64StdPaddingGzipped @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/pkg/container/containertest/Base64URL b/pkg/container/containertest/Base64URL index 9fe7b78..2113e30 100644 --- a/pkg/container/containertest/Base64URL +++ b/pkg/container/containertest/Base64URL @@ -1 +1 @@ -CoWZjdG4tdjGKWQGeglhACpXpdxwBGhOBgP0i_jkbddYEXF1SIqsFv3_hqgTujlJB1hiKUArH2AIsVSKtpGH-g1tI691qlTZrWoWbReObC6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raEFHdzlXbk1yMlpoRHlBalpVVFJ0UEZURmh6WXN4SjF2R211NXVEdDJFcFRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rc1FtZVcxb3RUeWNQeWRnQ1hxakxRS2JaRUZSMlF5cXJwYWZUZjVhNWFiYVdjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rc1FtZVcxb3RUeWNQeWRnQ1hxakxRS2JaRUZSMlF5cXJwYWZUZjVhNWFiYVdkbWV0YaNoYzdjOWYzM2VqYTYxMzQ2ZTVhMGhmNzNhNDJjOWplZDU4YWQ0ZTg1aGZlZjI5NTlmajBjY2NmN2I5NjFlbm9uY2VM4c8LJwfjIdKZbUd-WQGeglhAGRizpuAMSDUiVK3a7JS_Wr-Z1lqn9eASP5He4iZi2lsXcqgKOozEHOBddL302HETEZyWvZJpjsr7pRDl8nvcA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raHo4bWtKOTZOQjN5Y1VnNHcyeWVQbzd3cnJwYlVYZkZVNEhyQ2tEaEg0aEJjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1raEZ3SmhiNENaQVBScHlXWmMxNXA5NU04NDE3RW1RbUdIU3liMlRwOW95QVpjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1raEZ3SmhiNENaQVBScHlXWmMxNXA5NU04NDE3RW1RbUdIU3liMlRwOW95QVpkbWV0YaNoMTYwMTM4YzZqMmJkYWYzNzcxYWg4M2Y4MTdkM2pjM2IwYjdmNjQ5aDkwZGMxODA5amMxMDZiOTJmYWZlbm9uY2VM0GJFExCefuuIz3DAWQGeglhAKgFiPWomi3iuL4RcE_XMscG8JjAUBCTsCu9_aQY6U6sA_E2GnlI37ASYcnqgM0Kk4BLjta2nHgKh6T26CtY-AaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdkc0aXJlN3J6YmtTYXR1MVl4c0J1R3FLczlrTmdHWnRUWFFCNkhXRm1iUlRjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdnlwMWdWNlM3TFhMbUNOUEhRblR4VnBjeFJTNlhuZkdIaExycHVldk4xZ1VjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdnlwMWdWNlM3TFhMbUNOUEhRblR4VnBjeFJTNlhuZkdIaExycHVldk4xZ1VkbWV0YaNoYWE2NmY4NDdqOGQyZTIzNTM3ZWhjMzExYjNkNWozOTZlYTJjZTE5aGQ2NmM3YmVkajg3MGQ1YjNhNmRlbm9uY2VMw0wBN_QOVu5WH3ZUWQGeglhA4uBojvXBeoEJ4qnDmtHrtZArqBOj1jRMQrXOxy75JD2xpBhhxW_c90ofhjexFy5m-TM1a9pciFfVdgUwc8a7AqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1razVKRXNDUW1tWGZoMU5XazJVSjJBZ01zYmRxNDhmU1JoaXdiTmJmN0ZVbVFjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1ra0VXanFxRkVwZUJlakJBREpneTNRdmZBU01KSkZjc0FIZmRqNkpkSlJKSnZjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1ra0VXanFxRkVwZUJlakJBREpneTNRdmZBU01KSkZjc0FIZmRqNkpkSlJKSnZkbWV0YaNoZGVjOTBjOWFqYjZiNGU2NTRlY2hlMGYyN2VmNGo2YTJhZWZjM2U5aGY1NjNjNjEwajNhM2EyNzA4MDJlbm9uY2VMdQysOoQ6Wn3YZJ9KWQGeglhA58PG4OcNtwHt9nXjgcTBebRvhQhVTG6tYyJvTNeFaJj9V-JD3jiT4ZgLJKFenedmwNA0Et31Jfu6MNqkiYUcCaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ramQ5THQ0WXRKNFU4TjRRc29hUVRveEFqekxlMkZnbVZXUkhycTltQUFmTHVjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbkJITFl3N0tpbzVwb2NhclVoTW9BWmEzb25nZXpjcmJpcHRLRkxGU1REM1RjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbkJITFl3N0tpbzVwb2NhclVoTW9BWmEzb25nZXpjcmJpcHRLRkxGU1REM1RkbWV0YaNoNDNmNmZjNjlqOWUxMGNhM2I5NGg2YjNjMjYxY2pjMzBkZDlmYTI2aGNkMDAwYzU1ajA2YjZjMDNiM2Zlbm9uY2VMbwisskfMLx4-gstgWQGeglhAIgN-nA7mA4g2FKr6_CR3kCPzcegoqgIDWk1qCotQWLo0ntG2WoSfnJvPWj9o1HLtxTAKAWzexFwiBTamow2EA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdm5lcWd0ZG52cHVmdHQ0emJHSEZxMnRMSjRkc1dWN2N2NHhrbjlGajlXUjZjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcmhpc3JqVkFWOVROQ3dkejY4NURpdHY2eWRaSjIzWkh0cUFjM0E4SHJvUUhjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcmhpc3JqVkFWOVROQ3dkejY4NURpdHY2eWRaSjIzWkh0cUFjM0E4SHJvUUhkbWV0YaNoMGM3OGVmMmFqMjI1ZDgxYTg3ZGgxMzIyZWU5MmozZTJjZjNmOWZjaDQ1YjM1MWYzamExYTMzOTU1YzZlbm9uY2VMGkPaJ3crRATPQ7jrWQGeglhAFK5Ex__PUA3vkGgS6JKYwyGf03AvjNKLW7S8aDnC4lRCh0VjOdMQ1xHHsV6fsqBDcPC6dwqpFTHOO9YbMPBNCaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rZkZSRndzVVNNc1hzWVRvWGtWZ01mQnBwbTdxRkpqc2g4Z2FSZjY2Q1FvMmhjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdE5UdUhUakI2bWhhMkNKZFFRQ2FyUFE5cGN5a3RQcE5DVmg0TVhMVlNQNlFjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdE5UdUhUakI2bWhhMkNKZFFRQ2FyUFE5cGN5a3RQcE5DVmg0TVhMVlNQNlFkbWV0YaNoMWU2Zjc4YzVqOWRhOThkYWVjNmg4YjRjYTg1OGo4MjNmY2Y3MjliaGYyN2I1YzA1ajU4NjkyMDEyMWVlbm9uY2VMrIMpUz2MjCvzE0tMWQGeglhAy9s8amUFAKS7soQbA4oFzhU4cVRJ73LhUFicmTM3q5dd3vVfy-xeq7yhc-8orqEvsdiXPRVbfZdFtjmEbjRSCqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1raVoxMUxWUm9keU0yRmI2VjlUbmF5M3g4bVhVa3p0R0c3TlVxZHFrOHRYYkFjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rcFVKc3J5b2V1Mnp6WVNBdng3Z1VRcG0yc2JyRmVFSnFNVERIYVVnSjhrd3djcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rcFVKc3J5b2V1Mnp6WVNBdng3Z1VRcG0yc2JyRmVFSnFNVERIYVVnSjhrd3dkbWV0YaNoMjdiYjAzZGVqOTY2NzgxZTQwOGg0NDFjMGIzMGo0NDhmNzEwZmU0aDc0MmM0NDQ5ajdiZjVjMjhiMjJlbm9uY2VM7ctTlMIv0HNvXrsUWQGeglhAUgkAhE9RqmEC6Yx5EUrhcq61qNc3tJ1x1bfAuPO01gxVkD9v4PebljD1PVtYE8uFgQtMFA0u0oI4mHTBHcx8CKJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rdmZNNVdxZ1JlWThWN3FLRHdWaWs3NHRLVmhjNktOeEoxcHQ2MTNGMktySnpjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rdTV6WmhUajdENTF0azNGRTRNYUtCVm9paU1rRnpGNHdNOFl4SFhybmpUdlZjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdTV6WmhUajdENTF0azNGRTRNYUtCVm9paU1rRnpGNHdNOFl4SFhybmpUdlZkbWV0YaNoNWViOWY3YmJqOGFjYTI4MDhjOGhhZTFmMTA4MWo0ODllM2U1OTk4aGFmOTUwZTU0ajM1ZTMzZDNkMTVlbm9uY2VMn16caMQ_aieJes1jWQGeglhA3zJcxENVVf7UDXPewhqsoEHO6Y2fAjOkpjAjdpz244-w2GUand8PTxp1E7ET8FqFx9gD74WjPbzamIoaWsCzAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra252UjFjb2hRNzdNaU1mZ3JWelpvdVlGaHk1RTh2YUNXckFvdk1zNXJvZkxjY21kaC9mb28vYmFyY2V4cBpnfrIdY2lzc3g4ZGlkOmtleTp6Nk1rbmFhZmdwaUU0OHo3WjQ5N1F6NkVXQzI2ZlluRlI4NXJjYm5kV3Q4UFZoVk1jcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbmFhZmdwaUU0OHo3WjQ5N1F6NkVXQzI2ZlluRlI4NXJjYm5kV3Q4UFZoVk1kbWV0YaNoMmViYjFjMmZqYmE5ZGE1OGFkM2g2Yzk3OWU1YWoxNzM3YjBmN2Q4aDkxNGM3NDE5ajZjMGE4ODhkNzBlbm9uY2VMgiJAdle-huUpDQ2L \ No newline at end of file +CoWZjdG4tdjGKWQGiglhAXK6LJ-9JgdpCjLi54ixA5OFBFp7vBU02xIDUImpOqQivENmMUTPlVBMItJicruJkQW9TFYGIgdU834Sc-cX0DqJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa216WnZIQVhqWEN0QlJ2TFZhbWpBcmdOTUtXclpiQlozeXhlRmVSeTVic3p5Y2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3Q2NndmdXF5Zzd3RDdiS1VCQnhtbmlLOEFjY2dGYkpIZFo4U3hReHVGcG5uY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3Q2NndmdXF5Zzd3RDdiS1VCQnhtbmlLOEFjY2dGYkpIZFo4U3hReHVGcG5uZG1ldGGjaDBhY2E0NmVmajU4NDMyMWIxZWJoMWEzMjY0ZTFqMTIxMDg0OWM0MGgzYzAzZGJlZmphODI4ZTRmNjYwZW5vbmNlTKalaRQbcv4bIk1TTFkBooJYQEiwfwLY7-QP5CMQZZxZ43YFfownZz6nZ-jIiWSV40KCxlgrbFwI4SMqbZqGI-Liw6kzbpZDbYxM2gfz9euI7QqiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWttZ0d1azhFQjF3NUU2Nm9WQWlYd0MxZE1UTTRNcnFRTGdjMlhkTFRpMzdFZmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWttSFJMaTh6Y0NXTTZTanVSOVE3eWlGc3d1QnRqZHlYTmd0TUZ0dFhodjkyZmNwb2yBg2NhbGxjLltdg2E-Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWttSFJMaTh6Y0NXTTZTanVSOVE3eWlGc3d1QnRqZHlYTmd0TUZ0dFhodjkyZmRtZXRho2gxNjk2MTc3OWpiMTVjMjNkODM3aDkxMTRkODhiajkxZWYwZjBhMGFoYzNhZGJhZmNqZWI1MGIxODk2MGVub25jZUw1LGc2wZfkb6IbLW9ZAaKCWEAWLLHLuchRVuT6He5PokIoGcpkDmCD8WSdTYFxIT-owTrprc-XR2bVF_KmVr_dnA7KDIUhyEv4c6WoO_euiAsMomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1ra0ZyZkE5b2tQclFueHhrN2JMTUFWZUw0RkNwUHZwZDhvWWhVMW5OdDR2d2ZjY21kaC9mb28vYmFyY2V4cBpoki-3Y2lzc3g4ZGlkOmtleTp6Nk1rblk3eG1zaDQ3R3UxNkVKNDV5amF3YXdGQVZmYWVSN2hDVXFGYXpOUkJBU1ljcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rblk3eG1zaDQ3R3UxNkVKNDV5amF3YXdGQVZmYWVSN2hDVXFGYXpOUkJBU1lkbWV0YaNoOWRiNTk5M2NqYWYwMTg0NjkyOWhiNDY0YjI3OGo1NGY1YjI3ZTIxaGYzNDNmYmEyajA5YWVhZGJjYmRlbm9uY2VMFp4p-yZ4Ogcmi0vIWQGiglhA1zTngEaE-B2DWSRtYnTIVBw8B_hvjK-Xo1HJZJBmTor-ebgmRED4VSEO6ALSnJV94w0UVE5xtYIaFXMTWD0ABaJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3NTTnQ5WVZUYnBheEZFNFJXUWsyQUpUYjFZNDZKTmNaY2ZvRHF3b0dUQnMyY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa3dNd3VjQjQ1MXZ0MWVxMTJzRERWeUx5WnR2RHM3TmoxMVlNQ0pLUVF3UEx3Y3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa3dNd3VjQjQ1MXZ0MWVxMTJzRERWeUx5WnR2RHM3TmoxMVlNQ0pLUVF3UEx3ZG1ldGGjaDg4MmJmZTdlajg2NjVjZjRhZDNoYWE4Zjg0ZWJqNjEyZDA1YTBkY2hkZmMxM2ExOWpmY2NkNGFmNjM5ZW5vbmNlTAJEDRnTi6GKtY7R6lkBooJYQL4aUlU_8vy0StepejrIgZyVdJaBitXDohffD2K31q8h47gTEIFGTpiRXLPVFNClpcfq8vqz7tY4s4aFkfaf3gKiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtuSmNrRVE0NzJzaW1lZDI5M3VFVDRmTVNLa1hIekpNR0FGNndodzJiUlVxcWNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtyTkFmNWlKc3BLbzNYQW5NZUFyN3ZtR1VvaXU0Sk1YcVI4Qk5RdnAza0dpRGNwb2yBg2NhbGxjLltdg2E-Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtyTkFmNWlKc3BLbzNYQW5NZUFyN3ZtR1VvaXU0Sk1YcVI4Qk5RdnAza0dpRGRtZXRho2gzMTkxYzk5MWpjMmFjMGZiNGQ3aDNjMTVjODE2ajFiNWJkNzUwZmRoM2ZhN2Y3ZmJqY2NkN2EwM2E0YmVub25jZUxWk3ydedhXvgUkFh9ZAaKCWECJZ55_QKiJk3hKybyAvAEkiE51lyY2mNdGOfG7RTtkiQZt0l9O_-D-41Fq7qe-zeLcZOOb96n615aLCmwFqHIDomFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rbXpZWUxIM2IzY1FUV3RSQ2JTdGRLSEhDNHBTRlM0bW15R3h0R3ZiVm5Qa3hjY21kaC9mb28vYmFyY2V4cBpoki-3Y2lzc3g4ZGlkOmtleTp6Nk1rdnJHREprakJ1RTF4UnVqYngzTTRwQ21oRm5RYUVxaVBzZndaNzZiOTVrNXVjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rdnJHREprakJ1RTF4UnVqYngzTTRwQ21oRm5RYUVxaVBzZndaNzZiOTVrNXVkbWV0YaNoOTM2YmFiOWZqMDcxNDUwMzRiZGhhMzg1ZGI3OGpjYWQ0NWFmYjVhaGI4ZWRlNzU5ajlmNzU3NGYzMzNlbm9uY2VMKkjI37EdjtTTE2K0WQGiglhA5BhIT_LE47HO7eR1I5F0o4SsBb8DulP9ADLQLAbINm3-w8u_tsuO1F8JIwBNgra3k2djQgsHN7wFf4eikBd0DqJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3ZGdkFRM2ZuZTVLeWE3MVM5R0RyYnRxM0pFVTQ0eEt3OENXZ29wUTVmRzl5Y2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa25ZcE5mZllhUHg1OVhhb1JVSnFyb2N3WThuTFRUdllpdnRoWTZiWXdKVmtHY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa25ZcE5mZllhUHg1OVhhb1JVSnFyb2N3WThuTFRUdllpdnRoWTZiWXdKVmtHZG1ldGGjaDZjODdjM2RkamQzNmUzMjFlMGVoYjQzZjdlZGJqOTkyZmY2YmI1YmhmMzI1ZmJlYWpiYThmZGMxZjEzZW5vbmNlTIkknZW6tK1-EdreVFkBooJYQKyH4lVF6wSWRtLC5rNeMG93E9szx0tQ752O0A33oY7AWiyCmooVQ2QcwvNO4eYAhZ6O07UnT9cjRMJEJhh8VQiiYWhINAHtAe0BE3FzdWNhbi9kbGdAMS4wLjAtcmMuMahjYXVkeDhkaWQ6a2V5Ono2TWtxS3oxeVo3WHk2UndUZDlQZ2VuN29DWGh2SHRTM29wTlNCUTV4aHRUd0h6ZmNjbWRoL2Zvby9iYXJjZXhwGmiSL7djaXNzeDhkaWQ6a2V5Ono2TWtzTFlUMk53V0hTZ3pYTFZ2d291cGM0OTZ3Ym1mc0dac0NtWWdtdThlZlJoa2Nwb2yBg2NhbGxjLltdg2E-Zi52YWx1ZQJjc3VieDhkaWQ6a2V5Ono2TWtzTFlUMk53V0hTZ3pYTFZ2d291cGM0OTZ3Ym1mc0dac0NtWWdtdThlZlJoa2RtZXRho2gxMmZjNjdkNmpiYmQ3ZDZiMzlmaDdmY2IyZGQxajIzOTUyMmRkY2NoYjU3NzA3YTVqY2M5ZjE2MDU0ZWVub25jZUwyFq7E45CwN9A_5e9ZAaKCWEBkB2rGLezpJf2uyxS71bzCTucR4-WM-Jl13g_jLXAoiBBn7J6y6Ueak050Nm_2St91WQpXkCq8IQIwUqvCtCoComFoSDQB7QHtARNxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcHhBWkN6Z0xWa0Y2Zkx2RzdDSmdIRlUzSG50aHZnWE41d2Y5M3FiZnNTV1ZjY21kaC9mb28vYmFyY2V4cBpoki-3Y2lzc3g4ZGlkOmtleTp6Nk1rbXk2aUxjWEpOdlYycEpyWU03WE52VTZRbVFFVFVZbVZ1dUhvd1VkWTlkSFJjcG9sgYNjYWxsYy5bXYNhPmYudmFsdWUCY3N1Yng4ZGlkOmtleTp6Nk1rbXk2aUxjWEpOdlYycEpyWU03WE52VTZRbVFFVFVZbVZ1dUhvd1VkWTlkSFJkbWV0YaNoMmY2ZjcyZWRqYmE3YzMzMmZmZWhkNDUyYjFkMWo1ZGM5MDgxMDhkaGQ2NDIyNzQxajg1ZGQ1ODExMmJlbm9uY2VMk3PNrYZB3Hz0inGWWQGiglhAF2lX0RhdPOAhwR0O_peTUIZJLRoWYrzNSGTMO9aATJJ8gUvQvwY6K2WDBifwzFo6Wn_FfVNYdwDj9LmvpcTlAKJhaEg0Ae0B7QETcXN1Y2FuL2RsZ0AxLjAuMC1yYy4xqGNhdWR4OGRpZDprZXk6ejZNa3NIdWtwZXBBM2Z3cmVlbm9KaGU5MWdaaWtHWG95VjZ4NFZHbTEzYXQxazJFY2NtZGgvZm9vL2JhcmNleHAaaJIvt2Npc3N4OGRpZDprZXk6ejZNa2dveTFRYnBUcm1oOGhadXRBVVB0cU14cW9rWXZOb0RDUUh1MkNCdUU4MnNKY3BvbIGDY2FsbGMuW12DYT5mLnZhbHVlAmNzdWJ4OGRpZDprZXk6ejZNa2dveTFRYnBUcm1oOGhadXRBVVB0cU14cW9rWXZOb0RDUUh1MkNCdUU4MnNKZG1ldGGjaDAyM2YxYjEyajAyNDkzZjhjYWFoMmM1ZjQ1ZGJqNzllY2M1MGM1MGg5MDFmNWI5OWpiN2JlODk4YjI4ZW5vbmNlTDz3ETxDC8vlSUNQBA \ No newline at end of file diff --git a/pkg/container/containertest/Base64URLGzipped b/pkg/container/containertest/Base64URLGzipped index 4c43905..72a392c 100644 --- a/pkg/container/containertest/Base64URLGzipped +++ b/pkg/container/containertest/Base64URLGzipped @@ -1 +1 @@ -PH4sIAAAAAAAA_5zWa5cUxR3HcVcRiejmuCII3siKwgnubnd1dVW1eIS5rzM9w8zs3HZETFd19_T07PTcZ2cH1KMgoB41YgIo3nM0G1GCRhCRiILIEaIhIEo0Bi9c9ewaFfEazYMd8iCPdvIGvk8-p_71e1RnZaujyt_e2_LQLYk5Fbb1QefFR9-_5LzWO59-9PfWoWV_nN_f177z06eH31j13Je8-Wpk35bTccKjtV_22MRfnz9DufuSg8Ntn02KzZtw83sLxj-mGE441FIoVZhidal9qTl8J9fJdRRZJz_IlIpaI2pavTKjDVxZR_5MQXa4aFIFgr-gMCrrOTWUqWNajNZERzin0GLRz7IxsTcfAgnGsqrRpedyXVQpMq2Wn5K68dmLWLpU-p9msRbQ-usuHEsGorZK0BbwxWzxvCdrhDK2vL23UMCRTM0RlStVxPK5vpsXMaWvj3Vee90i5Wq9s6r0VbRTWalC__-smtXKyu8MERNOwJSZkCACeAFgzkC6xGkYAhOLqkJEiBXJkJAuCUBUTMIA4KEgAEWzchbT5DXHfxwc7LiqNnPmRwdHgBa8NO6jDQ9uvW6wa7DHYetfP3DxO4s7JhybLa1YcerUV-Yae7aDww_P2_zjvH9euWqOOW3yjBWtO2__bKzLvfnR_enEpmaBMtFuy4qEsgrlqRMpPm86mql6apjI4V63V_bFgl5WT-USpQhDowVK2amiFmM8tEUsiRJcCeowHM9AvVQreLGE83LMkcBBI99dAE0ANZNtAGFewJjwkilBJPG8TphoKEQhmihKpsgBXuIVhXEGRRQpRGAmhzkRqIpOaQNo4afuoTWTj60cfjw5fQQoxy98ZPGmF3YsWLJo8voJ587Y9-X38geyLBq_nHXZIy9fPun5m1pebqVzv7J-NuaM08XEzYl3v3kmtfuCr2Yc-VAa3vtxS7NAnpKSKsBEzVaMphWdFB0kgNSEgHpwiPe7a0qaxYtVG_SVq_7RAhkey-asYmfUZ1XKMbGSr-Vo1UZ60rZyNV6Uy8l8MY_iES1ic0abAGom2wCCEqcLVFRNpEKBaghAaCBJUYgKOVNVGMQCUyXRQBqFTJWwKQkIIoZFQW8AvZV64bRDez_6flYbbmu8oDa_euu2yJB9w1vX3uO-YrZvobj2yK07V7-1YebjrRcgbsvS4fiyJ9_gpx4KvT5n_A7PTz-cqF_asuuaoTFnn2N-bbU2CaSHi0bJlrEi3kyh6NFrgqOu59VKOJ2t9Xj4sllT4wXqJdFQfzI2aiBvIGSG_Zls2IsDSdKblRV3QlQz2X7Z2evudVBk16QIM1yiCZsBaiLbAOIEnROQppu8AjgKiKqIhgQVwDQCTCKJkqCIBIqGpoicCilvcoxDWJewcPLE3TD0fHzb0y_BrtRd2gjQN3fd--36ju_CYzuV8adcsOeA95UvVv-09N1t05_bN7DllouWTN-18r4nTm-9Y8uZ2L02fj1XLizHlzyfrZ-x65Pt503b0uyJs0oDhpWuSh4vwFpJjlcDciKRCbtQTzmvxkWrKIWCUQm51FomO1ogKyMLyXSPx0xHxKrixwFT7a9XgzUUTuedXgDi8XKmgGJmJOwMNQHUTLYBhERGkE6YCTXEYYAYxxuE5xGnCbopAkWjAtCgbhCMKAUQmQqkvA4wJaQBpNyhPfDyK9Ijs-yOv40ArTv_hoMtSbB12NjtzD1Ef35ow47k_D3frn41f-2LX6YmdTxzzba3t180_7XFV5m-fz304tj2Td-t7Nx_oblszv7ctI2Hmz1xhayl1myOkhEOxUt2S_S4shFvMI7dIbseyPa4AlalJ8q6g2KhGBktUJGVfQEaSsXtsK7EnXnC6zIP84ZHrYUyGR9vcxXj9lwuaIvG7E0ANZM9-QcBHkNCNFMFEKhYFTRiSBoUOV0RTE3DKmWMCJKhijzgeJWakgoIwyLUuAZQSlq-Y177mBOSHt49AjRF-PBAe9d1H3ffcvyZczrHLAhP9X3jC5y797YFa-mNxYl_evDqo4fu_8NZh3d88un1R1f_5cfBf8_76p3tv_psyooll_k-nj2mWaC6b8BCmVrCytYjIFZKF1wytmUFKdijVuoCLIdrBV8dk3AxbIwWqJRAkpyRMnkY9ReLcipX8PR3u6RcrheykFINGCCc7-lJ25LMpjUB1Ez25IljDHFYpCYUiUYR4yVkUMKYIEiSiTDBSFcRgAajPAOU6ibjmShBToOwAXRf-6V31zceuXHdjHV9I0D7Jn7wgXzr4TVX_-PYw_mpLatO69_qW7R1Q-m8o9OcsGP2rM_f3lM_3n0_-aL18zWHV-eHzkik9r4_LriMt16a98Tm889sdiTASDqRwFLW3evj6_mU6i96cqInGEY4Ea0F1brlsPUXJSlCrcpogfRCzGll1YKRsqXrWEuVajq21wK5buKv5FxOV7wQy4Wz5Wwg3NRIaCbbABKopoi6BkymSSrTRCpSQ8UqEEQBmkTTBQp0VcSGBgAvYaiZCgQMUiz89wXdVtm7cNtjqvLmb5ZtHAHq2vfX9Uem2q-ZXTzw3oub0ruemj52f29hwuWeZ2_6_qylidcO5d9p3310-_wptTV3Dv1WXZzbuLsNDU2-dzDy7Sf84MxxTQKZAxWi5wO0Agci0WTOnStHklUpbPaHSi7JcsV9UbvH6_IHzHTVNeqRwLtlj6rV_T3ZehxGex2KLA3EXX4XhNmaGZdj-VjCRgy37HA4mhkJTWQbQBImhOkcbyIeQA1oiOcNBVBEdBWYlOc0IgBAREPnBYoABSavUAI0RhFuAM22T_nzs8NTTyz94YFVI0Bze_9-4utz3dvfW5B7fXL7uJWvr-1rO-uG5evusY_jlk-ynvzheOfE5IFIt4ieunAfO3bK-CWuX2w-0HbQP_fDi-8Y2NPsitNYLCfr2YLGEhrwg7LNoL3ZWKLSAzwDxNvNV_OA1L0oFVdToz5xuURswBupyoo7Ggua8Vg8kIjV5TLvcyCUDJsV7PXKuglTehn5mwBqJnvyBSEd6qpKTV5HCEOeU4mBCJSwpImmIkBd4BRGoUEUHUlMQ6amQVVVJB2CBpBwxRsH-9-cvnYnOfvd_wQAAP__hMJ751MQAAA \ No newline at end of file +PH4sIAAAAAAAA_5zW65MU1f3HcS7-hB8igpfIgqUxCqLC0tfTfUADMzs7M87ObubaOzMEsM-lp6eb6Zmee49GvEKwVBQRgygIUVHLCwoqoJAAikCQaJRICCqFGPCCcXVFoaBMJbukKs929h94P3nV-ZzvSg2XrEkV9u7kwFW3J6bv7bj7tflfrjm4e8_grc7Dke4jPx6c_Mm87olH_O9Of35_59-X7z2xe056dKx7156F074aOXzCcemCRe-Y4T3n3v7SNV-fdd4qVfcLA48NPDbwArtYxqo1mcxJT2ebmWZmUgE3s09jtUxqMsmQKSZ1ptRBu5lF0WK75ZbawpVO25NW2rLtfgG5XFZKtlwBr1GPVfyKU9YjZS_BOEv0yVouNxmpBUxr-TH6osmv4Uyx-L9NM5PUWqLtmqTohbQnkWp1i5JlxiOWLxAFAdaJeU13Z0gwWsN2AOdzc267A6tz5uDmGTPvUH-pNVfUOWU6CBfLqP9ZkqUl9fc6w3KqRmTVUDWGB1BiiKoDCBhJptRABAuCxrJY0zHPQBZjbLCY8hKCVBOplbMwDXbdsO9O4ZT5m20fTPywBymh3HfTBjDl5MzlT65xpm15kC4-f_2hlTtKyQUbrj78wBPfjn1e3T9i7OmuEfmt25d4z_q2K_0H36lRiw_9NbK0Szy0d3A_kPKuFk6qOYqVTSCj2uGNknxdT0BN0Etlqklxr8QaSqzF468oQl-R0oo7rztKQaK5crqzmMq7vOV4uh4jFZ2zPIrfFL0hS_ZYOFikDSA1ku1FAqJEZIRUA2gMy0OeY4EOKGVlHmCDIgZKKhYhp2NKGQR5YvBYZAWVp4j0Io1b4N905xU_e_SJj1-4pAfpbefa207fnMit_9XP39xxw7D3Fk0bH7vs09W_3uwcG3Vk5Nxv14yYP2Zj7lLPQ01Nnw0bsmXTssuZprL6-dX3zB3U9NH9q8_tB1KhgwUG8gA54HH5OcMyW1vqjmLnO11GNB0Ph6ut8WI1zAbdSMv2FclKhWK2riCS9trFvDvmd3tC0bpo8SAeC5QExZfiO3LeIOuKePkGkBrJnkEiWJYlnjEg1nhERSKouowIUgEvGUTGRBN5Ims6QiyVZVUwNIp4GUNOxr1I199x4OH_y6103Q-3DOhBuhYw1x1Y9eHYjejREfahp2Z9SNaqo83Xb0yff-mFr3_srFg7at3FV2aEy28atv2p8zbc-313YOQ3y65gm1Z9MHzdipHm0H4gFYmaLrQGWjpFLp4ICmagamWKHI560tlAsoa1uNcotoQKWj7fGuvz3Ck1v5hJgvZQJGYIKTlcFrypWocVLHPuEC_oWa2e0M3OVsen-BuZuwayZ-YOckQARDJ4TLAsChqUdVHVOBkA1pA5GVKGkaiqA04CDISiIcoUMhRjCHuRmjJraxO7f_ji-PiTE3pf0g83F4Zc-hF4-7uTV4XPG7xw986TC__2_qlnjrf9OLDrrKfeXioPGvtN0-NHRv8OHh7_5fzP1V3Lt40Zar-z-c3Vtx5O54b0AykTCrGsVS1iKmkdapQ1q6lqKWkV2gD1J3MIY09SlHw5O6yHgn1FqnoTLU66YobsbASoKq46rmoka2c8LomNkHBFYH2VVldNa-GTQgNIjWR7kTiVAIYRsYEFTcMCgpDqogw4TFXGwKqmYaACRtMJ4ClmEDAQFaHAYAbSXqRXxt9z8fIVxXOXjpjI9SDtXIkK6MJ3T6xbv-WuN7azi9K8UGl5a1t056x9G6fgxJJ5g03lSBk_O2juXV0Lnh2kVHfIwcqrKw-9dboId5X-eU5__iRPLhyxMsF03C9ZnWYKtRfZajIbUDjb68k7XFG2UL5adKesKOwrku42qkgrRaK2lI1jYJaMelnpaLf9RhW0sfmUrjk-FymaAcHBDSA1ku1FYjVeBBpGBqUCpjIUKdI5TuAwwqqhiQAAyvAQ6BoDBCgiwSCizMsSQhzfi_SP-2K_-PGE9saxTzMze5Dy-z0P7Di679olRys3fzdIXbZgbpvy0Krm5oXnnP_1w9yzj0yF8_64bCjzW2H22feEv48NLk6_8UD32fWPNvlf_CS18OUB_UDSE1WcaNUKSsqx_SjA-zNeLYI4T0cybfM4pbMg4AknnbbOetjqK1K-blcKUt4DWcNrVBzBIiJ2fGE7W8gCKZbI4WhBy4KkP9MWhA0gNZL9LxJLVMQhA_KEkTAjEkbnBZFILMQGQRLPi5gSpAOqEShqqoFFWZA4jCHoRRLeO9XcMqtwy7p35cd6kIY99snirRBtnvqcHN9benz4hCuG3LJt9bafZki7thTP_mHpi5PGXfbY6N1Nm68Z992tLD51S-q5DR98dsnhAbNSNzwZvOr_-4FUTdpy0XIjlg0nZSHkqaneUBVUCylgZEtmwQ3LhZrhR3WtkrX7PHegSpISVKtKuCYFyu11s5pK6FHbR112BNtsIZUOt1fSHVIuLzYydw1kz8ydoGqUF4nBizIHZYFyog5ZLHCU0QwEOZGROVEiusr824blDCgBSSIMgGdO8ClPP7KYPJnsQIeKG3uQrnz5teFvvD98hrrOc3nrmK3rn9s_-09PjBzaddOwxDl3Hz1xfEx810uPhvYcbL3sL9dNWjnuxamjXnCFEheAP29i5nXf3tGf6y7r6qwrJZfHlLyGLCvuWCf05Qp6JGDYBaNWqnvS5bKN_VRBVaWvSDnJBG4lHBEAl05yIccVIHZabmmLBttdobSjaKY3FRY9alvY42oAqZFsL5KKCAaEQkMlokgpyzKijhlRpaIoGZClCLCU1ahOIGQhFWSDQwLLqkhlzrwkvOH-ScEtSw68Omr87B6kpYWRa185WbvuC8u8aMC0i1pXbHeO72P9982fsHhh2xbf8WGe0y5Zq-wAk3YenZ05evG93MzVAx78aXJlxr1r2hce7A9Srqh6jIIVjNU7INENUg7Tqmz7tFBb2pfy-oq6zxuOOO0kWo27-3yC58tCwi900DxOtpk-k2txFCfuCGKllnOscqVk-W0N1dmoFcw3coI3kO1FElVWYhiiGhzgCS-xPES6zFKVAZxgyIAXJZWjAOsygppAEG_8Z-wIQZzai7Ri5eyp3V8981nL9fET_woAAP__svLMp3sQAAA \ No newline at end of file diff --git a/pkg/container/containertest/Bytes b/pkg/container/containertest/Bytes index 32c6d7ebf0c58d879d4e70b572404ed0ab735277..c256776f93a3602ec7b4d509fa1642c07b36ed05 100644 GIT binary patch literal 4220 zcmbW)X^<4<83%A62(lI+8m(j%5Rf3^?)LTefdrY^x$HTdnVnlScE5f1&h$)A_gpa| ztcqGH2n8sL36MkxDvAe$HL=i=!>B8ViV)GnBP)s&i4aXnO;_#sA>Vek`dfF`|3m-k zdES37+9*M*&{09}&5qu*EVJm9Q6InZ^5DF}np>xQ`1Ovb2G;$F?6jW0V9%?2ZoT7b z#y4+p?BQ`*^tUHgz5e!#FO*D}>X_zVzUSs+zq#-BfzOS(OFzBw*N42L z*3BH)8W?-+!8?>k|MKG{2X_`1Sd-w4&G7#IjpvU9N%Xt(g3G5&29KB=!R9bwx0%m_SyaGcf2}2fPH%FK6CTp%Au*h zdnUNdT>tSChn_q-eewa~q9;#u^t2U^)&6;ZKRwX+V9W@RO7&VbkuTOlK~rNAi3Veb zT?Q64eI8%6Vo^yxT_O?*56)I{#&D6Wio(Vt&Ln5#vYXUxE#oC?i6U)9(OB7F(m{+e zdC{YI^4*CRkXngtBB7k4#7mrpiG+kBJT5_nz$ivQI7X955kUw@;)Eg*B;+8&XgH8i z0^%&DVMd+S7TCMB0pHRDTrN_pCG@7Z!v!+&w zi9G6NK_TidxLm1h3rMZRHjyw?IbBoRy!6+m&4GcdH~xH*?IOpL-462s#p zrU8i%P}F%%Xd)e^*RF^h$hNhO_H+#)xtW(HZIHjees^m7ON%%5U;6#?yXCR%r{=CQ z77ir7`P$lfuIJ$uQ=XnU`@+KyJa}eJ_4Ldm>*m~iVDICrkM4N}dvD?hki@#oxXHSR z8oFP}H)uuz{z4cKs?2h-5zUl{YJ6E65by!)5C+*g?s zbF;Ul&b+n%qT=AF!MQJ#MycL+_r11#eb=#>-xdGcaqZ+;`#d`@yq&GyIbj4ya!#nw zL8}^NycHV91DFz5^;p%zyP0^&lc1rNHWU1o$&0X$4le(JXc=##rUP`9~!%0^^yMH{`%sGD;0th09BZ|Obr~o+0 zxd%m3bf5%9;+^5bP)uVaXO&2#ye`)enh|U)?M$8TPS*G&HnxIXis#^$vM}&AN}%Q!Z-5P zdrn?HX#_~6SiHyJf-a+;%f>`C70-1;Z?zsN`vbLf9n0%cGpOW4M95a1E<3ymF2y3n znquo}I!C%1Caq?~bT(3}1*&dCNo#yQ2Y|vQ)7~IeYyqj2*dLH2K$8SbYK+Knh#>M5 zstAxqNSXo~PVpE55QIk*iGrd4NM{8i6bdls3ExDTcx2bW`TJisuh{qcgdrrLpZ9bT zKRsz%;KW^j)t~Cyb#jUR%9M}y3NLGuXWrocI)L;&0KTexkU#Po9}rq zv1P-H?c)c%m-UYXN%O^U!_G+@jj}=`N5K(AU0t_cX%_->;CNT(4G?8{J^uGD`@g~|g_3&%WN%FiE zl_!s(rp;Y*$^LzzMbYZy;)Xb$Nj$dsZ@v55KAW*(-jly*iydCP{79y9^AiJ`itMV< zxw$m*2mISdkK!Y#;7X$>rRm*iLyPd$hKgk?xH}~GunA8%62WUhX!`SpnW-eP3OQUP zjm6U>N2ha{oNeJ;CeDLm8WI6BYpFdHP$(A<%9>qICSA1_kXngtB4McWj|wqf6QKYI z5p({Q6cTZc9}M6cDmWsHv)l!RmM8`TfYeBVbLt?#_#f8YnNnfax#eBgUwqp5$NvC) CDkO6N literal 4180 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 G5C8y~7K5t* diff --git a/pkg/container/reader.go b/pkg/container/reader.go index c96bf58..82c368d 100644 --- a/pkg/container/reader.go +++ b/pkg/container/reader.go @@ -142,12 +142,29 @@ func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) { return nil, delegation.ErrDelegationNotFound } +// GetDelegationBundle is the same as GetToken but only return a delegation.Bundle, with the right type. +// If not found, delegation.ErrDelegationNotFound is returned. +func (ctn Reader) GetDelegationBundle(cid cid.Cid) (*delegation.Bundle, error) { + bndl, ok := ctn[cid] + if !ok { + return nil, delegation.ErrDelegationNotFound + } + if tkn, ok := bndl.token.(*delegation.Token); ok { + return &delegation.Bundle{ + Cid: cid, + Decoded: tkn, + Sealed: bndl.sealed, + }, nil + } + return nil, delegation.ErrDelegationNotFound +} + // GetAllDelegations returns all the delegation.Token in the container. -func (ctn Reader) GetAllDelegations() iter.Seq[delegation.Bundle] { - return func(yield func(delegation.Bundle) bool) { +func (ctn Reader) GetAllDelegations() iter.Seq[*delegation.Bundle] { + return func(yield func(*delegation.Bundle) bool) { for c, bndl := range ctn { if t, ok := bndl.token.(*delegation.Token); ok { - if !yield(delegation.Bundle{ + if !yield(&delegation.Bundle{ Cid: c, Decoded: t, Sealed: bndl.sealed, diff --git a/pkg/container/writer.go b/pkg/container/writer.go index a8ca0ff..1ae73ec 100644 --- a/pkg/container/writer.go +++ b/pkg/container/writer.go @@ -73,12 +73,12 @@ func (ctn Writer) ToBase64URLWriter(w io.Writer) error { return ctn.toWriter(headerBase64URL, w) } -// ToBase64URL encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding. +// ToBase64URLGzipped encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding. func (ctn Writer) ToBase64URLGzipped() (string, error) { return ctn.toString(headerBase64URLGzip) } -// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer. +// ToBase64URLGzipWriter is the same as ToBase64URL, but with an io.Writer. func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error { return ctn.toWriter(headerBase64URLGzip, w) } diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 09dd312..5fde690 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" + "github.com/MetaMask/go-did-it/didtest" "github.com/stretchr/testify/require" "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/internal/didtest" ) //go:embed testdata/new.dagjson diff --git a/token/delegation/delegationtest/generator/generator.go b/token/delegation/delegationtest/generator/generator.go index 4f26639..b9bcb83 100644 --- a/token/delegation/delegationtest/generator/generator.go +++ b/token/delegation/delegationtest/generator/generator.go @@ -12,6 +12,7 @@ import ( "github.com/MetaMask/go-did-it" didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" "github.com/MetaMask/go-did-it/crypto" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/ucan-wg/go-ucan/pkg/command" @@ -19,7 +20,6 @@ import ( "github.com/ucan-wg/go-ucan/pkg/policy/policytest" "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" - "github.com/ucan-wg/go-ucan/token/internal/didtest" ) const ( diff --git a/token/delegation/delegationtest/generator/main.go b/token/delegation/delegationtest/generator/main.go index e5d5fca..09618f4 100644 --- a/token/delegation/delegationtest/generator/main.go +++ b/token/delegation/delegationtest/generator/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/ucan-wg/go-ucan/token/internal/didtest" + "github.com/MetaMask/go-did-it/didtest" ) func main() { diff --git a/token/delegation/examples_test.go b/token/delegation/examples_test.go index 7537498..fdf2b2a 100644 --- a/token/delegation/examples_test.go +++ b/token/delegation/examples_test.go @@ -8,6 +8,7 @@ import ( "fmt" "time" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagcbor" @@ -17,7 +18,6 @@ import ( "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/ucan-wg/go-ucan/token/internal/didtest" "github.com/ucan-wg/go-ucan/token/internal/envelope" ) diff --git a/token/delegation/schema_test.go b/token/delegation/schema_test.go index 33f6a01..7127755 100644 --- a/token/delegation/schema_test.go +++ b/token/delegation/schema_test.go @@ -5,12 +5,12 @@ import ( _ "embed" "testing" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipld/go-ipld-prime" "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/internal/didtest" "github.com/ucan-wg/go-ucan/token/internal/envelope" ) diff --git a/token/internal/didtest/crypto.go b/token/internal/didtest/crypto.go deleted file mode 100644 index 9836d4d..0000000 --- a/token/internal/didtest/crypto.go +++ /dev/null @@ -1,139 +0,0 @@ -// 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 ( - "encoding/base64" - "fmt" - - "github.com/MetaMask/go-did-it" - didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" - "github.com/MetaMask/go-did-it/crypto" - "github.com/MetaMask/go-did-it/crypto/ed25519" -) - -const ( - // all are ed25519 as base64 - alicePrivKeyB64 = "zth/9cTSUVwlLzfEWwLCcOkaEmjrRGPOI6mOJksWAYZ3Toe7ymxAzDeiseyxbmEpJ81qYM3dZ8XrXqgonnTTEw==" - bobPrivKeyB64 = "+p1REV3MkUnLhUMbFe9RcSsmo33TT/FO85yaV+c6fiYJCBsdiwfMwodlkzSAG3sHQIuZj8qnJ678oJucYy7WEg==" - carolPrivKeyB64 = "aSu3vTwE7z3pXaTaAhVLeizuqnZUJZQHTCSLMLxyZh5LDoZQn80uoQgMEdsbOhR+zIqrjBn5WviGurDkKYVfug==" - danPrivKeyB64 = "s1zM1av6og3o0UMNbEs/RyezS7Nk/jbSYL2Z+xPEw9Cho/KuEAa75Sf4yJHclLwpKXNucbrZ2scE8Iy8K05KWQ==" - erinPrivKeyB64 = "+qHpaAR3iivWMEl+pkXmq+uJeHtqFiY++XOXtZ9Tu/WPABCO+eRFrTCLJykJEzAPGFmkJF8HQ7DMwOH7Ry3Aqw==" - frankPrivKeyB64 = "4k/1N0+Fq73DxmNbGis9PY2KgKxWmtDWhmi1E6sBLuGd7DS0TWjCn1Xa3lXkY49mFszMjhWC+V6DCBf7R68u4Q==" -) - -// 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 + 1 - PersonaBob - PersonaCarol - PersonaDan - PersonaErin - PersonaFrank -) - -var privKeys map[Persona]crypto.PrivateKeySigningBytes - -func init() { - privKeys = make(map[Persona]crypto.PrivateKeySigningBytes, 6) - for persona, pB64 := range privKeyB64() { - privBytes, err := base64.StdEncoding.DecodeString(pB64) - if err != nil { - return - } - - privKey, err := ed25519.PrivateKeyFromBytes(privBytes) - if err != nil { - return - } - - privKeys[persona] = privKey - } -} - -// DID returns a did.DID based on the Persona's Ed25519 public key. -func (p Persona) DID() did.DID { - return didkeyctl.FromPrivateKey(p.PrivKey()) -} - -// Name returns the username of the Persona. -func (p Persona) Name() string { - name, ok := map[Persona]string{ - PersonaAlice: "Alice", - PersonaBob: "Bob", - PersonaCarol: "Carol", - PersonaDan: "Dan", - PersonaErin: "Erin", - PersonaFrank: "Frank", - }[p] - if !ok { - panic(fmt.Sprintf("Unknown persona: %v", p)) - } - - return name -} - -// PrivKey returns the Ed25519 private key for the Persona. -func (p Persona) PrivKey() crypto.PrivateKeySigningBytes { - res, ok := privKeys[p] - if !ok { - panic(fmt.Sprintf("Unknown persona: %v", p)) - } - return res -} - -func (p Persona) PrivKeyConfig() string { - res, ok := privKeyB64()[p] - if !ok { - panic(fmt.Sprintf("Unknown persona: %v", p)) - } - return res -} - -// PubKey returns the Ed25519 public key for the Persona. -func (p Persona) PubKey() crypto.PublicKey { - return p.PrivKey().Public() -} - -func privKeyB64() map[Persona]string { - return map[Persona]string{ - 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() []Persona { - return []Persona{ - PersonaAlice, - PersonaBob, - PersonaCarol, - PersonaDan, - PersonaErin, - PersonaFrank, - } -} - -// DidToName retrieve the persona's name from its DID. -func DidToName(d did.DID) string { - return map[did.DID]string{ - PersonaAlice.DID(): "Alice", - PersonaBob.DID(): "Bob", - PersonaCarol.DID(): "Carol", - PersonaDan.DID(): "Dan", - PersonaErin.DID(): "Erin", - PersonaFrank.DID(): "Frank", - }[d] -} diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index 6d34a4c..72f798c 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -4,6 +4,7 @@ import ( _ "embed" "testing" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/stretchr/testify/require" @@ -11,7 +12,6 @@ import ( "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/policy/policytest" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" - "github.com/ucan-wg/go-ucan/token/internal/didtest" "github.com/ucan-wg/go-ucan/token/invocation" ) diff --git a/toolkit/_example/Readme.md b/toolkit/_example/Readme.md index 54bfa45..282dcae 100644 --- a/toolkit/_example/Readme.md +++ b/toolkit/_example/Readme.md @@ -8,7 +8,7 @@ Please note that UCAN in itself doesn't enforce any protocol, topology or transp Your situation may be different from this, and would call for a different setup. -Remember that everything in `go-ucan-toolkit` is essentially helpers, pre-made building blocks. You can use them, change them or make your own. +Remember that everything in `/toolkit` is essentially helpers, pre-made building blocks. You can use them, change them or make your own. ## Scenario 1 diff --git a/toolkit/_example/_protocol-issuer/request-resolver.go b/toolkit/_example/_protocol-issuer/request-resolver.go index ff7497a..47db4fd 100644 --- a/toolkit/_example/_protocol-issuer/request-resolver.go +++ b/toolkit/_example/_protocol-issuer/request-resolver.go @@ -4,10 +4,10 @@ import ( "encoding/json" "net/http" - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/MetaMask/go-did-it" - "github.com/INFURA/go-ucan-toolkit/issuer" + "github.com/ucan-wg/go-ucan/pkg/command" + "github.com/ucan-wg/go-ucan/toolkit/issuer" ) func RequestResolver(r *http.Request) (*issuer.ResolvedRequest, error) { diff --git a/toolkit/_example/_protocol-issuer/requester.go b/toolkit/_example/_protocol-issuer/requester.go index fa0db0e..a212578 100644 --- a/toolkit/_example/_protocol-issuer/requester.go +++ b/toolkit/_example/_protocol-issuer/requester.go @@ -8,12 +8,13 @@ import ( "log" "net/http" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" - "github.com/INFURA/go-ucan-toolkit/client" - "github.com/INFURA/go-ucan-toolkit/issuer" + "github.com/ucan-wg/go-ucan/toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/issuer" ) var _ client.DelegationRequester = &Requester{} @@ -55,5 +56,5 @@ func (r Requester) RequestDelegation(ctx context.Context, audience did.DID, cmd return nil, err } - return issuer.DecodeResponse(res) + return issuer.DecodeResponse(res, audience, cmd, subject) } diff --git a/toolkit/_example/alice-client-issuer/alice.go b/toolkit/_example/alice-client-issuer/alice.go index a6bee86..9531c02 100644 --- a/toolkit/_example/alice-client-issuer/alice.go +++ b/toolkit/_example/alice-client-issuer/alice.go @@ -13,19 +13,20 @@ import ( "syscall" "time" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/token/delegation" - example "github.com/INFURA/go-ucan-toolkit/_example" - protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" - "github.com/INFURA/go-ucan-toolkit/client" - "github.com/INFURA/go-ucan-toolkit/issuer" - "github.com/INFURA/go-ucan-toolkit/server/bearer" + example "github.com/ucan-wg/go-ucan/toolkit/_example" + protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer" + "github.com/ucan-wg/go-ucan/toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/issuer" + "github.com/ucan-wg/go-ucan/toolkit/server/bearer" ) func main() { @@ -49,7 +50,7 @@ func main() { } } -func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DID, +func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivateKeySigningBytes, d did.DID, serviceIssuerUrl string, serviceUrl string, serviceDid did.DID) error { log.Printf("Alice DID is %s", d.String()) @@ -67,7 +68,7 @@ func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DI return delegation.New(iss, aud, cmd, policies, subject) } - cli, err := client.NewWithIssuer(priv, protocol.NewRequester("http://"+serviceIssuerUrl), issuingLogic) + cli, err := client.NewWithIssuer(priv, d, protocol.NewRequester("http://"+serviceIssuerUrl), issuingLogic) if err != nil { return err } diff --git a/toolkit/_example/bob-client/bob.go b/toolkit/_example/bob-client/bob.go index 99b786d..feb7413 100644 --- a/toolkit/_example/bob-client/bob.go +++ b/toolkit/_example/bob-client/bob.go @@ -12,14 +12,17 @@ import ( "syscall" "time" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" + "github.com/MetaMask/go-did-it/crypto/ed25519" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" - example "github.com/INFURA/go-ucan-toolkit/_example" - protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" - "github.com/INFURA/go-ucan-toolkit/client" - "github.com/INFURA/go-ucan-toolkit/server/bearer" + example "github.com/ucan-wg/go-ucan/toolkit/_example" + protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer" + "github.com/ucan-wg/go-ucan/toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/server/bearer" ) func main() { @@ -43,14 +46,15 @@ func main() { func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl string, serviceDid did.DID) error { // Let's generate a keypair for our client: - priv, d, err := did.GenerateEd25519() + pub, priv, err := ed25519.GenerateKeyPair() if err != nil { return err } + d := didkeyctl.FromPublicKey(pub) log.Printf("Bob DID is %s", d.String()) - cli, err := client.NewClient(priv, protocol.NewRequester("http://"+aliceUrl)) + cli, err := client.NewClient(priv, d, protocol.NewRequester("http://"+aliceUrl)) if err != nil { return err } @@ -69,13 +73,13 @@ func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl strin select { case <-ctx.Done(): return nil - case <-time.After(5 * time.Second): + case <-time.After(1 * time.Second): } } } func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, aliceDid did.DID, proofs container.Writer) error { - // we construct a URL that include the our DID and Alice DID as path, as requested by the UCAN policy we get issued + // we construct a URL that include our DID and Alice DID as path, as requested by the UCAN policy we get issued u, err := url.JoinPath("http://"+serviceUrl, aliceDid.String(), clientDid.String()) if err != nil { return err diff --git a/toolkit/_example/service-issuer/issuer.go b/toolkit/_example/service-issuer/issuer.go index cac7894..f3e9dcd 100644 --- a/toolkit/_example/service-issuer/issuer.go +++ b/toolkit/_example/service-issuer/issuer.go @@ -11,16 +11,17 @@ import ( "syscall" "time" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/token/delegation" - example "github.com/INFURA/go-ucan-toolkit/_example" - protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer" - "github.com/INFURA/go-ucan-toolkit/issuer" + example "github.com/ucan-wg/go-ucan/toolkit/_example" + protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer" + "github.com/ucan-wg/go-ucan/toolkit/issuer" ) func main() { @@ -42,7 +43,7 @@ func main() { } } -func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) error { +func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivateKeySigningBytes) error { issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) { log.Printf("issuing delegation to %v for %v", aud, cmd) diff --git a/toolkit/_example/service/service.go b/toolkit/_example/service/service.go index f09d532..beb1f6c 100644 --- a/toolkit/_example/service/service.go +++ b/toolkit/_example/service/service.go @@ -9,10 +9,10 @@ import ( "os/signal" "syscall" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" - example "github.com/INFURA/go-ucan-toolkit/_example" - "github.com/INFURA/go-ucan-toolkit/server/exectx" + example "github.com/ucan-wg/go-ucan/toolkit/_example" + "github.com/ucan-wg/go-ucan/toolkit/server/exectx" ) func main() { diff --git a/toolkit/_example/shared_values.go b/toolkit/_example/shared_values.go index befa737..468b00d 100644 --- a/toolkit/_example/shared_values.go +++ b/toolkit/_example/shared_values.go @@ -3,8 +3,10 @@ package example import ( "encoding/base64" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" + "github.com/MetaMask/go-did-it/crypto" + "github.com/MetaMask/go-did-it/crypto/ed25519" ) // Endpoints @@ -16,20 +18,20 @@ var AliceIssuerUrl = ":8082" // Service -var ServicePrivKey crypto.PrivKey +var ServicePrivKey crypto.PrivateKeySigningBytes var ServiceDid did.DID // Alice -var AlicePrivKey crypto.PrivKey +var AlicePrivKey crypto.PrivateKeySigningBytes var AliceDid did.DID func init() { - servPrivRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=") - ServicePrivKey, _ = crypto.UnmarshalPrivateKey(servPrivRaw) - ServiceDid, _ = did.FromPrivKey(ServicePrivKey) + servPrivRaw, _ := base64.StdEncoding.DecodeString("HVcbgoj30c+7zoQzUgpl7Jc7bkXoyvo9bMX5OHaAohpv036EMxuWXGqmEWhFKHPEuRAaIGSURK8pyUYOAseiiQ==") + ServicePrivKey, _ = ed25519.PrivateKeyFromBytes(servPrivRaw) + ServiceDid = didkeyctl.FromPrivateKey(ServicePrivKey) - alicePrivRaw, _ := base64.StdEncoding.DecodeString("CAESQFESA31nDYUhXXwbCNSFvg7M+TOFgyxy0tVX6o+TkJAKqAwDvtGxZeGyUjibGd/op+xOLvzE6BrTIOw62K3yLp8=") - AlicePrivKey, _ = crypto.UnmarshalPrivateKey(alicePrivRaw) - AliceDid, _ = did.FromPrivKey(AlicePrivKey) + alicePrivRaw, _ := base64.StdEncoding.DecodeString("jIIk/4ZBgIzx7fU41AWYRUDjgQmgFTIXxN4WeZAPCjwE04oLfiHgNjwIIZi97a6WwSIL5tFGdkrqDkSmDx95tw==") + AlicePrivKey, _ = ed25519.PrivateKeyFromBytes(alicePrivRaw) + AliceDid = didkeyctl.FromPrivateKey(AlicePrivKey) } diff --git a/toolkit/client/client.go b/toolkit/client/client.go index 470c12b..6236ebe 100644 --- a/toolkit/client/client.go +++ b/toolkit/client/client.go @@ -5,9 +5,10 @@ import ( "fmt" "iter" + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" "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/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" "github.com/ucan-wg/go-ucan/pkg/policy" @@ -17,17 +18,13 @@ import ( type Client struct { did did.DID - privKey crypto.PrivKey + privKey crypto.PrivateKeySigningBytes pool *Pool requester DelegationRequester } -func NewClient(privKey crypto.PrivKey, requester DelegationRequester) (*Client, error) { - d, err := did.FromPrivKey(privKey) - if err != nil { - return nil, err - } +func NewClient(privKey crypto.PrivateKeySigningBytes, d did.DID, requester DelegationRequester) (*Client, error) { return &Client{ did: d, privKey: privKey, diff --git a/toolkit/client/client_test.go b/toolkit/client/client_test.go index 5a310f5..541e1a0 100644 --- a/toolkit/client/client_test.go +++ b/toolkit/client/client_test.go @@ -6,8 +6,9 @@ import ( "iter" "time" - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/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" @@ -21,7 +22,7 @@ func ExampleNewClient() { // requester is an adaptor for a real world issuer, we use a mock in that example requester := &requesterMock{persona: servicePersona} - client, err := NewClient(clientPersona.PrivKey(), requester) + client, err := NewClient(clientPersona.PrivKey(), clientPersona.DID(), requester) handleError(err) cont, err := client.PrepareInvoke( diff --git a/toolkit/client/clientissuer.go b/toolkit/client/clientissuer.go index 57afc75..4653fdc 100644 --- a/toolkit/client/clientissuer.go +++ b/toolkit/client/clientissuer.go @@ -5,9 +5,10 @@ import ( "fmt" "iter" + "github.com/MetaMask/go-did-it" + "github.com/MetaMask/go-did-it/crypto" "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/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" ) @@ -29,8 +30,8 @@ type WithIssuer struct { logic DlgIssuingLogic } -func NewWithIssuer(privKey crypto.PrivKey, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) { - client, err := NewClient(privKey, requester) +func NewWithIssuer(privKey crypto.PrivateKeySigningBytes, d did.DID, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) { + client, err := NewClient(privKey, d, requester) if err != nil { return nil, err } diff --git a/toolkit/client/pool.go b/toolkit/client/pool.go index 13603cc..71d9d69 100644 --- a/toolkit/client/pool.go +++ b/toolkit/client/pool.go @@ -6,8 +6,9 @@ import ( "sync" "time" + "github.com/MetaMask/go-did-it" "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" ) diff --git a/toolkit/client/proof.go b/toolkit/client/proof.go index fa6c701..14d557d 100644 --- a/toolkit/client/proof.go +++ b/toolkit/client/proof.go @@ -4,8 +4,9 @@ import ( "iter" "math" + "github.com/MetaMask/go-did-it" "github.com/ipfs/go-cid" - "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" ) @@ -23,12 +24,12 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd com // TODO: maybe that should be part of delegation.Token directly? dlgMatch := func(dlg *delegation.Token, issuer did.DID, cmd command.Command, subject did.DID) bool { // The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f - if dlg.Subject() != subject { + if !dlg.Subject().Equal(subject) { return false } // 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() != issuer { + if !dlg.Audience().Equal(issuer) { return false } // The command of each delegation must "allow" the one before it. - 4g @@ -72,7 +73,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd com at := cur.bundle // if it's a root delegation, we found a valid proof - if at.Decoded.Issuer() == at.Decoded.Subject() { + if at.Decoded.Issuer().Equal(at.Decoded.Subject()) { if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize { bestProof = append([]cid.Cid{}, cur.path...) // make a copy bestSize = cur.size diff --git a/toolkit/client/proof_test.go b/toolkit/client/proof_test.go index cfca2c2..c729418 100644 --- a/toolkit/client/proof_test.go +++ b/toolkit/client/proof_test.go @@ -4,8 +4,9 @@ import ( "iter" "testing" + "github.com/MetaMask/go-did-it/didtest" "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation/delegationtest" diff --git a/toolkit/client/requester.go b/toolkit/client/requester.go index 9cb3855..3e683e5 100644 --- a/toolkit/client/requester.go +++ b/toolkit/client/requester.go @@ -5,8 +5,9 @@ import ( "iter" "time" + "github.com/MetaMask/go-did-it" "github.com/avast/retry-go/v4" - "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" ) diff --git a/toolkit/client/requester_infura.go b/toolkit/client/requester_infura.go deleted file mode 100644 index b60f64b..0000000 --- a/toolkit/client/requester_infura.go +++ /dev/null @@ -1,99 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "iter" - "net/http" - "net/url" - - "github.com/ucan-wg/go-ucan/did" - "github.com/ucan-wg/go-ucan/pkg/command" - "github.com/ucan-wg/go-ucan/token/delegation" -) - -var _ DelegationRequester = &InfuraRequester{} - -type InfuraRequester struct { - baseURL string -} - -// NewInfuraRequester create a requester client for the Infura UCAN token issuer. -// dev: http://ucan-issuer-api.commercial-dev.eks-dev.infura.org -// prod: http://ucan-issuer-api.commercial-prod.eks.infura.org -func NewInfuraRequester(baseURL string) *InfuraRequester { - return &InfuraRequester{baseURL: baseURL} -} - -func (i InfuraRequester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { - p, err := url.JoinPath(i.baseURL, "v1/token/generate-with-did") - if err != nil { - return nil, err - } - - payload := struct { - Cmd string `json:"cmd"` - Aud string `json:"aud"` - }{ - Cmd: cmd.String(), - Aud: audience.String(), - } - - body, err := json.Marshal(payload) - if err != nil { - return nil, err - } - - r, err := http.NewRequest(http.MethodPost, p, bytes.NewReader(body)) - if err != nil { - return nil, err - } - - res, err := http.DefaultClient.Do(r.WithContext(ctx)) - if err != nil { - return nil, err - } - if res.StatusCode != http.StatusOK { - msg, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("request failed with status %d, then failed to read response body: %w", res.StatusCode, err) - } - return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, msg) - } - - resp := struct { - Cid string `json:"cid"` - Content string `json:"content"` - }{} - if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { - return nil, err - } - - raw, err := base64.StdEncoding.DecodeString(resp.Content) - if err != nil { - return nil, err - } - - tkn, c, err := delegation.FromSealed(raw) - if err != nil { - return nil, err - } - - // For sanity, we verify that the delegation we got matches the expected subject, - // meaning that we are talking to the expected issuer. - if tkn.Subject() != subject { - return nil, fmt.Errorf("received token has unexpected subject: expected %s, got %s", subject, tkn.Subject()) - } - - return func(yield func(*delegation.Bundle, error) bool) { - yield(&delegation.Bundle{ - Cid: c, - Decoded: tkn, - Sealed: raw, - }, nil) - }, nil -} diff --git a/toolkit/issuer/http_wrapper.go b/toolkit/issuer/http_wrapper.go index a0a095f..e9fa220 100644 --- a/toolkit/issuer/http_wrapper.go +++ b/toolkit/issuer/http_wrapper.go @@ -6,12 +6,12 @@ import ( "iter" "net/http" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" "github.com/ucan-wg/go-ucan/token/delegation" - - "github.com/INFURA/go-ucan-toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/client" ) type RequestResolver func(r *http.Request) (*ResolvedRequest, error) @@ -60,7 +60,7 @@ func HttpWrapper(requester client.DelegationRequester, resolver RequestResolver) }) } -func DecodeResponse(res *http.Response) (iter.Seq2[*delegation.Bundle, error], error) { +func DecodeResponse(res *http.Response, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { defer res.Body.Close() if res.StatusCode != http.StatusOK { msg, err := io.ReadAll(res.Body) @@ -73,9 +73,20 @@ func DecodeResponse(res *http.Response) (iter.Seq2[*delegation.Bundle, error], e if err != nil { return nil, err } + + // the container doesn't guarantee the ordering, so we must order the delegation in a chain + proof := client.FindProof(func() iter.Seq[*delegation.Bundle] { + return cont.GetAllDelegations() + }, audience, cmd, subject) + return func(yield func(*delegation.Bundle, error) bool) { - for bundle := range cont.GetAllDelegations() { - if !yield(&bundle, nil) { + for _, c := range proof { + bndl, err := cont.GetDelegationBundle(c) + if err != nil { + yield(nil, err) + return + } + if !yield(bndl, nil) { return } } diff --git a/toolkit/issuer/root_issuer.go b/toolkit/issuer/root_issuer.go index 5723a8c..fe77691 100644 --- a/toolkit/issuer/root_issuer.go +++ b/toolkit/issuer/root_issuer.go @@ -5,12 +5,13 @@ import ( "fmt" "iter" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" + didkeyctl "github.com/MetaMask/go-did-it/controller/did-key" + "github.com/MetaMask/go-did-it/crypto" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/token/delegation" - - "github.com/INFURA/go-ucan-toolkit/client" + "github.com/ucan-wg/go-ucan/toolkit/client" ) // RootIssuingLogic is a function that decides what powers are given to a client. @@ -29,16 +30,13 @@ var _ client.DelegationRequester = &RootIssuer{} // Feel free to replace this component with your own flavor. type RootIssuer struct { did did.DID - privKey crypto.PrivKey + privKey crypto.PrivateKeySigningBytes logic RootIssuingLogic } -func NewRootIssuer(privKey crypto.PrivKey, logic RootIssuingLogic) (*RootIssuer, error) { - d, err := did.FromPrivKey(privKey) - if err != nil { - return nil, err - } +func NewRootIssuer(privKey crypto.PrivateKeySigningBytes, logic RootIssuingLogic) (*RootIssuer, error) { + d := didkeyctl.FromPrivateKey(privKey) return &RootIssuer{ did: d, privKey: privKey, @@ -47,7 +45,7 @@ func NewRootIssuer(privKey crypto.PrivKey, logic RootIssuingLogic) (*RootIssuer, } func (r *RootIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) { - if subject != r.did { + if !subject.Equal(r.did) { return nil, fmt.Errorf("subject DID doesn't match the issuer DID") } diff --git a/toolkit/server/bearer/bearer_test.go b/toolkit/server/bearer/bearer_test.go index 0070159..23c5b03 100644 --- a/toolkit/server/bearer/bearer_test.go +++ b/toolkit/server/bearer/bearer_test.go @@ -4,7 +4,9 @@ import ( "net/http" "testing" + _ "github.com/MetaMask/go-did-it/verifiers/did-key" "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/container" "github.com/ucan-wg/go-ucan/pkg/container/containertest" ) diff --git a/toolkit/server/exectx/middlewares.go b/toolkit/server/exectx/middlewares.go index c431905..b46969f 100644 --- a/toolkit/server/exectx/middlewares.go +++ b/toolkit/server/exectx/middlewares.go @@ -4,9 +4,9 @@ import ( "errors" "net/http" - "github.com/ucan-wg/go-ucan/did" + "github.com/MetaMask/go-did-it" - "github.com/INFURA/go-ucan-toolkit/server/bearer" + "github.com/ucan-wg/go-ucan/toolkit/server/bearer" ) // ExtractMW returns an HTTP middleware tasked with: @@ -38,7 +38,7 @@ func ExtractMW(next http.Handler, serviceDID did.DID) http.Handler { return } - if ucanCtx.Invocation().Subject() != serviceDID { + if !ucanCtx.Invocation().Subject().Equal(serviceDID) { http.Error(w, "UCAN delegation doesn't match the service DID", http.StatusUnauthorized) return } diff --git a/toolkit/server/exectx/middlewares_test.go b/toolkit/server/exectx/middlewares_test.go index 00dd652..08c6865 100644 --- a/toolkit/server/exectx/middlewares_test.go +++ b/toolkit/server/exectx/middlewares_test.go @@ -6,9 +6,10 @@ import ( "net/http/httptest" "testing" + "github.com/MetaMask/go-did-it/didtest" "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/container" "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/invocation" diff --git a/toolkit/server/exectx/ucanctx.go b/toolkit/server/exectx/ucanctx.go index 04250d3..11d99d0 100644 --- a/toolkit/server/exectx/ucanctx.go +++ b/toolkit/server/exectx/ucanctx.go @@ -6,9 +6,9 @@ import ( "net/http" "slices" - "github.com/INFURA/go-ethlibs/jsonrpc" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" @@ -16,8 +16,7 @@ import ( "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/INFURA/go-ucan-toolkit/server/extargs" + "github.com/ucan-wg/go-ucan/toolkit/server/extargs" ) var _ delegation.Loader = &UcanCtx{} @@ -33,9 +32,8 @@ type UcanCtx struct { meta *meta.Meta // all meta combined, with no overwriting // argument sources - http *extargs.HttpExtArgs - jsonrpc *extargs.JsonRpcExtArgs - infura *extargs.InfuraExtArgs + http *extargs.HttpExtArgs + custom map[string]*extargs.CustomExtArgs } // FromContainer prepare a UcanCtx from a UCAN container, for further evaluation in a server pipeline. @@ -79,7 +77,7 @@ func FromContainer(cont container.Reader) (*UcanCtx, error) { chainTo := inv.Issuer() for _, c := range inv.Proof() { dlg := ctx.dlgs[c] - if dlg.Audience() != chainTo { + if !dlg.Audience().Equal(chainTo) { return nil, fmt.Errorf("proof chain is broken or not ordered correctly") } chainTo = dlg.Issuer() @@ -137,28 +135,19 @@ func (ctn *UcanCtx) VerifyHttp(req *http.Request) error { return ctn.http.Verify() } -// VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request. -// These arguments will be set in the `.jsonrpc` argument key, at the root. -// This function can only be called once per context. +// VerifyCustom verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler. +// These arguments will be set under the given argument key, at the root. +// This function can only be called once per context and key. // After being used, those constructed arguments will be used in ExecutionAllowed as well. -func (ctn *UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error { - if ctn.jsonrpc != nil { - panic("only use once per request context") +func (ctn *UcanCtx) VerifyCustom(key string, assembler func(ma datamodel.MapAssembler)) error { + if ctn.custom == nil { + ctn.custom = make(map[string]*extargs.CustomExtArgs) } - ctn.jsonrpc = extargs.NewJsonRpcExtArgs(ctn.policies, ctn.inv.Arguments(), req) - return ctn.jsonrpc.Verify() -} - -// VerifyInfura verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler. -// These arguments will be set in the `.inf` argument key, at the root. -// This function can only be called once per context. -// After being used, those constructed arguments will be used in ExecutionAllowed as well. -func (ctn *UcanCtx) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error { - if ctn.infura != nil { - panic("only use once per request context") + if _, ok := ctn.custom[key]; ok { + panic("only use once per request context and key") } - ctn.infura = extargs.NewInfuraExtArgs(ctn.policies, assembler) - return ctn.infura.Verify() + ctn.custom[key] = extargs.NewCustomExtArgs(key, ctn.policies, assembler) + return ctn.custom[key].Verify() } // ExecutionAllowed does the final verification of the invocation. @@ -174,19 +163,14 @@ func (ctn *UcanCtx) ExecutionAllowed() error { } newArgs.Include(httpArgs) } - if ctn.jsonrpc != nil { - jsonRpcArgs, err := ctn.jsonrpc.Args() - if err != nil { - return nil, err + if ctn.custom != nil { + for _, cea := range ctn.custom { + customArgs, err := cea.Args() + if err != nil { + return nil, err + } + newArgs.Include(customArgs) } - newArgs.Include(jsonRpcArgs) - } - if ctn.infura != nil { - infuraArgs, err := ctn.infura.Args() - if err != nil { - return nil, err - } - newArgs.Include(infuraArgs) } return newArgs, nil diff --git a/toolkit/server/exectx/ucanctx_test.go b/toolkit/server/exectx/ucanctx_test.go index 7d72309..7ae8a58 100644 --- a/toolkit/server/exectx/ucanctx_test.go +++ b/toolkit/server/exectx/ucanctx_test.go @@ -1,29 +1,26 @@ package exectx_test import ( - "bytes" "context" - "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" - "github.com/INFURA/go-ethlibs/jsonrpc" + "github.com/MetaMask/go-did-it/didtest" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/did/didtest" + "github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/container" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/invocation" - - "github.com/INFURA/go-ucan-toolkit/server/exectx" + "github.com/ucan-wg/go-ucan/toolkit/server/exectx" ) const ( @@ -45,16 +42,11 @@ func TestUcanCtxFullFlow(t *testing.T) { // some basic HTTP constraints policy.Equal(".http.method", literal.String("GET")), policy.Like(".http.path", "/foo/*"), - // some JsonRpc constraints - policy.Or( - policy.Like(".jsonrpc.method", "eth_*"), - policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), - ), - // some infura constraints + // some custom constraints // Network - policy.Equal(".inf.ntwk", literal.String(network)), + policy.Equal(".custom.ntwk", literal.String(network)), // Quota - policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + policy.LessThanOrEqual(".custom.quota.ur", literal.Int(1234)), ) dlg, err := delegation.Root(service.DID(), user.DID(), cmd, pol, @@ -84,12 +76,7 @@ func TestUcanCtxFullFlow(t *testing.T) { // MAKING A REQUEST: we pass the container in the Bearer HTTP header - jrpc := jsonrpc.NewRequest() - jrpc.Method = "eth_call" - jrpc.Params = jsonrpc.MustParams("0x599784", true) - jrpcBytes, err := jrpc.MarshalJSON() - require.NoError(t, err) - req, err := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes)) + req, err := http.NewRequest(http.MethodGet, "/foo/bar", nil) require.NoError(t, err) req.Header.Set("Authorization", "Bearer "+string(contBytes)) @@ -132,33 +119,13 @@ func TestUcanCtxFullFlow(t *testing.T) { }) } - // SERVER: JsonRpc checks + // SERVER: custom args checks - jsonrpcMw := func(next http.Handler) http.Handler { + customArgsMw := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ucanCtx, ok := exectx.FromContext(r.Context()) require.True(t, ok) - - var jrpc jsonrpc.Request - err := json.NewDecoder(r.Body).Decode(&jrpc) - require.NoError(t, err) - - err = ucanCtx.VerifyJsonRpc(&jrpc) - if err != nil { - http.Error(w, err.Error(), http.StatusUnauthorized) - return - } - next.ServeHTTP(w, r) - }) - } - - // SERVER: custom infura checks - - infuraMw := func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ucanCtx, ok := exectx.FromContext(r.Context()) - require.True(t, ok) - err := ucanCtx.VerifyInfura(func(ma datamodel.MapAssembler) { + err := ucanCtx.VerifyCustom("custom", func(ma datamodel.MapAssembler) { qp.MapEntry(ma, "ntwk", qp.String(network)) qp.MapEntry(ma, "quota", qp.Map(1, func(ma datamodel.MapAssembler) { qp.MapEntry(ma, "ur", qp.Int(1234)) @@ -183,7 +150,7 @@ func TestUcanCtxFullFlow(t *testing.T) { w.WriteHeader(http.StatusOK) } - sut := authMw(httpMw(jsonrpcMw(infuraMw(http.HandlerFunc(handler))))) + sut := authMw(httpMw(customArgsMw(http.HandlerFunc(handler)))) rec := httptest.NewRecorder() sut.ServeHTTP(rec, req) diff --git a/toolkit/server/extargs/Readme.md b/toolkit/server/extargs/Readme.md index ecaedf7..f44472f 100644 --- a/toolkit/server/extargs/Readme.md +++ b/toolkit/server/extargs/Readme.md @@ -8,31 +8,28 @@ In this package, we cross the chasm of the pure UCAN world into our practical ne ## Example -Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request (header and JSONRPC body): +Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request: ```json { "http": { "scheme": "https", "method": "POST", - "host": "mainnet.infura.io", + "host": "example.com", "path": "" }, - "jsonrpc": { - "jsonrpc": "2.0", - "method": "eth_blockbynumber", - "params": [], - "id": 1 + "custom": { + "foo": "bar" } } ``` Those `args` can be evaluated against a delegation's policy, for example: ```json { - "cmd": "/infura/jsonrpc", + "cmd": "/foo/bar", "pol": [ - ["==", ".http.host", "mainnet.infura.io"], - ["like", ".jsonrpc.method", "eth_*"] + ["==", ".http.host", "example.com"], + ["like", ".custom.foo", "ba*"] ] } ``` @@ -50,7 +47,7 @@ There is a way to get around that, and have the best of both worlds, but **it co ```json { "http": "zQmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D", - "jsonrpc": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9" + "custom": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9" } ``` @@ -63,4 +60,4 @@ Therefore, the server-side logic is made to have this hashing optional: - the client can opt out of passing that hash, and won't benefit from the enforced security The particular hash selected is SHA2-256 of the DAG-CBOR encoded argument, expressed in the form of a Multihash in raw bytes. -The arguments being hashed are the complete map of values, including the root key being replaced (for example `jsonrpc` or `http`). \ No newline at end of file +The arguments being hashed are the complete map of values, including the root key being replaced (for example `http` or `custom` here). \ No newline at end of file diff --git a/toolkit/server/extargs/infura.go b/toolkit/server/extargs/custom.go similarity index 57% rename from toolkit/server/extargs/infura.go rename to toolkit/server/extargs/custom.go index ff6c066..51accd1 100644 --- a/toolkit/server/extargs/infura.go +++ b/toolkit/server/extargs/custom.go @@ -8,13 +8,13 @@ import ( "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/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/policy" ) -const InfuraArgsKey = "inf" - -type InfuraExtArgs struct { +type CustomExtArgs struct { + key string pol policy.Policy originalArgs args.ReadOnly assembler func(ma datamodel.MapAssembler) @@ -24,44 +24,44 @@ type InfuraExtArgs struct { argsIpld ipld.Node } -func NewInfuraExtArgs(pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *InfuraExtArgs { - return &InfuraExtArgs{pol: pol, assembler: assembler} +func NewCustomExtArgs(key string, pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *CustomExtArgs { + return &CustomExtArgs{key: key, pol: pol, assembler: assembler} } -func (ia *InfuraExtArgs) Verify() error { - if err := ia.makeArgs(); err != nil { +func (cea *CustomExtArgs) Verify() error { + if err := cea.makeArgs(); err != nil { return err } - // Note: InfuraExtArgs doesn't support verifying a hash computed client-side like the other + // Note: CustomExtArgs doesn't support verifying a hash computed client-side like the other // external args, as the arguments are by nature dynamic. The client can't generate a meaningful hash. - ok, leaf := ia.pol.PartialMatch(ia.argsIpld) + ok, leaf := cea.pol.PartialMatch(cea.argsIpld) if !ok { return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) } return nil } -func (ia *InfuraExtArgs) Args() (*args.Args, error) { - if err := ia.makeArgs(); err != nil { +func (cea *CustomExtArgs) Args() (*args.Args, error) { + if err := cea.makeArgs(); err != nil { return nil, err } - return ia.args, nil + return cea.args, nil } -func (ia *InfuraExtArgs) makeArgs() error { +func (cea *CustomExtArgs) makeArgs() error { var outerErr error - ia.once.Do(func() { + cea.once.Do(func() { var err error - ia.args, err = makeInfuraArgs(ia.assembler) + cea.args, err = makeCustomArgs(cea.key, cea.assembler) if err != nil { outerErr = err return } - ia.argsIpld, err = ia.args.ToIPLD() + cea.argsIpld, err = cea.args.ToIPLD() if err != nil { outerErr = err return @@ -70,14 +70,14 @@ func (ia *InfuraExtArgs) makeArgs() error { return outerErr } -func makeInfuraArgs(assembler func(ma datamodel.MapAssembler)) (*args.Args, error) { +func makeCustomArgs(key string, assembler func(ma datamodel.MapAssembler)) (*args.Args, error) { n, err := qp.BuildMap(basicnode.Prototype.Any, -1, assembler) if err != nil { return nil, err } res := args.New() - err = res.Add(InfuraArgsKey, n) + err = res.Add(key, n) if err != nil { return nil, err } diff --git a/toolkit/server/extargs/infura_test.go b/toolkit/server/extargs/custom_test.go similarity index 76% rename from toolkit/server/extargs/infura_test.go rename to toolkit/server/extargs/custom_test.go index b8de085..86d53cd 100644 --- a/toolkit/server/extargs/infura_test.go +++ b/toolkit/server/extargs/custom_test.go @@ -7,18 +7,19 @@ import ( "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy/literal" ) -func ExampleInfuraExtArgs() { - // Note: this is an example for how to build arguments, but you likely want to use InfuraExtArgs +func ExampleCustomExtArgs() { + // Note: this is an example for how to build arguments, but you likely want to use CustomExtArgs // through UcanCtx. pol := policy.Policy{} // policies from the delegations // We will construct the following args: - // { + // "key": { // "ntwk":"eth-mainnet", // "quota":{ // "ur":1234, @@ -29,7 +30,7 @@ func ExampleInfuraExtArgs() { // "up":1234 // } // } - infArgs := NewInfuraExtArgs(pol, func(ma datamodel.MapAssembler) { + customArgs := NewCustomExtArgs("key", pol, func(ma datamodel.MapAssembler) { qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { qp.MapEntry(ma, "ur", qp.Int(1234)) @@ -41,14 +42,14 @@ func ExampleInfuraExtArgs() { })) }) - err := infArgs.Verify() + err := customArgs.Verify() fmt.Println(err) // Output: // } -func TestInfura(t *testing.T) { +func TestCustom(t *testing.T) { assembler := func(ma datamodel.MapAssembler) { qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet")) qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) { @@ -74,24 +75,24 @@ func TestInfura(t *testing.T) { { name: "matching args", pol: policy.MustConstruct( - policy.Equal(".inf.ntwk", literal.String("eth-mainnet")), - policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + policy.Equal(".key.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".key.quota.ur", literal.Int(1234)), ), expected: true, }, { name: "wrong network", pol: policy.MustConstruct( - policy.Equal(".inf.ntwk", literal.String("avalanche-fuji")), - policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)), + policy.Equal(".key.ntwk", literal.String("avalanche-fuji")), + policy.LessThanOrEqual(".key.quota.ur", literal.Int(1234)), ), expected: false, }, { name: "unrespected quota", pol: policy.MustConstruct( - policy.Equal(".inf.ntwk", literal.String("eth-mainnet")), - policy.LessThanOrEqual(".inf.quota.ur", literal.Int(100)), + policy.Equal(".key.ntwk", literal.String("eth-mainnet")), + policy.LessThanOrEqual(".key.quota.ur", literal.Int(100)), ), expected: false, }, @@ -99,7 +100,7 @@ func TestInfura(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - extArgs := NewInfuraExtArgs(tc.pol, assembler) + extArgs := NewCustomExtArgs("key", tc.pol, assembler) _, err := extArgs.Args() require.NoError(t, err) diff --git a/toolkit/server/extargs/http.go b/toolkit/server/extargs/http.go index e2062f6..7c354cd 100644 --- a/toolkit/server/extargs/http.go +++ b/toolkit/server/extargs/http.go @@ -13,6 +13,7 @@ import ( "github.com/ipld/go-ipld-prime/fluent/qp" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/multiformats/go-multihash" + "github.com/ucan-wg/go-ucan/pkg/args" "github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/token/invocation" @@ -88,7 +89,7 @@ func (hea *HttpExtArgs) verifyHash() error { mhBytes, err := n.AsBytes() if err != nil { - return fmt.Errorf("http args hash should be a string") + return fmt.Errorf("http args hash should be bytes") } data, err := ipld.Encode(hea.argsIpld, dagcbor.Encode) @@ -112,7 +113,7 @@ func (hea *HttpExtArgs) verifyHash() error { // If that hash is inserted at the HttpArgsKey key in the invocation arguments, // this increases the security as the UCAN token cannot be used with a different // HTTP request. -// For convenience, the hash is returned as a read to use invocation argument. +// For convenience, the hash is returned as a ready-to-use invocation argument. func MakeHttpHash(req *http.Request) (invocation.Option, error) { // Note: the hash is computed on the full IPLD args, including HttpArgsKey computedArgs, err := makeHttpArgs(req) diff --git a/toolkit/server/extargs/http_test.go b/toolkit/server/extargs/http_test.go index ebb20f7..3c1dbd6 100644 --- a/toolkit/server/extargs/http_test.go +++ b/toolkit/server/extargs/http_test.go @@ -5,9 +5,10 @@ import ( "net/http/httptest" "testing" + "github.com/MetaMask/go-did-it/didtest" "github.com/multiformats/go-multihash" "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/pkg/policy" @@ -200,3 +201,10 @@ func TestHttpHash(t *testing.T) { }) } } + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} diff --git a/toolkit/server/extargs/jsonrpc.go b/toolkit/server/extargs/jsonrpc.go deleted file mode 100644 index 51e911e..0000000 --- a/toolkit/server/extargs/jsonrpc.go +++ /dev/null @@ -1,171 +0,0 @@ -package extargs - -import ( - "bytes" - "encoding/json" - "fmt" - "sync" - - "github.com/INFURA/go-ethlibs/jsonrpc" - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec/dagcbor" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/fluent/qp" - "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/multiformats/go-multihash" - "github.com/ucan-wg/go-ucan/pkg/args" - "github.com/ucan-wg/go-ucan/pkg/policy" - "github.com/ucan-wg/go-ucan/pkg/policy/literal" - "github.com/ucan-wg/go-ucan/token/invocation" -) - -// JsonRpcArgsKey is the key in the args, used for: -// - if it exists in the invocation, holds a hash of the args derived from the JsonRpc request -// - in the final args to be evaluated against the policies, holds the args derived from the JsonRpc request -const JsonRpcArgsKey = "jsonrpc" - -type JsonRpcExtArgs struct { - pol policy.Policy - originalArgs args.ReadOnly - req *jsonrpc.Request - - once sync.Once - args *args.Args - argsIpld ipld.Node -} - -func NewJsonRpcExtArgs(pol policy.Policy, originalArgs args.ReadOnly, req *jsonrpc.Request) *JsonRpcExtArgs { - return &JsonRpcExtArgs{pol: pol, originalArgs: originalArgs, req: req} -} - -func (jrea *JsonRpcExtArgs) Verify() error { - if err := jrea.makeArgs(); err != nil { - return err - } - - if err := jrea.verifyHash(); err != nil { - return err - } - - ok, leaf := jrea.pol.PartialMatch(jrea.argsIpld) - if !ok { - return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String()) - } - return nil -} - -func (jrea *JsonRpcExtArgs) Args() (*args.Args, error) { - if err := jrea.makeArgs(); err != nil { - return nil, err - } - return jrea.args, nil -} - -func (jrea *JsonRpcExtArgs) makeArgs() error { - var outerErr error - jrea.once.Do(func() { - var err error - jrea.args, err = makeJsonRpcArgs(jrea.req) - if err != nil { - outerErr = err - return - } - - jrea.argsIpld, err = jrea.args.ToIPLD() - if err != nil { - outerErr = err - return - } - }) - return outerErr -} - -func (jrea *JsonRpcExtArgs) verifyHash() error { - n, err := jrea.originalArgs.GetNode(JsonRpcArgsKey) - if err != nil { - // no hash found, nothing to verify - return nil - } - - mhBytes, err := n.AsBytes() - if err != nil { - return fmt.Errorf("jsonrpc args hash should be a string") - } - - data, err := ipld.Encode(jrea.argsIpld, dagcbor.Encode) - if err != nil { - return fmt.Errorf("can't encode derived args in dag-cbor: %w", err) - } - - sum, err := multihash.Sum(data, multihash.SHA2_256, -1) - if err != nil { - return err - } - - if !bytes.Equal(mhBytes, sum) { - return fmt.Errorf("derived args from jsonrpc request don't match the expected hash") - } - - return nil -} - -// MakeJsonRpcHash compute the hash of the derived arguments from the JsonRPC request. -// If that hash is inserted at the JsonRpcArgsKey key in the invocation arguments, -// this increases the security as the UCAN token cannot be used with a different -// JsonRPC request. -// For convenience, the hash is returned as a read to use invocation argument. -func MakeJsonRpcHash(req *jsonrpc.Request) (invocation.Option, error) { - // Note: the hash is computed on the full IPLD args, including JsonRpcArgsKey - computedArgs, err := makeJsonRpcArgs(req) - if err != nil { - return nil, err - } - - n, err := computedArgs.ToIPLD() - if err != nil { - return nil, err - } - - data, err := ipld.Encode(n, dagcbor.Encode) - if err != nil { - return nil, err - } - - sum, err := multihash.Sum(data, multihash.SHA2_256, -1) - if err != nil { - return nil, err - } - - return invocation.WithArgument(JsonRpcArgsKey, []byte(sum)), nil -} - -func makeJsonRpcArgs(req *jsonrpc.Request) (*args.Args, error) { - deserialized := make([]any, len(req.Params)) - for i, param := range req.Params { - err := json.Unmarshal(param, &deserialized[i]) - if err != nil { - return nil, err - } - } - - params, err := literal.List(deserialized) - if err != nil { - return nil, err - } - - n, err := qp.BuildMap(basicnode.Prototype.Any, 3, func(ma datamodel.MapAssembler) { - qp.MapEntry(ma, "jsonrpc", qp.String(req.JSONRPC)) - qp.MapEntry(ma, "method", qp.String(req.Method)) - qp.MapEntry(ma, "params", qp.Node(params)) - }) - if err != nil { - return nil, err - } - - res := args.New() - err = res.Add(JsonRpcArgsKey, n) - if err != nil { - return nil, err - } - return res, nil -} diff --git a/toolkit/server/extargs/jsonrpc_test.go b/toolkit/server/extargs/jsonrpc_test.go deleted file mode 100644 index ceb715d..0000000 --- a/toolkit/server/extargs/jsonrpc_test.go +++ /dev/null @@ -1,188 +0,0 @@ -package extargs - -import ( - "testing" - - "github.com/INFURA/go-ethlibs/jsonrpc" - "github.com/multiformats/go-multihash" - "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/pkg/policy" - "github.com/ucan-wg/go-ucan/pkg/policy/literal" - "github.com/ucan-wg/go-ucan/token/invocation" -) - -func TestJsonRpc(t *testing.T) { - tests := []struct { - name string - req *jsonrpc.Request - pol policy.Policy - expected bool - }{ - { - name: "or on method, not matching", - req: jsonrpc.MustRequest(1839673506133526, "eth_getBlockByNumber", - "0x599784", true, - ), - pol: policy.MustConstruct( - policy.Or( - policy.Equal(".jsonrpc.method", literal.String("eth_getCode")), - policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")), - policy.Equal(".jsonrpc.method", literal.String("eth_call")), - policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")), - ), - ), - expected: false, - }, - { - name: "or on method, matching", - req: jsonrpc.MustRequest(1839673506133526, "eth_call", - map[string]string{"to": "0xBADBADBADBADBADBADBADBADBADBADBADBADBAD1"}, - ), - pol: policy.MustConstruct( - policy.Or( - policy.Equal(".jsonrpc.method", literal.String("eth_getCode")), - policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")), - policy.Equal(".jsonrpc.method", literal.String("eth_call")), - policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")), - ), - ), - expected: true, - }, - { - name: "complex, optional parameter, matching", - req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall", - true, false, 1234, "callTracer", - ), - pol: policy.MustConstruct( - policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), - policy.Or( - policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")), - policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")), - ), - ), - expected: true, - }, - { - name: "complex, optional parameter, missing parameter", - req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall", - true, false, 1234, - ), - pol: policy.MustConstruct( - policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), - policy.Or( - policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")), - policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")), - ), - ), - expected: true, - }, - { - name: "complex, parameter not matching", - req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall", - true, false, 1234, "ho_no", - ), - pol: policy.MustConstruct( - policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), - policy.Or( - policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")), - policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")), - ), - ), - expected: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - // we don't test the args hash here - emptyArgs := args.New().ReadOnly() - - ctx := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req) - - _, err := ctx.Args() - require.NoError(t, err) - - if tc.expected { - require.NoError(t, ctx.Verify()) - } else { - require.Error(t, ctx.Verify()) - } - }) - } -} - -func TestJsonRpcHash(t *testing.T) { - servicePersona := didtest.PersonaAlice - clientPersona := didtest.PersonaBob - - req := jsonrpc.MustRequest(1839673506133526, "debug_traceCall", - true, false, 1234, "ho_no", - ) - pol := policy.MustConstruct( - policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")), - ) - - makeArg := func(data []byte, code uint64) invocation.Option { - mh, err := multihash.Sum(data, code, -1) - require.NoError(t, err) - return invocation.WithArgument(JsonRpcArgsKey, []byte(mh)) - } - - tests := []struct { - name string - argOptions []invocation.Option - expected bool - }{ - { - name: "correct hash", - argOptions: []invocation.Option{must(MakeJsonRpcHash(req))}, - expected: true, - }, - { - name: "non-matching hash", - argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.SHA2_256)}, - expected: false, - }, - { - name: "wrong type of hash", - argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.BLAKE3)}, - expected: false, - }, - { - name: "no hash", - argOptions: nil, - expected: true, // having a hash is not enforced - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - inv, err := invocation.New( - clientPersona.DID(), - command.MustParse("/foo"), - servicePersona.DID(), - nil, - tc.argOptions..., // inject hash argument, if any - ) - require.NoError(t, err) - - ctx := NewJsonRpcExtArgs(pol, inv.Arguments(), req) - - if tc.expected { - require.NoError(t, ctx.Verify()) - } else { - require.Error(t, ctx.Verify()) - } - }) - } -} - -func must[T any](t T, err error) T { - if err != nil { - panic(err) - } - return t -}