From c2cc0ca0a4f450ac7f1fa70161cb12c8a17678b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 30 Aug 2024 22:06:59 +0200 Subject: [PATCH] delegation: first import with rough IPLD round-trip --- delegation/delegation.ipldsch | 32 ++++++ delegation/encoding.go | 33 ++++++ delegation/policy/match.go | 3 +- delegation/policy/match_test.go | 5 +- delegation/policy/policy.go | 3 +- delegation/policy/selector/supported_test.go | 3 +- delegation/schema.go | 76 ++++++++++++++ delegation/schema_test.go | 60 +++++++++++ delegation/view.go | 100 +++++++++++++++++++ go.mod | 12 ++- go.sum | 14 +++ 11 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 delegation/delegation.ipldsch create mode 100644 delegation/encoding.go create mode 100644 delegation/schema.go create mode 100644 delegation/schema_test.go create mode 100644 delegation/view.go diff --git a/delegation/delegation.ipldsch b/delegation/delegation.ipldsch new file mode 100644 index 0000000..0e8e489 --- /dev/null +++ b/delegation/delegation.ipldsch @@ -0,0 +1,32 @@ +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 + pol Policy + + # 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 Policy struct { + +} \ No newline at end of file diff --git a/delegation/encoding.go b/delegation/encoding.go new file mode 100644 index 0000000..e993d46 --- /dev/null +++ b/delegation/encoding.go @@ -0,0 +1,33 @@ +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/policy/match.go b/delegation/policy/match.go index 5cf07a5..277a707 100644 --- a/delegation/policy/match.go +++ b/delegation/policy/match.go @@ -7,7 +7,8 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/must" - "github.com/storacha-network/go-ucanto/core/policy/selector" + + "github.com/ucan-wg/go-ucan/delegation/policy/selector" ) // Match determines if the IPLD node matches the policy document. diff --git a/delegation/policy/match_test.go b/delegation/policy/match_test.go index e7546a5..ee011d0 100644 --- a/delegation/policy/match_test.go +++ b/delegation/policy/match_test.go @@ -9,9 +9,10 @@ import ( "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/storacha-network/go-ucanto/core/policy/literal" - "github.com/storacha-network/go-ucanto/core/policy/selector" "github.com/stretchr/testify/require" + + "github.com/ucan-wg/go-ucan/delegation/policy/literal" + "github.com/ucan-wg/go-ucan/delegation/policy/selector" ) func TestMatch(t *testing.T) { diff --git a/delegation/policy/policy.go b/delegation/policy/policy.go index 49f1f48..1e023ea 100644 --- a/delegation/policy/policy.go +++ b/delegation/policy/policy.go @@ -5,7 +5,8 @@ package policy import ( "github.com/gobwas/glob" "github.com/ipld/go-ipld-prime" - "github.com/storacha-network/go-ucanto/core/policy/selector" + + "github.com/ucan-wg/go-ucan/delegation/policy/selector" ) const ( diff --git a/delegation/policy/selector/supported_test.go b/delegation/policy/selector/supported_test.go index 8a29471..f7b18ef 100644 --- a/delegation/policy/selector/supported_test.go +++ b/delegation/policy/selector/supported_test.go @@ -12,10 +12,11 @@ import ( "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/datamodel" basicnode "github.com/ipld/go-ipld-prime/node/basic" - "github.com/storacha-network/go-ucanto/core/policy/selector" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/wI2L/jsondiff" + + "github.com/ucan-wg/go-ucan/delegation/policy/selector" ) //go:embed supported.json diff --git a/delegation/schema.go b/delegation/schema.go new file mode 100644 index 0000000..f2c7c38 --- /dev/null +++ b/delegation/schema.go @@ -0,0 +1,76 @@ +package delegation + +import ( + _ "embed" + "fmt" + "sync" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "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") +} + +type PayloadModel struct { + // Issuer DID (sender) + Iss string + // Audience DID (receiver) + Aud string + // Principal that the chain is about (the Subject) + // optional: can be nil + Sub *string + + // The Command to eventually invoke + Cmd string + + // The delegation policy + Pol PolicyModel + + // A unique, random nonce + Nonce []byte + + // Arbitrary Metadata + // optional: can be nil + Meta MetaModel + + // "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer + // optional: can be nil + Nbf *int64 + // The timestamp at which the Invocation becomes invalid + // optional: can be nil + Exp *int64 +} + +type MetaModel struct { + Keys []string + Values map[string]datamodel.Node +} + +type PolicyModel struct { +} + +func PointerTo[T any](v T) *T { + return &v +} diff --git a/delegation/schema_test.go b/delegation/schema_test.go new file mode 100644 index 0000000..6abad79 --- /dev/null +++ b/delegation/schema_test.go @@ -0,0 +1,60 @@ +package delegation + +import ( + "fmt" + "testing" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/stretchr/testify/require" +) + +func TestSchemaRoundTrip(t *testing.T) { + p := &PayloadModel{ + Iss: "did:key:abc123", + Aud: "did:key:def456", + Sub: PointerTo(""), + Cmd: "/foo/bar", + Pol: PolicyModel{}, // TODO: have something here + Nonce: []byte("super-random"), + Meta: MetaModel{ + Keys: []string{"foo", "bar"}, + Values: map[string]datamodel.Node{ + "foo": bindnode.Wrap(PointerTo("fooo"), nil), + "bar": bindnode.Wrap(PointerTo("baaar"), nil), + }, + }, + Nbf: PointerTo(int64(123456)), + Exp: PointerTo(int64(123456)), + } + + cborBytes, err := p.EncodeDagCbor() + require.NoError(t, err) + fmt.Println("cborBytes length", len(cborBytes)) + fmt.Println("cbor", string(cborBytes)) + + jsonBytes, err := p.EncodeDagJson() + require.NoError(t, err) + fmt.Println("jsonBytes length", len(jsonBytes)) + fmt.Println("json: ", string(jsonBytes)) + + fmt.Println() + + readCbor, err := DecodeDagCbor(cborBytes) + require.NoError(t, err) + fmt.Println("readCbor", readCbor) + require.Equal(t, p, readCbor) + + readJson, err := DecodeDagJson(jsonBytes) + require.NoError(t, err) + fmt.Println("readJson", readJson) + require.Equal(t, p, readJson) +} + +func BenchmarkSchemaLoad(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, _ = ipld.LoadSchemaBytes(schemaBytes) + } +} diff --git a/delegation/view.go b/delegation/view.go new file mode 100644 index 0000000..b9d2f6d --- /dev/null +++ b/delegation/view.go @@ -0,0 +1,100 @@ +package delegation + +import ( + "fmt" + "time" + + "github.com/ipld/go-ipld-prime/datamodel" + + "github.com/ucan-wg/go-ucan/delegation/policy" + "github.com/ucan-wg/go-ucan/did" +) + +type View 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 string + // The delegation policy + Policy policy.Policy + // A unique, random nonce + Nonce []byte + // Arbitrary Metadata + Meta map[string]datamodel.Node + // "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 +} + +// ViewFromModel build a decoded view of the raw IPLD data. +// This function also serves as validation. +func ViewFromModel(m PayloadModel) (*View, error) { + var view View + var err error + + view.Issuer, err = did.Parse(m.Iss) + if err != nil { + return nil, fmt.Errorf("parse iss: %w", err) + } + + view.Audience, err = did.Parse(m.Aud) + if err != nil { + return nil, fmt.Errorf("parse audience: %w", err) + } + + if m.Sub != nil { + view.Subject, err = did.Parse(*m.Sub) + if err != nil { + return nil, fmt.Errorf("parse subject: %w", err) + } + } else { + view.Subject = did.Undef + } + + // TODO: make that a Command object, and validate it + view.Command = m.Cmd + + // TODO: parsing + validation + view.Policy = policy.Policy{} + + if len(m.Nonce) == 0 { + return nil, fmt.Errorf("nonce is required") + } + view.Nonce = m.Nonce + + // TODO: copy? + view.Meta = m.Meta.Values + + if m.Nbf != nil { + view.NotBefore = time.Unix(*m.Nbf, 0) + } + + if m.Exp != nil { + view.Expiration = time.Unix(*m.Exp, 0) + } + + return &view, nil +} + +func (view *View) Capability() *Capability { + return &Capability{ + Subject: view.Subject, + Command: view.Command, + Policy: view.Policy, + } +} + +// Capability is a subset of a delegation formed by the triple (subject, command, policy). +type Capability struct { + // Principal that the chain is about (the Subject) + Subject did.DID + // The Command to eventually invoke + Command string + // The delegation policy + Policy policy.Policy +} diff --git a/go.mod b/go.mod index c5c2959..4a46b48 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,30 @@ module github.com/ucan-wg/go-ucan go 1.22.1 require ( + github.com/gobwas/glob v0.2.3 + github.com/ipfs/go-cid v0.4.1 github.com/ipld/go-ipld-prime v0.21.0 + github.com/multiformats/go-multibase v0.0.3 + github.com/multiformats/go-varint v0.0.6 github.com/stretchr/testify v1.9.0 + github.com/wI2L/jsondiff v0.6.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ipfs/go-cid v0.4.1 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.0.3 // indirect github.com/multiformats/go-base36 v0.1.0 // indirect - github.com/multiformats/go-multibase v0.0.3 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-varint v0.0.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 8351aca..930d7f4 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -55,7 +57,19 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= +github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=