From f85ece49fae299bfbe8a5d40345818717b845580 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Mon, 16 Sep 2024 17:18:16 -0400 Subject: [PATCH] feat(delegation): add validation/accessors --- delegation/delegatiom_options.go | 30 ------- delegation/delegation.go | 141 +++++++++++++++++++++++------ delegation/delegation_test.go | 149 +++++++++++++++++++++++++++++++ delegation/testdata/new.dagjson | 1 + delegation/testdata/root.dagjson | 1 + 5 files changed, 264 insertions(+), 58 deletions(-) create mode 100644 delegation/delegation_test.go create mode 100644 delegation/testdata/new.dagjson create mode 100644 delegation/testdata/root.dagjson diff --git a/delegation/delegatiom_options.go b/delegation/delegatiom_options.go index e3014cd..982e4c5 100644 --- a/delegation/delegatiom_options.go +++ b/delegation/delegatiom_options.go @@ -4,7 +4,6 @@ package delegation import ( "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ucan-wg/go-ucan/did" "time" ) @@ -25,13 +24,6 @@ func applyConfigOptions(c *config, options ...Option) error { return nil } -func WithExpiration(o *time.Time) Option { - return func(c *config) error { - c.Expiration = o - return nil - } -} - func WithMeta(o map[string]datamodel.Node) Option { return func(c *config) error { c.Meta = o @@ -39,31 +31,9 @@ func WithMeta(o map[string]datamodel.Node) Option { } } -func WithNoExpiration(o bool) Option { - return func(c *config) error { - c.NoExpiration = o - return nil - } -} - func WithNotBefore(o *time.Time) Option { return func(c *config) error { c.NotBefore = o return nil } } - -// WithSubject is a did.DID representing the Subject. -func WithSubject(o *did.DID) Option { - return func(c *config) error { - c.Subject = o - return nil - } -} - -func WithPowerline(o bool) Option { - return func(c *config) error { - c.Powerline = o - return nil - } -} diff --git a/delegation/delegation.go b/delegation/delegation.go index cc7ced6..7fbe1bd 100644 --- a/delegation/delegation.go +++ b/delegation/delegation.go @@ -1,6 +1,7 @@ package delegation import ( + "crypto/rand" "errors" "fmt" "time" @@ -23,30 +24,22 @@ type Delegation struct { } //go:generate -command options go run github.com/selesy/go-options -//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -stringer=false -imports=time,github.com/ucan-wg/go-ucan/did,github.com/ipld/go-ipld-prime/datamodel +//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -stringer=false -imports=time,github.com/ipld/go-ipld-prime/datamodel type config struct { - Expiration *time.Time - Meta map[string]datamodel.Node - NoExpiration bool - NotBefore *time.Time - // is a did.DID representing the Subject. - Subject *did.DID - Powerline bool + Meta map[string]datamodel.Node + NotBefore *time.Time } -// Required fields for delegation - -// Requirements for root - -func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, pol *policy.Policy, exp *time.Time, nonce []byte, opts ...Option) (*Delegation, error) { +func New(privKey crypto.PrivKey, aud did.DID, sub *did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, exp *time.Time, opts ...Option) (*Delegation, error) { cfg, err := newConfig(opts...) if err != nil { return nil, err } - if !iss.Defined() { - return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "iss") + issuer, err := did.FromPrivKey(privKey) + if err != nil { + return nil, err } if !aud.Defined() { @@ -55,8 +48,8 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, audience := aud.String() var subject *string - if cfg.Subject != nil && cfg.Subject.Defined() { - s := cfg.Subject.String() + if sub != nil { + s := sub.String() subject = &s } @@ -65,7 +58,11 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, return nil, err } - nonce = []uint8(nonce) + var meta *token.Map__String__Any + if len(cfg.Meta) > 0 { + m := token.ToIPLDMapStringAny(cfg.Meta) + meta = &m + } var notBefore *int if cfg.NotBefore != nil { @@ -73,20 +70,14 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, notBefore = &n } - var meta *token.Map__String__Any - if len(cfg.Meta) > 0 { - m := token.ToIPLDMapStringAny(cfg.Meta) - meta = &m - } - var expiration *int - if exp != nil && !cfg.NoExpiration { - e := int(cfg.NotBefore.Unix()) + if exp != nil { + e := int(exp.Unix()) expiration = &e } tkn := &token.Token{ - Issuer: iss.String(), + Issuer: issuer.String(), Audience: &audience, Subject: subject, Command: cmd.String(), @@ -111,16 +102,82 @@ func New(privKey crypto.PrivKey, iss did.DID, aud did.DID, cmd *command.Command, return dlg, nil } -type validateFunc func() error +func Root(privKey crypto.PrivKey, aud did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, exp *time.Time, opts ...Option) (*Delegation, error) { + sub, err := did.FromPrivKey(privKey) + if err != nil { + return nil, err + } + + return New(privKey, aud, &sub, cmd, pol, nonce, exp, opts...) +} + +func (d *Delegation) Audience() did.DID { + id, _ := did.Parse(*d.envel.TokenPayload().Audience) + + return id +} + +func (d *Delegation) Command() *command.Command { + cmd, _ := command.Parse(d.envel.TokenPayload().Command) + + return cmd +} + +func (d *Delegation) IsPowerline() bool { + return d.envel.TokenPayload().Subject == nil +} + +func (d *Delegation) IsRoot() bool { + return &d.envel.TokenPayload().Issuer == d.envel.TokenPayload().Subject +} + +func (d *Delegation) Issuer() did.DID { + id, _ := did.Parse(d.envel.TokenPayload().Issuer) + + return id +} + +func (d *Delegation) Meta() map[string]datamodel.Node { + return d.envel.TokenPayload().Meta.Values +} + +func (d *Delegation) Nonce() []byte { + return *d.envel.TokenPayload().Nonce +} + +func (d *Delegation) Policy() policy.Policy { + pol, _ := policy.FromIPLD(*d.envel.TokenPayload().Policy) + + return pol +} + +func (d *Delegation) Subject() *did.DID { + if d.envel.TokenPayload().Subject == nil { + return nil + } + + id, _ := did.Parse(*d.envel.TokenPayload().Subject) + + return &id +} func (d *Delegation) Validate() error { return errors.Join( d.validateDID("iss", &d.envel.TokenPayload().Issuer, false), d.validateDID("aud", d.envel.TokenPayload().Audience, false), d.validateDID("sub", d.envel.TokenPayload().Subject, true), + d.validateCommand(), + d.validatePolicy(), + d.validateNonce(), ) } +func (d *Delegation) validateCommand() error { + _, err := command.Parse(d.envel.TokenPayload().Command) + + return err +} + func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrOptional bool) error { if identity == nil && !nullableOrOptional { return fmt.Errorf("a required DID is missing: %s", fieldName) @@ -137,3 +194,31 @@ func (d *Delegation) validateDID(fieldName string, identity *string, nullableOrO return nil } + +func (d *Delegation) validateNonce() error { + if d.envel.TokenPayload().Nonce == nil || len(*d.envel.TokenPayload().Nonce) < 1 { + return fmt.Errorf("nonce is required: must not be nil or empty") + } + + return nil +} + +func (d *Delegation) validatePolicy() error { + if d.envel.TokenPayload().Policy == nil { + return fmt.Errorf("the \"pol\" field is required") + } + + _, err := policy.FromIPLD(*d.envel.TokenPayload().Policy) + + return err +} + +func Nonce() ([]byte, error) { + nonce := make([]byte, 32) + + if _, err := rand.Read(nonce); err != nil { + return nil, err + } + + return nonce, nil +} diff --git a/delegation/delegation_test.go b/delegation/delegation_test.go new file mode 100644 index 0000000..5101ef7 --- /dev/null +++ b/delegation/delegation_test.go @@ -0,0 +1,149 @@ +package delegation_test + +import ( + "crypto/rand" + "testing" + "time" + + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/capability/command" + "github.com/ucan-wg/go-ucan/capability/policy" + "github.com/ucan-wg/go-ucan/delegation" + "github.com/ucan-wg/go-ucan/did" + "gotest.tools/v3/golden" +) + +const ( + nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0" + + AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM=" + AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv" + + issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU=" + issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2" + + subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c=" + subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2" + subJectCmd = "/foo/bar" + subjectPol = ` +[ + [ + "==", + ".status", + "draft" + ], + [ + "all", + ".reviewer", + [ + "like", + ".email", + "*@example.com" + ] + ], + [ + "any", + ".tags", + [ + "or", + [ + [ + "==", + ".", + "news" + ], + [ + "==", + ".", + "press" + ] + ] + ] + ] +] +` +) + +func TestConstructors(t *testing.T) { + t.Parallel() + + privKey := privKey(t, issuerPrivKeyCfg) + + aud, err := did.Parse(AudienceDID) + + sub, err := did.Parse(subjectDID) + require.NoError(t, err) + + cmd, err := command.Parse(subJectCmd) + require.NoError(t, err) + + pol, err := policy.FromDagJson(subjectPol) + require.NoError(t, err) + + exp := time.Time{} + + meta := map[string]datamodel.Node{ + "foo": basicnode.NewString("fooo"), + "bar": basicnode.NewString("barr"), + } + + t.Run("New", func(t *testing.T) { + dlg, err := delegation.New(privKey, aud, &sub, cmd, pol, []byte(nonce), &exp, delegation.WithMeta(meta)) + require.NoError(t, err) + + data, err := dlg.ToDagJson() + require.NoError(t, err) + + t.Log(string(data)) + + golden.Assert(t, string(data), "new.dagjson") + }) + + t.Run("Root", func(t *testing.T) { + t.Parallel() + + dlg, err := delegation.Root(privKey, aud, cmd, pol, []byte(nonce), &exp, delegation.WithMeta(meta)) + require.NoError(t, err) + + data, err := dlg.ToDagJson() + require.NoError(t, err) + + t.Log(string(data)) + + golden.Assert(t, string(data), "root.dagjson") + }) +} + +func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey { + t.Helper() + + privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) + require.NoError(t, err) + + privKey, err := crypto.UnmarshalPrivateKey(privKeyMar) + require.NoError(t, err) + + return privKey +} + +func TestKey(t *testing.T) { + t.Skip() + + priv, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + + privMar, err := crypto.MarshalPrivateKey(priv) + require.NoError(t, err) + + privCfg := crypto.ConfigEncodeKey(privMar) + t.Log(privCfg) + + id, err := did.FromPubKey(priv.GetPublic()) + require.NoError(t, err) + t.Log(id) + + t.Fail() +} diff --git a/delegation/testdata/new.dagjson b/delegation/testdata/new.dagjson new file mode 100644 index 0000000..9d06287 --- /dev/null +++ b/delegation/testdata/new.dagjson @@ -0,0 +1 @@ +[{"/":{"bytes":"P2lPLfdMuZuc4NPZ0mbozU+/bn5xoWlJsu+Fvaxi4ICYXVJb9/wiTTht3WJEFqjxXLxfTl4BMZF3J1CNvMPqBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","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:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"}}] \ No newline at end of file diff --git a/delegation/testdata/root.dagjson b/delegation/testdata/root.dagjson new file mode 100644 index 0000000..66c6e4c --- /dev/null +++ b/delegation/testdata/root.dagjson @@ -0,0 +1 @@ +[{"/":{"bytes":"0sjiwG9BOgpezz6qw5UiD+rqOeqFLn4+Qds1PvbnsUBoc3RhF6IVxIeoOXDh1ufv3RHaI/zg4wjYpUwAMpTACw"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":-62135596800,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","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:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"}}] \ No newline at end of file