From f44b5cb921649d9301f5731b7e47969cea2df0c9 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Wed, 18 Sep 2024 12:20:54 -0400 Subject: [PATCH] feat(delegation): update to provide encoding/decoding straight from/to View --- delegation/delegation.go | 1 + delegation/delegation_test.go | 142 +++++++++++++++++++++++++++ delegation/encoding.go | 33 ------- delegation/ipld.go | 168 ++++++++++++++++++++++++++++++++ delegation/schema.go | 14 +++ delegation/schema_test.go | 84 +++++++++------- delegation/testdata/new.dagjson | 1 + delegation/view.go | 10 +- 8 files changed, 379 insertions(+), 74 deletions(-) create mode 100644 delegation/delegation.go create mode 100644 delegation/delegation_test.go delete mode 100644 delegation/encoding.go create mode 100644 delegation/ipld.go create mode 100644 delegation/testdata/new.dagjson diff --git a/delegation/delegation.go b/delegation/delegation.go new file mode 100644 index 0000000..4c26134 --- /dev/null +++ b/delegation/delegation.go @@ -0,0 +1 @@ +package delegation diff --git a/delegation/delegation_test.go b/delegation/delegation_test.go new file mode 100644 index 0000000..db324ee --- /dev/null +++ b/delegation/delegation_test.go @@ -0,0 +1,142 @@ +package delegation_test + +import ( + "crypto/rand" + "testing" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/did" +) + +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), delegation.WithExpiration(&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), delegation.WithExpiration(&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/encoding.go b/delegation/encoding.go deleted file mode 100644 index e993d46..0000000 --- a/delegation/encoding.go +++ /dev/null @@ -1,33 +0,0 @@ -package delegation - -import ( - "github.com/ipld/go-ipld-prime" - "github.com/ipld/go-ipld-prime/codec/dagcbor" - "github.com/ipld/go-ipld-prime/codec/dagjson" -) - -func (p *PayloadModel) EncodeDagCbor() ([]byte, error) { - return ipld.Marshal(dagcbor.Encode, p, PayloadType()) -} - -func (p *PayloadModel) EncodeDagJson() ([]byte, error) { - return ipld.Marshal(dagjson.Encode, p, PayloadType()) -} - -func DecodeDagCbor(data []byte) (*PayloadModel, error) { - var p PayloadModel - _, err := ipld.Unmarshal(data, dagcbor.Decode, &p, PayloadType()) - if err != nil { - return nil, err - } - return &p, nil -} - -func DecodeDagJson(data []byte) (*PayloadModel, error) { - var p PayloadModel - _, err := ipld.Unmarshal(data, dagjson.Decode, &p, PayloadType()) - if err != nil { - return nil, err - } - return &p, nil -} diff --git a/delegation/ipld.go b/delegation/ipld.go new file mode 100644 index 0000000..a64be83 --- /dev/null +++ b/delegation/ipld.go @@ -0,0 +1,168 @@ +package delegation + +import ( + "io" + + "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/libp2p/go-libp2p/core/crypto" + + "github.com/ucan-wg/go-ucan/did" + "github.com/ucan-wg/go-ucan/internal/envelope" +) + +// Encode marshals a View to the format specified by the provided +// codec.Encoder. +func (d *View) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) { + node, err := d.ToIPLD(privKey) + if err != nil { + return nil, err + } + + return ipld.Encode(node, encFn) +} + +// EncodeWriter is the same as Encode but accepts an io.Writer. +func (d *View) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error { + node, err := d.ToIPLD(privKey) + if err != nil { + return err + } + + return ipld.EncodeStreaming(w, node, encFn) +} + +// ToDagCbor marshals the View to the DAG-CBOR format. +func (d *View) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) { + return d.Encode(privKey, dagcbor.Encode) +} + +// ToDagCborWriter is the same as ToDagCbor but it accepts an io.Writer. +func (d *View) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error { + return d.EncodeWriter(w, privKey, dagcbor.Encode) +} + +// ToDagJson marshals the View to the DAG-JSON format. +func (d *View) ToDagJson(privKey crypto.PrivKey) ([]byte, error) { + return d.Encode(privKey, dagjson.Encode) +} + +// ToDagJsonWriter is the same as ToDagJson but it accepts an io.Writer. +func (d *View) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error { + return d.EncodeWriter(w, privKey, dagjson.Encode) +} + +// ToIPLD wraps the View in an IPLD datamodel.Node. +func (d *View) ToIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { + var sub *string + if d.Subject != did.Undef { + s := d.Subject.String() + sub = &s + } + + pol, err := d.Policy.ToIPLD() + if err != nil { + return nil, err + } + + metaKeys := make([]string, len(d.Meta)) + i := 0 + + for k := range d.Meta { + metaKeys[i] = k + i++ + } + + var nbf *int64 + if d.NotBefore != nil { + u := d.NotBefore.Unix() + nbf = &u + } + + var exp *int64 + if d.Expiration != nil { + u := d.Expiration.Unix() + exp = &u + } + + model := &PayloadModel{ + Iss: d.Issuer.String(), + Aud: d.Audience.String(), + Sub: sub, + Cmd: d.Command.String(), + Pol: pol, + Nonce: d.Nonce, + Meta: MetaModel{ + Keys: metaKeys, + Values: d.Meta, + }, + Nbf: nbf, + Exp: exp, + } + + return envelope.ToIPLD(privKey, model) +} + +// Decode unmarshals the input data using the format specified by the +// provided codec.Decoder into a View. +// +// An error is returned if the conversion fails, or if the resulting +// View is invalid. +func Decode(b []byte, decFn codec.Decoder) (*View, error) { + node, err := ipld.Decode(b, decFn) + if err != nil { + return nil, err + } + return FromIPLD(node) +} + +// DecodeReader is the same as Decode, but accept an io.Reader. +func DecodeReader(r io.Reader, decFn codec.Decoder) (*View, error) { + node, err := ipld.DecodeStreaming(r, decFn) + if err != nil { + return nil, err + } + return FromIPLD(node) +} + +// FromDagCbor unmarshals the input data into a View. +// +// An error is returned if the conversion fails, or if the resulting +// View is invalid. +func FromDagCbor(data []byte) (*View, error) { + return Decode(data, dagcbor.Decode) +} + +// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader. +func FromDagCborReader(r io.Reader) (*View, error) { + return DecodeReader(r, dagcbor.Decode) +} + +// FromDagJson unmarshals the input data into a View. +// +// An error is returned if the conversion fails, or if the resulting +// View is invalid. +func FromDagJson(data []byte) (*View, error) { + return Decode(data, dagjson.Decode) +} + +// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader. +func FromDagJsonReader(r io.Reader) (*View, error) { + return DecodeReader(r, dagjson.Decode) +} + +// FromIPLD unwraps a View from the provided IPLD datamodel.Node +// +// An error is returned if the conversion fails, or if the resulting +// View is invalid. +func FromIPLD(node datamodel.Node) (*View, error) { + tkn, _, err := envelope.FromIPLD[*PayloadModel](node) // TODO add CID to view + if err != nil { + return nil, err + } + + return ViewFromModel(*tkn) +} diff --git a/delegation/schema.go b/delegation/schema.go index 4615c6b..6dde5aa 100644 --- a/delegation/schema.go +++ b/delegation/schema.go @@ -7,9 +7,13 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/schema" + "github.com/ucan-wg/go-ucan/internal/envelope" ) +const Tag = "ucan/dlg@1.0.0-rc.1" + //go:embed delegation.ipldsch var schemaBytes []byte @@ -33,6 +37,8 @@ func PayloadType() schema.Type { return mustLoadSchema().TypeByName("Payload") } +var _ envelope.Tokener = (*PayloadModel)(nil) + type PayloadModel struct { // Issuer DID (sender) Iss string @@ -63,6 +69,14 @@ type PayloadModel struct { Exp *int64 } +func (e *PayloadModel) Prototype() schema.TypedPrototype { + return bindnode.Prototype((*PayloadModel)(nil), PayloadType()) +} + +func (*PayloadModel) Tag() string { + return Tag +} + type MetaModel struct { Keys []string Values map[string]datamodel.Node diff --git a/delegation/schema_test.go b/delegation/schema_test.go index bb7daed..75a839d 100644 --- a/delegation/schema_test.go +++ b/delegation/schema_test.go @@ -1,66 +1,76 @@ -package delegation +package delegation_test import ( + _ "embed" "fmt" "testing" "github.com/ipld/go-ipld-prime" "github.com/stretchr/testify/require" + "github.com/ucan-wg/go-ucan/delegation" + "gotest.tools/v3/golden" ) +//go:embed delegation.ipldsch +var schemaBytes []byte + func TestSchemaRoundTrip(t *testing.T) { - const delegationJson = ` -{ - "aud":"did:key:def456", - "cmd":"/foo/bar", - "exp":123456, - "iss":"did:key:abc123", - "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":"" -} -` + // const delegationJson = ` + // { + // "aud":"did:key:def456", + // "cmd":"/foo/bar", + // "exp":123456, + // "iss":"did:key:abc123", + // "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":"" + // } + // ` + + delegationJson := golden.Get(t, "new.dagjson") + privKey := privKey(t, issuerPrivKeyCfg) + // format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson // function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson() - p1, err := DecodeDagJson([]byte(delegationJson)) + p1, err := delegation.FromDagJson([]byte(delegationJson)) require.NoError(t, err) - cborBytes, err := p1.EncodeDagCbor() + cborBytes, err := p1.ToDagCbor(privKey) require.NoError(t, err) fmt.Println("cborBytes length", len(cborBytes)) fmt.Println("cbor", string(cborBytes)) - p2, err := DecodeDagCbor(cborBytes) + p2, err := delegation.FromDagCbor(cborBytes) require.NoError(t, err) fmt.Println("read Cbor", p2) - readJson, err := p2.EncodeDagJson() + readJson, err := p2.ToDagJson(privKey) require.NoError(t, err) fmt.Println("readJson length", len(readJson)) fmt.Println("json: ", string(readJson)) - require.JSONEq(t, delegationJson, string(readJson)) + require.JSONEq(t, string(delegationJson), string(readJson)) } func BenchmarkSchemaLoad(b *testing.B) { 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/view.go b/delegation/view.go index 61f629d..1b8dfc7 100644 --- a/delegation/view.go +++ b/delegation/view.go @@ -27,9 +27,9 @@ type View struct { // Arbitrary Metadata Meta map[string]datamodel.Node // "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer - NotBefore time.Time + NotBefore *time.Time // The timestamp at which the Invocation becomes invalid - Expiration time.Time + Expiration *time.Time } // ViewFromModel build a decoded view of the raw IPLD data. @@ -76,11 +76,13 @@ func ViewFromModel(m PayloadModel) (*View, error) { view.Meta = m.Meta.Values if m.Nbf != nil { - view.NotBefore = time.Unix(*m.Nbf, 0) + t := time.Unix(*m.Nbf, 0) + view.NotBefore = &t } if m.Exp != nil { - view.Expiration = time.Unix(*m.Exp, 0) + t := time.Unix(*m.Exp, 0) + view.Expiration = &t } return &view, nil