From 0592717637e34fbedd57887c15fe556d590ea481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 9 Dec 2024 20:39:47 +0100 Subject: [PATCH] (WIP) refine the token constructors: - for invocation, reorder the parameters for a more "natural language" mental model - for delegation, make "subject" a required parameter to avoid make powerline by mistake - for delegation, implement powerline --- pkg/container/serial_test.go | 3 +-- token/delegation/delegation.go | 38 ++++++++++++++++++----------- token/delegation/delegation_test.go | 24 +++++++++++++++--- token/delegation/examples_test.go | 3 +-- token/delegation/options.go | 16 ------------ token/invocation/examples_test.go | 2 +- token/invocation/invocation.go | 4 ++- token/invocation/invocation_test.go | 4 +-- token/invocation/ipld_test.go | 2 +- token/invocation/options.go | 6 ++++- 10 files changed, 58 insertions(+), 44 deletions(-) diff --git a/pkg/container/serial_test.go b/pkg/container/serial_test.go index be828fb..3894e7a 100644 --- a/pkg/container/serial_test.go +++ b/pkg/container/serial_test.go @@ -160,13 +160,12 @@ func randToken() (*delegation.Token, cid.Cid, []byte) { opts := []delegation.Option{ delegation.WithExpiration(time.Now().Add(time.Hour)), - delegation.WithSubject(iss), } for i := 0; i < 3; i++ { opts = append(opts, delegation.WithMeta(randomString(8), randomString(10))) } - t, err := delegation.New(iss, aud, cmd, pol, opts...) + t, err := delegation.Root(iss, aud, cmd, pol, opts...) if err != nil { panic(err) } diff --git a/token/delegation/delegation.go b/token/delegation/delegation.go index 1d6208a..4955e48 100644 --- a/token/delegation/delegation.go +++ b/token/delegation/delegation.go @@ -44,16 +44,15 @@ type Token struct { expiration *time.Time } -// New creates a validated Token from the provided parameters and options. +// New creates a validated delegation Token from the provided parameters and options. +// This is typically used to delegate a given power to another agent. // -// When creating a delegated token, the Issuer's (iss) DID is assembled -// using the public key associated with the private key sent as the first -// parameter. -func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { +// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)". +func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) { tkn := &Token{ issuer: iss, audience: aud, - subject: did.Undef, + subject: sub, command: cmd, policy: pol, meta: meta.NewMeta(), @@ -81,16 +80,27 @@ func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Optio return tkn, nil } -// Root creates a validated UCAN delegation Token from the provided -// parameters and options. +// Root creates a validated UCAN delegation Token from the provided parameters and options. +// This is typically used to create and give a power to an agent. // -// When creating a root token, both the Issuer's (iss) and Subject's -// (sub) DIDs are assembled from the public key associated with the -// private key passed as the first argument. -func Root(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { - opts = append(opts, WithSubject(iss)) +// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself". +func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { + return New(iss, aud, cmd, pol, iss, opts...) +} - return New(iss, aud, cmd, pol, opts...) +// Powerline creates a validated UCAN delegation Token from the provided parameters and options. +// +// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject. +// This is a very powerful pattern, use it only if you understand it. +// Powerline delegations MUST NOT be used as the root delegation to a resource +// +// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with +// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys +// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked. +// +// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything". +func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { + return New(iss, aud, cmd, pol, did.Undef, opts...) } // Issuer returns the did.DID representing the Token's issuer. diff --git a/token/delegation/delegation_test.go b/token/delegation/delegation_test.go index 8da08b4..5c80a2d 100644 --- a/token/delegation/delegation_test.go +++ b/token/delegation/delegation_test.go @@ -75,9 +75,8 @@ func TestConstructors(t *testing.T) { require.NoError(t, err) t.Run("New", func(t *testing.T) { - tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, + tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, didtest.PersonaAlice.DID(), delegation.WithNonce([]byte(nonce)), - delegation.WithSubject(didtest.PersonaAlice.DID()), delegation.WithExpiration(exp), delegation.WithMeta("foo", "fooo"), delegation.WithMeta("bar", "barr"), @@ -106,6 +105,23 @@ func TestConstructors(t *testing.T) { golden.Assert(t, string(data), "root.dagjson") }) + + t.Run("Powerline", func(t *testing.T) { + t.Parallel() + + tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, + delegation.WithNonce([]byte(nonce)), + delegation.WithExpiration(exp), + delegation.WithMeta("foo", "fooo"), + delegation.WithMeta("bar", "barr"), + ) + require.NoError(t, err) + + data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey()) + require.NoError(t, err) + + golden.Assert(t, string(data), "powerline.dagjson") + }) } func TestEncryptedMeta(t *testing.T) { @@ -153,7 +169,7 @@ func TestEncryptedMeta(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, + tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey), ) require.NoError(t, err) @@ -191,7 +207,7 @@ func TestEncryptedMeta(t *testing.T) { } // Create token with multiple encrypted values - tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...) + tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...) require.NoError(t, err) data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey()) diff --git a/token/delegation/examples_test.go b/token/delegation/examples_test.go index 1ea6f55..9848ed9 100644 --- a/token/delegation/examples_test.go +++ b/token/delegation/examples_test.go @@ -41,8 +41,7 @@ func ExampleNew() { )), ) - tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol, - delegation.WithSubject(didtest.PersonaAlice.DID()), + tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol, didtest.PersonaAlice.DID(), delegation.WithExpirationIn(time.Hour), delegation.WithNotBeforeIn(time.Minute), delegation.WithMeta("foo", "bar"), diff --git a/token/delegation/options.go b/token/delegation/options.go index 3348760..bd26744 100644 --- a/token/delegation/options.go +++ b/token/delegation/options.go @@ -3,8 +3,6 @@ package delegation import ( "fmt" "time" - - "github.com/ucan-wg/go-ucan/did" ) // Option is a type that allows optional fields to be set during the @@ -85,20 +83,6 @@ func WithNotBeforeIn(nbf time.Duration) Option { } } -// WithSubject sets the Tokens's optional "subject" field to the value of -// provided did.DID. -// -// This Option should only be used with the New constructor - since -// Subject is a required parameter when creating a Token via the Root -// constructor, any value provided via this Option will be silently -// overwritten. -func WithSubject(sub did.DID) Option { - return func(t *Token) error { - t.subject = sub - return nil - } -} - // WithNonce sets the Token's nonce with the given value. // If this option is not used, a random 12-byte nonce is generated for this required field. func WithNonce(nonce []byte) Option { diff --git a/token/invocation/examples_test.go b/token/invocation/examples_test.go index d3255d0..e505a7d 100644 --- a/token/invocation/examples_test.go +++ b/token/invocation/examples_test.go @@ -27,7 +27,7 @@ func ExampleNew() { return } - inv, err := invocation.New(iss, sub, cmd, prf, + inv, err := invocation.New(iss, cmd, sub, prf, invocation.WithArgument("uri", args["uri"]), invocation.WithArgument("headers", args["headers"]), invocation.WithArgument("payload", args["payload"]), diff --git a/token/invocation/invocation.go b/token/invocation/invocation.go index 577f4d8..20c0a47 100644 --- a/token/invocation/invocation.go +++ b/token/invocation/invocation.go @@ -67,7 +67,9 @@ type Token struct { // // With the exception of the WithMeta option, all others will overwrite // the previous contents of their target field. -func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) { +// +// You can read it as "(Issuer - I) executes (command) on (subject)". +func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...Option) (*Token, error) { iat := time.Now() tkn := Token{ diff --git a/token/invocation/invocation_test.go b/token/invocation/invocation_test.go index a8d2455..16ec980 100644 --- a/token/invocation/invocation_test.go +++ b/token/invocation/invocation_test.go @@ -129,7 +129,7 @@ func TestToken_ExecutionAllowed(t *testing.T) { testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject) }) - t.Run("passes - arguments satify example policy", func(t *testing.T) { + t.Run("passes - arguments satisfy example policy", func(t *testing.T) { t.Parallel() testFails(t, invocation.ErrPolicyNotSatisfied, didtest.PersonaFrank, delegationtest.NominalCommand, policytest.SpecInvalidArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidExamplePolicy) @@ -142,7 +142,7 @@ func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args opts = append(opts, invocation.WithArguments(args)) - tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...) + tkn, err := invocation.New(persona.DID(), cmd, didtest.PersonaAlice.DID(), prf, opts...) require.NoError(t, err) return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader()) diff --git a/token/invocation/ipld_test.go b/token/invocation/ipld_test.go index 9754c16..4e822e3 100644 --- a/token/invocation/ipld_test.go +++ b/token/invocation/ipld_test.go @@ -16,7 +16,7 @@ func TestSealUnsealRoundtrip(t *testing.T) { privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew() require.NoError(t, err) - tkn1, err := invocation.New(iss, sub, cmd, prf, + tkn1, err := invocation.New(iss, cmd, sub, prf, invocation.WithArgument("uri", args["uri"]), invocation.WithArgument("headers", args["headers"]), invocation.WithArgument("payload", args["payload"]), diff --git a/token/invocation/options.go b/token/invocation/options.go index 1715383..dcf7fc8 100644 --- a/token/invocation/options.go +++ b/token/invocation/options.go @@ -38,11 +38,15 @@ func WithArguments(args *args.Args) Option { // WithAudience sets the Token's audience to the provided did.DID. // +// This can be used if the resource on which the token operates on is different +// from the subject. In that situation, the subject is akin to the "service" and +// the audience is akin to the resource. +// // If the provided did.DID is the same as the Token's subject, the // audience is not set. func WithAudience(aud did.DID) Option { return func(t *Token) error { - if t.subject.String() != aud.String() { + if t.subject != aud { t.audience = aud }