Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

invocation: rephrase slightly the proof rules to be less confusing down the line #93

Merged
merged 7 commits into from
Dec 10, 2024
3 changes: 3 additions & 0 deletions did/did.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ func (d DID) PubKey() (crypto.PubKey, error) {

// String formats the decentralized identity document (DID) as a string.
func (d DID) String() string {
if d == Undef {
return "(undefined)"
}
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
return "did:key:" + key
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/container/serial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
66 changes: 52 additions & 14 deletions token/delegation/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ package delegation
// TODO: change the "delegation" link above when the specification is merged

import (
"encoding/base64"
"errors"
"fmt"
"strings"
"time"

"github.com/ucan-wg/go-ucan/did"
Expand Down Expand Up @@ -44,16 +46,15 @@ type Token struct {
expiration *time.Time
}

// New creates a validated Token from the provided parameters and options.
// New creates a validated delegation Token from the provided parameters and options.
// This is typically used to delegate a given power to another agent.
//
// When creating a delegated token, the Issuer's (iss) DID is assembled
// using the public key associated with the private key sent as the first
// parameter.
func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad you included these - I was thinking overnight that we could also state these in "user story" form. Perhaps something like "As an Issuer, I want to allow my Audience to Invoke Command+Policy on Subject". I tried to be consistent with https://github.com/ucan-wg/spec?tab=readme-ov-file#roles.

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(),
Expand Down Expand Up @@ -81,16 +82,27 @@ func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Optio
return tkn, nil
}

// Root creates a validated UCAN delegation Token from the provided
// parameters and options.
// Root creates a validated UCAN delegation Token from the provided parameters and options.
// This is typically used to create and give a power to an agent.
//
// When creating a root token, both the Issuer's (iss) and Subject's
// (sub) DIDs are assembled from the public key associated with the
// private key passed as the first argument.
func Root(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
opts = append(opts, WithSubject(iss))
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
return New(iss, aud, cmd, pol, iss, opts...)
}

return New(iss, aud, cmd, pol, opts...)
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
//
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
// This is a very powerful pattern, use it only if you understand it.
// Powerline delegations MUST NOT be used as the root delegation to a resource
//
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
//
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
return New(iss, aud, cmd, pol, did.Undef, opts...)
}

// Issuer returns the did.DID representing the Token's issuer.
Expand Down Expand Up @@ -160,6 +172,32 @@ func (t *Token) IsValidAt(ti time.Time) bool {
return true
}

func (t *Token) String() string {
var res strings.Builder

var kind string
switch {
case t.issuer == t.subject:
kind = " (root delegation)"
case t.subject == did.Undef:
kind = " (powerline delegation)"
default:
kind = " (normal delegation)"
}

res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
res.WriteString(fmt.Sprintf("Subject: %s%s\n", t.Subject(), kind))
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
res.WriteString(fmt.Sprintf("Policy: %s\n", t.Policy()))
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
res.WriteString(fmt.Sprintf("NotBefore: %v\n", t.NotBefore()))
res.WriteString(fmt.Sprintf("Expiration: %v", t.Expiration()))

return res.String()
}

func (t *Token) validate() error {
var errs error

Expand Down
27 changes: 20 additions & 7 deletions token/delegation/delegation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ const (
]
`

newCID = "zdpuAwa4qv3ncMDPeDoqVxjZy3JoyWsbqUzm94rdA1AvRFkkw"
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"

aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
)

Expand All @@ -75,9 +72,8 @@ func TestConstructors(t *testing.T) {
require.NoError(t, err)

t.Run("New", func(t *testing.T) {
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, didtest.PersonaCarol.DID(),
delegation.WithNonce([]byte(nonce)),
delegation.WithSubject(didtest.PersonaAlice.DID()),
delegation.WithExpiration(exp),
delegation.WithMeta("foo", "fooo"),
delegation.WithMeta("bar", "barr"),
Expand Down Expand Up @@ -106,6 +102,23 @@ func TestConstructors(t *testing.T) {

golden.Assert(t, string(data), "root.dagjson")
})

t.Run("Powerline", func(t *testing.T) {
t.Parallel()

tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
delegation.WithNonce([]byte(nonce)),
delegation.WithExpiration(exp),
delegation.WithMeta("foo", "fooo"),
delegation.WithMeta("bar", "barr"),
)
require.NoError(t, err)

data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
require.NoError(t, err)

golden.Assert(t, string(data), "powerline.dagjson")
})
}

func TestEncryptedMeta(t *testing.T) {
Expand Down Expand Up @@ -153,7 +166,7 @@ func TestEncryptedMeta(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
)
require.NoError(t, err)
Expand Down Expand Up @@ -191,7 +204,7 @@ func TestEncryptedMeta(t *testing.T) {
}

// Create token with multiple encrypted values
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
require.NoError(t, err)

data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 5 additions & 4 deletions token/delegation/delegationtest/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ const (
var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}

type newDelegationParams struct {
privKey crypto.PrivKey
privKey crypto.PrivKey // iss
aud did.DID
cmd command.Command
pol policy.Policy
sub did.DID
opts []delegation.Option
}

Expand Down Expand Up @@ -89,8 +90,8 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
aud: personas[1].DID(),
cmd: delegationtest.NominalCommand,
pol: policytest.EmptyPolicy,
sub: didtest.PersonaAlice.DID(),
opts: []delegation.Option{
delegation.WithSubject(didtest.PersonaAlice.DID()),
delegation.WithNonce(constantNonce),
},
}
Expand All @@ -117,7 +118,7 @@ func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari vari
p.cmd = delegationtest.AttenuatedCommand
}},
{name: "InvalidSubject", variant: func(p *newDelegationParams) {
p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID()))
p.sub = didtest.PersonaBob.DID()
}},
{name: "InvalidExpired", variant: func(p *newDelegationParams) {
// Note: this makes the generator not deterministic
Expand Down Expand Up @@ -161,7 +162,7 @@ func (g *generator) createDelegation(params newDelegationParams, name string, va
return cid.Undef, err
}

tkn, err := delegation.New(issDID, params.aud, params.cmd, params.pol, params.opts...)
tkn, err := delegation.New(issDID, params.aud, params.cmd, params.pol, params.sub, params.opts...)
if err != nil {
return cid.Undef, err
}
Expand Down
6 changes: 3 additions & 3 deletions token/delegation/delegationtest/token_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions token/delegation/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
16 changes: 0 additions & 16 deletions token/delegation/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 8 additions & 2 deletions token/delegation/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ func TestSchemaRoundTrip(t *testing.T) {
p1, err := delegation.FromDagJson(delegationJson)
require.NoError(t, err)

_, newCID, err := p1.ToSealed(privKey)
require.NoError(t, err)

cborBytes, id, err := p1.ToSealed(privKey)
require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
assert.Equal(t, envelope.CIDToBase58BTC(newCID), envelope.CIDToBase58BTC(id))

p2, c2, err := delegation.FromSealed(cborBytes)
require.NoError(t, err)
Expand All @@ -58,10 +61,13 @@ func TestSchemaRoundTrip(t *testing.T) {
p1, err := delegation.FromDagJsonReader(buf)
require.NoError(t, err)

_, newCID, err := p1.ToSealed(privKey)
require.NoError(t, err)

cborBytes := &bytes.Buffer{}
id, err := p1.ToSealedWriter(cborBytes, privKey)
require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
assert.Equal(t, envelope.CIDToBase58BTC(newCID), envelope.CIDToBase58BTC(id))

// buf = bytes.NewBuffer(cborBytes.Bytes())
p2, c2, err := delegation.FromSealedReader(cborBytes)
Expand Down
2 changes: 1 addition & 1 deletion token/delegation/testdata/new.dagjson
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"/":{"bytes":"BBabgnWqd+cjwG1td0w9BudNocmUwoR89RMZTqZHk3osCXEI/bOkko0zTvlusaE4EMBBeSzZDKzjvunLBfdiBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/[email protected]":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"}}]
[{"/":{"bytes":"YJsl8EMLnXSFE/nKKjMxz9bHHo+Y7QeLEzukEzW1TB+m53TTiY1aOt+qUO8JaTcOKsOHt/a4Vn+YiOd5CkLdAQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/[email protected]":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkgupchh5HwuHahS7YsyE8bLua1Mr8p2iKNRhyvSvRAs9n"}}]
1 change: 1 addition & 0 deletions token/delegation/testdata/powerline.dagjson
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"/":{"bytes":"i3YkPDvNSU4V8XYEluZhLH0b+NDcW/6+PtPSUHC17cmXXqgelG0K4EzWQQkS9UsYCHfkZSCn9NjGSXYMMFhaAQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/[email protected]":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]]}}]
2 changes: 1 addition & 1 deletion token/invocation/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down
24 changes: 23 additions & 1 deletion token/invocation/invocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
package invocation

import (
"encoding/base64"
"errors"
"fmt"
"strings"
"time"

"github.com/ipfs/go-cid"
Expand Down Expand Up @@ -67,7 +69,9 @@ type Token struct {
//
// With the exception of the WithMeta option, all others will overwrite
// the previous contents of their target field.
func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) {
//
// You can read it as "(Issuer - I) executes (command) on (subject)".
func New(iss did.DID, cmd command.Command, sub did.DID, prf []cid.Cid, opts ...Option) (*Token, error) {
iat := time.Now()

tkn := Token{
Expand Down Expand Up @@ -212,6 +216,24 @@ func (t *Token) IsValidAt(ti time.Time) bool {
return true
}

func (t *Token) String() string {
var res strings.Builder

res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
res.WriteString(fmt.Sprintf("Subject: %v\n", t.Subject()))
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
res.WriteString(fmt.Sprintf("Args: %s\n", t.Arguments()))
res.WriteString(fmt.Sprintf("Proof: %v\n", t.Proof()))
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
res.WriteString(fmt.Sprintf("Expiration: %v\n", t.Expiration()))
res.WriteString(fmt.Sprintf("Invoked At: %v\n", t.InvokedAt()))
res.WriteString(fmt.Sprintf("Cause: %v", t.Cause()))

return res.String()
}

func (t *Token) validate() error {
var errs error

Expand Down
Loading
Loading