diff --git a/delegation/delegatiom_options.go b/delegation/delegatiom_options.go index c3aebad..e3014cd 100644 --- a/delegation/delegatiom_options.go +++ b/delegation/delegatiom_options.go @@ -1,19 +1,14 @@ package delegation -// Code generated by github.com/launchdarkly/go-options. DO NOT EDIT. - -import "fmt" +// Code generated by github.com/selesy/go-options. DO NOT EDIT. import ( + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ucan-wg/go-ucan/did" "time" ) -type ApplyOptionFunc func(c *config) error - -func (f ApplyOptionFunc) apply(c *config) error { - return f(c) -} +type Option func(c *config) error func newConfig(options ...Option) (config, error) { var c config @@ -23,152 +18,52 @@ func newConfig(options ...Option) (config, error) { func applyConfigOptions(c *config, options ...Option) error { for _, o := range options { - if err := o.apply(c); err != nil { + if err := o(c); err != nil { return err } } return nil } -type Option interface { - apply(*config) error -} - -type withExpirationImpl struct { - o *time.Time -} - -func (o withExpirationImpl) apply(c *config) error { - c.Expiration = o.o - return nil -} - -func (o withExpirationImpl) String() string { - name := "WithExpiration" - - // hack to avoid go vet error about passing a function to Sprintf - var value interface{} = o.o - return fmt.Sprintf("%s: %+v", name, value) -} - func WithExpiration(o *time.Time) Option { - return withExpirationImpl{ - o: o, + return func(c *config) error { + c.Expiration = o + return nil } } -type withMetaImpl struct { - o map[string]any -} - -func (o withMetaImpl) apply(c *config) error { - c.Meta = o.o - return nil -} - -func (o withMetaImpl) String() string { - name := "WithMeta" - - // hack to avoid go vet error about passing a function to Sprintf - var value interface{} = o.o - return fmt.Sprintf("%s: %+v", name, value) -} - -func WithMeta(o map[string]any) Option { - return withMetaImpl{ - o: o, +func WithMeta(o map[string]datamodel.Node) Option { + return func(c *config) error { + c.Meta = o + return nil } } -type withNoExpirationImpl struct { - o bool -} - -func (o withNoExpirationImpl) apply(c *config) error { - c.NoExpiration = o.o - return nil -} - -func (o withNoExpirationImpl) String() string { - name := "WithNoExpiration" - - // hack to avoid go vet error about passing a function to Sprintf - var value interface{} = o.o - return fmt.Sprintf("%s: %+v", name, value) -} - func WithNoExpiration(o bool) Option { - return withNoExpirationImpl{ - o: o, + return func(c *config) error { + c.NoExpiration = o + return nil } } -type withNotBeforeImpl struct { - o *time.Time -} - -func (o withNotBeforeImpl) apply(c *config) error { - c.NotBefore = o.o - return nil -} - -func (o withNotBeforeImpl) String() string { - name := "WithNotBefore" - - // hack to avoid go vet error about passing a function to Sprintf - var value interface{} = o.o - return fmt.Sprintf("%s: %+v", name, value) -} - func WithNotBefore(o *time.Time) Option { - return withNotBeforeImpl{ - o: o, + return func(c *config) error { + c.NotBefore = o + return nil } } -type withSubjectImpl struct { - o *did.DID -} - -func (o withSubjectImpl) apply(c *config) error { - c.Subject = o.o - return nil -} - -func (o withSubjectImpl) String() string { - name := "WithSubject" - - // hack to avoid go vet error about passing a function to Sprintf - var value interface{} = o.o - return fmt.Sprintf("%s: %+v", name, value) -} - // WithSubject is a did.DID representing the Subject. func WithSubject(o *did.DID) Option { - return withSubjectImpl{ - o: o, + return func(c *config) error { + c.Subject = o + return nil } } -type withPowerlineImpl struct { - o bool -} - -func (o withPowerlineImpl) apply(c *config) error { - c.Powerline = o.o - return nil -} - -func (o withPowerlineImpl) String() string { - name := "WithPowerline" - - // hack to avoid go vet error about passing a function to Sprintf - var value interface{} = o.o - return fmt.Sprintf("%s: %+v", name, value) -} - func WithPowerline(o bool) Option { - return withPowerlineImpl{ - o: o, + return func(c *config) error { + c.Powerline = o + return nil } } diff --git a/delegation/delegation.go b/delegation/delegation.go index 5884991..cc7ced6 100644 --- a/delegation/delegation.go +++ b/delegation/delegation.go @@ -1,11 +1,12 @@ package delegation import ( + "errors" + "fmt" "time" "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/node/bindnode" - "github.com/ipld/go-ipld-prime/schema" + "github.com/libp2p/go-libp2p/core/crypto" "github.com/ucan-wg/go-ucan/capability/command" "github.com/ucan-wg/go-ucan/capability/policy" "github.com/ucan-wg/go-ucan/did" @@ -14,15 +15,19 @@ import ( ) const ( - Tag = "ucan/dlg@" + Tag = "ucan/dlg@1.0.0-rc.1" ) -//go:generate -command options go run github.com/launchdarkly/go-options -//go:generate options -type=config -prefix=With -output=delegatiom_options.go -cmp=false -imports=time,github.com/ucan-wg/go-ucan/did +type Delegation struct { + envel *envelope.Envelope +} + +//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 type config struct { Expiration *time.Time - Meta map[string]any + Meta map[string]datamodel.Node NoExpiration bool NotBefore *time.Time // is a did.DID representing the Subject. @@ -30,81 +35,105 @@ type config struct { Powerline bool } -type Meta struct { - Keys []string - Values map[string]datamodel.Node -} +// Required fields for delegation -func NewMeta(meta map[string]any) Meta { - keys := make([]string, len(meta)) - values := make(map[string]datamodel.Node, len(meta)) - i := 0 +// Requirements for root - for k, v := range meta { - keys[i] = k - values[k] = bindnode.Wrap(&v, nil) - } - - return Meta{ - Keys: keys, - Values: values, - } -} - -var _ envelope.Tokener = (*Token)(nil) - -type Token struct { - // Issuer DID (sender) - Issuer did.DID - // Audience DID (receiver) - Audience did.DID - // Principal that the chain is about (the Subject) - Subject *did.DID - // The Command to eventually invoke - Command *command.Command - // The delegation policy - Policy *policy.Policy - // A unique, random nonce - Nonce []byte - // Arbitrary Metadata - Meta Meta - // "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer - NotBefore *time.Time - // The timestamp at which the Invocation becomes invalid - Expiration *time.Time -} - -func New(iss did.DID, aud did.DID, prf []Token, cmd *command.Command, pol *policy.Policy, exp *time.Time, nonce []byte, opts ...Option) (*Token, error) { +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) { cfg, err := newConfig(opts...) if err != nil { return nil, err } - tkn := &Token{ - Issuer: iss, - Audience: aud, - Subject: cfg.Subject, - Command: cmd, - Policy: pol, - Nonce: nonce, - NotBefore: cfg.NotBefore, + if !iss.Defined() { + return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "iss") } + if !aud.Defined() { + return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "aud") + } + audience := aud.String() + + var subject *string + if cfg.Subject != nil && cfg.Subject.Defined() { + s := cfg.Subject.String() + subject = &s + } + + policy, err := pol.ToIPLD() + if err != nil { + return nil, err + } + + nonce = []uint8(nonce) + + var notBefore *int + if cfg.NotBefore != nil { + n := int(cfg.NotBefore.Unix()) + notBefore = &n + } + + var meta *token.Map__String__Any if len(cfg.Meta) > 0 { - tkn.Meta = NewMeta(cfg.Meta) + m := token.ToIPLDMapStringAny(cfg.Meta) + meta = &m } + var expiration *int if exp != nil && !cfg.NoExpiration { - tkn.Expiration = exp + e := int(cfg.NotBefore.Unix()) + expiration = &e } - return tkn, nil + tkn := &token.Token{ + Issuer: iss.String(), + Audience: &audience, + Subject: subject, + Command: cmd.String(), + Policy: &policy, + Nonce: &nonce, + Meta: meta, + NotBefore: notBefore, + Expiration: expiration, + } + + envel, err := envelope.New(privKey, tkn, Tag) + if err != nil { + return nil, err + } + + dlg := &Delegation{envel: envel} + + if err := dlg.Validate(); err != nil { + return nil, err + } + + return dlg, nil } -func (d *Token) Tag() string { - return Tag +type validateFunc func() error + +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), + ) } -func (d *Token) Prototype() schema.TypedPrototype { - return bindnode.Prototype((*Token)(nil), mustLoadSchema().TypeByName("Delegation"), token.BindnodeOptions()...) +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) + } + + id, err := did.Parse(*identity) + if err != nil { + + } + + if !id.Defined() && !id.Key() { + return fmt.Errorf("a required DID is missing: %s", fieldName) + } + + return nil } diff --git a/delegation/delegation.ipldsch b/delegation/delegation.ipldsch deleted file mode 100644 index af1953e..0000000 --- a/delegation/delegation.ipldsch +++ /dev/null @@ -1,60 +0,0 @@ -type DID string - -# The Delegation payload MUST describe the authorization claims, who is involved, and its validity period. -type Payload struct { - # Issuer DID (sender) - iss DID - # Audience DID (receiver) - aud DID - # Principal that the chain is about (the Subject) - sub optional DID - - # The Command to eventually invoke - cmd String - - # The delegation policy - # It doesn't seem possible to represent it with a schema. - pol Any - - # A unique, random nonce - nonce Bytes - - # Arbitrary Metadata - meta {String : Any} - - # "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer - nbf optional Int - # The timestamp at which the Invocation becomes invalid - exp nullable Int -} - -type Delegation struct { - # Issuer DID (sender) - issuer DID (rename "iss") - # Audience DID (receiver) - audience DID (rename "aud") - # Principal that the chain is about (the Subject) - subject optional DID (rename "sub") - - # The Command to eventually invoke - command String (rename "cmd") - - # The delegation policy - # It doesn't seem possible to represent it with a schema. - policy Any (rename "pol") - - # A unique, random nonce - nonce Bytes - - # Arbitrary Metadata - meta {String : Any} - - # "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer - notBefore optional Int (rename "nbf") - # The timestamp at which the Invocation becomes invalid - expiration nullable Int (rename "exp") -} - -type Save struct { - -} \ No newline at end of file diff --git a/delegation/delegation_test.go b/delegation/delegation_test.go deleted file mode 100644 index 3f3c5dc..0000000 --- a/delegation/delegation_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package delegation_test - -import ( - "testing" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec/dagjson" - "github.com/ipld/go-ipld-prime/node/bindnode" - "github.com/ipld/go-ipld-prime/schema" - "github.com/stretchr/testify/require" - "github.com/ucan-wg/go-ucan/delegation" - "github.com/ucan-wg/go-ucan/internal/token" -) - -func TestToken_Proto(t *testing.T) { - t.Parallel() - - const delegationJson = ` -{ - "aud":"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "cmd":"/foo/bar", - "exp":123456, - "iss":"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "meta":{ - "bar":"baaar", - "foo":"fooo" - }, - "nbf":123456, - "nonce":{ - "/":{ - "bytes":"c3VwZXItcmFuZG9t" - } - }, - "pol":[ - ["==", ".status", "draft"], - ["all", ".reviewer", [ - ["like", ".email", "*@example.com"]] - ], - ["any", ".tags", [ - ["or", [ - ["==", ".", "news"], - ["==", ".", "press"]] - ]] - ] - ], - "sub":"did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" -} -` - - proto := (*delegation.Token)(nil).Prototype() - - node, err := ipld.DecodeUsingPrototype([]byte(delegationJson), dagjson.Decode, proto) - require.NoError(t, err) - - tkn, ok := bindnode.Unwrap(node).(*delegation.Token) - require.True(t, ok) - - t.Log("Token:") - t.Log(" Audience:", tkn.Audience) - t.Log(" Command: ", tkn.Command) - // t.Log(" Expiration: ", token.Expiration) - t.Log(" Issuer:", tkn.Issuer) - // t.Log(" Meta:", token.Meta) - // t.Log(" NotBefore", token.NotBefore) - // t.Log(" Nonce:", token.Nonce) - // t.Log(" Policy:", token.Policy) - t.Log(" Subject:", tkn.Subject) - - // token.Command = nil - // token.Meta = nil - // token.Policy = nil - // token.Expiration = nil - // token.NotBefore = nil - - _ = bindnode.Wrap(tkn, proto.Type(), token.BindnodeOptions()...) - - typed, ok := node.(schema.TypedNode) - require.True(t, ok) - - json, err := ipld.Encode(typed.Representation(), dagjson.Encode) - require.NoError(t, err) - - require.JSONEq(t, delegationJson, string(json)) - - t.Log(string(json)) - t.Fail() -} diff --git a/delegation/encoding.go b/delegation/encoding.go new file mode 100644 index 0000000..a03f08b --- /dev/null +++ b/delegation/encoding.go @@ -0,0 +1,93 @@ +package delegation + +import ( + "fmt" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ucan-wg/go-ucan/internal/envelope" +) + +// Encode marshals a Delegation to the the format specified by the provided +// codec.Encoder. +func (d *Delegation) Encode(encFn codec.Encoder) ([]byte, error) { + node, err := d.ToIPLD() + if err != nil { + return nil, err + } + + return ipld.Encode(node, encFn) +} + +// ToDagCbor marshals the Delegation to the DAG-CBOR format. +func (d *Delegation) ToDagCbor() ([]byte, error) { + return d.Encode(dagcbor.Encode) +} + +// ToDagJson marshals the Delegation to the DAG-JSON format. +func (d *Delegation) ToDagJson() ([]byte, error) { + return d.Encode(dagjson.Encode) +} + +// ToIPLD wraps the Delegation in an IPLD datamodel.Node. +func (d *Delegation) ToIPLD() (datamodel.Node, error) { + return d.envel.Wrap() +} + +// Decode unmarshals the input data using the format specified by the +// provided codec.Decoder into a Delegation. +// +// An error is returned if the conversion fails, or if the resulting +// Delegation is invalid. +func Decode(b []byte, decFn codec.Decoder) (*Delegation, error) { + node, err := ipld.Decode(b, decFn) + if err != nil { + return nil, err + } + + return FromIPLD(node) +} + +// FromDagCbor unmarshals the input data into a Delegation. +// +// An error is returned if the conversion fails, or if the resulting +// Delegation is invalid. +func FromDagCbor(data []byte) (*Delegation, error) { + return Decode(data, dagcbor.Decode) +} + +// FromDagsjon unmarshals the input data into a Delegation. +// +// An error is returned if the conversion fails, or if the resulting +// Delegation is invalid. +func FromDagJson(data []byte) (*Delegation, error) { + return Decode(data, dagjson.Decode) +} + +// FromIPLD unwraps a Delegation from the provided IPLD datamodel.Node +// +// An error is returned if the conversion fails, or if the resulting +// Delegation is invalid. +func FromIPLD(node datamodel.Node) (*Delegation, error) { + envel, err := envelope.Unwrap(node) + if err != nil { + return nil, err + } + + if envel.Tag() != Tag { + return nil, fmt.Errorf("wrong tag for TokenPayload: received %s but expected %s", envel.Tag(), Tag) + } + + dlg := &Delegation{ + envel: envel, + } + + if err := dlg.Validate(); err != nil { + return nil, err + } + + return dlg, nil +} diff --git a/delegation/encoding_test.go b/delegation/encoding_test.go new file mode 100644 index 0000000..794cf09 --- /dev/null +++ b/delegation/encoding_test.go @@ -0,0 +1,101 @@ +package delegation_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/delegation" +) + +func TestEncodingRoundTrip(t *testing.T) { + const delegationJson = ` +[ + { + "/": { + "bytes": "QWr0Pk+sSWE1nszuBMQzggbHX4ofJb8QRdwrLJK/AGCx2p4s/xaCRieomfstDjsV4ezBzX1HARvcoNgdwDQ8Aw" + } + }, + { + "h": { + "/": { + "bytes": "NO0BcQ" + } + }, + "ucan/dlg@1.0.0-rc.1": { + "aud": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2", + "cmd": "/foo/bar", + "exp": -62135596800, + "iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2", + "meta": { + "bar": "barr", + "foo": "fooo" + }, + "nbf": -62135596800, + "nonce": { + "/": { + "bytes": "X93ORvN1QIXrKPyEP5m5XoVK9VLX9nX8VV/+HlWrp9c" + } + }, + "pol": [ + [ + "==", + ".status", + "draft" + ], + [ + "all", + ".reviewer", + [ + "like", + ".email", + "*@example.com" + ] + ], + [ + "any", + ".tags", + [ + "or", + [ + [ + "==", + ".", + "news" + ], + [ + "==", + ".", + "press" + ] + ] + ] + ] + ], + "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2" + } + } +] +` + // format: dagJson --> Delegation --> dagCbor --> Delegation --> dagJson + // function: FromDagJson() ToDagCbor() FromDagCbor() ToDagJson() + + p1, err := delegation.FromDagJson([]byte(delegationJson)) + require.NoError(t, err) + + cborBytes, err := p1.ToDagCbor() + require.NoError(t, err) + fmt.Println("cborBytes length", len(cborBytes)) + fmt.Println("cbor", string(cborBytes)) + + p2, err := delegation.FromDagCbor(cborBytes) + require.NoError(t, err) + fmt.Println("read Cbor", p2) + + readJson, err := p2.ToDagJson() + require.NoError(t, err) + fmt.Println("readJson length", len(readJson)) + fmt.Println("json: ", string(readJson)) + + require.JSONEq(t, delegationJson, string(readJson)) +} diff --git a/delegation/junk.json b/delegation/junk.json deleted file mode 100644 index e69de29..0000000 diff --git a/delegation/schema.go b/delegation/schema.go deleted file mode 100644 index 4f6ede9..0000000 --- a/delegation/schema.go +++ /dev/null @@ -1,33 +0,0 @@ -package delegation - -import ( - _ "embed" - "fmt" - "sync" - - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/schema" -) - -//go:embed delegation.ipldsch -var schemaBytes []byte - -var ( - once sync.Once - ts *schema.TypeSystem - err error -) - -func mustLoadSchema() *schema.TypeSystem { - once.Do(func() { - ts, err = ipld.LoadSchemaBytes(schemaBytes) - }) - if err != nil { - panic(fmt.Errorf("failed to load IPLD schema: %s", err)) - } - return ts -} - -func PayloadType() schema.Type { - return mustLoadSchema().TypeByName("Payload") -} diff --git a/delegation/schema_test.go b/delegation/schema_test.go deleted file mode 100644 index 1c81c6e..0000000 --- a/delegation/schema_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package delegation - -import ( - "testing" - - "github.com/ipld/go-ipld-prime" -) - -func BenchmarkSchemaLoad(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _, _ = ipld.LoadSchemaBytes(schemaBytes) - } -} diff --git a/internal/envelope/envelope.go b/internal/envelope/envelope.go index b63eada..b9831df 100644 --- a/internal/envelope/envelope.go +++ b/internal/envelope/envelope.go @@ -98,10 +98,16 @@ func Unwrap(node datamodel.Node) (*Envelope, error) { return nil, err } - return &Envelope{ + envel := &Envelope{ signature: signature, sigPayload: sigPayload, - }, nil + } + + if ok, err := envel.Verify(); !ok || err != nil { + return nil, fmt.Errorf("envelope was not signed by issuer") + } + + return envel, nil } // Signature returns the cryptographic signature of the Envelope's diff --git a/internal/envelope/testdata/example.dagjson b/internal/envelope/testdata/example.dagjson index 4577a11..d801946 100644 --- a/internal/envelope/testdata/example.dagjson +++ b/internal/envelope/testdata/example.dagjson @@ -1 +1,20 @@ -[{"/":{"bytes":"PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"cmd":"","exp":null,"iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh","sub":null}}] \ No newline at end of file +[ + { + "/": { + "bytes": "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg" + } + }, + { + "h": { + "/": { + "bytes": "NO0BcQ" + } + }, + "ucan/example@v1.0.0-rc.1": { + "cmd": "", + "exp": null, + "iss": "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh", + "sub": null + } + } +] \ No newline at end of file