Compare commits
23 Commits
v1-policy-
...
envelope
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ca17ea63d | ||
|
|
658794041e | ||
|
|
f85ece49fa | ||
|
|
da9f2e7bec | ||
|
|
bd1775b2f5 | ||
|
|
285cb5f6e7 | ||
|
|
4c05d866f2 | ||
|
|
646127abe7 | ||
|
|
7cead1bf8d | ||
|
|
16f0f38b43 | ||
|
|
2f183aa6f4 | ||
|
|
6d1b7ee01f | ||
|
|
599c5d30b0 | ||
|
|
1c2f602f4d | ||
|
|
8441f99d5d | ||
|
|
1525aaa139 | ||
|
|
ab4018d218 | ||
|
|
39987eadaa | ||
|
|
b77f8d6bb0 | ||
|
|
719837e3cd | ||
|
|
2205d5d4ce | ||
|
|
3a542ecc85 | ||
|
|
ba1c45c088 |
34
.github/workflows/bench.yml
vendored
34
.github/workflows/bench.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: go continuous benchmark
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- v1
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
deployments: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
benchmark:
|
|
||||||
name: Run Go continuous benchmark
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: "stable"
|
|
||||||
- name: Run benchmark
|
|
||||||
run: go test -v ./... -bench=. -run=xxx -benchmem | tee output.txt
|
|
||||||
|
|
||||||
- name: Store benchmark result
|
|
||||||
uses: benchmark-action/github-action-benchmark@v1
|
|
||||||
with:
|
|
||||||
name: Go Benchmark
|
|
||||||
tool: 'go'
|
|
||||||
output-file-path: output.txt
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Push and deploy GitHub pages branch automatically
|
|
||||||
auto-push: true
|
|
||||||
# Show alert with commit comment on detecting possible performance regression
|
|
||||||
alert-threshold: '200%'
|
|
||||||
comment-on-alert: true
|
|
||||||
@@ -16,12 +16,15 @@ var _ fmt.Stringer = (*Command)(nil)
|
|||||||
// by one or more slash-separated Segments of lowercase characters.
|
// by one or more slash-separated Segments of lowercase characters.
|
||||||
//
|
//
|
||||||
// [Command]: https://github.com/ucan-wg/spec#command
|
// [Command]: https://github.com/ucan-wg/spec#command
|
||||||
type Command string
|
type Command struct {
|
||||||
|
segments []string
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a validated command from the provided list of segment strings.
|
// New creates a validated command from the provided list of segment
|
||||||
// An error is returned if an invalid Command would be formed
|
// strings. An error is returned if an invalid Command would be
|
||||||
func New(segments ...string) Command {
|
// formed
|
||||||
return Top().Join(segments...)
|
func New(segments ...string) *Command {
|
||||||
|
return &Command{segments: segments}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse verifies that the provided string contains the required
|
// Parse verifies that the provided string contains the required
|
||||||
@@ -29,26 +32,26 @@ func New(segments ...string) Command {
|
|||||||
// Command.
|
// Command.
|
||||||
//
|
//
|
||||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
func Parse(s string) (Command, error) {
|
func Parse(s string) (*Command, error) {
|
||||||
if !strings.HasPrefix(s, "/") {
|
if !strings.HasPrefix(s, "/") {
|
||||||
return "", ErrRequiresLeadingSlash
|
return nil, ErrRequiresLeadingSlash
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||||
return "", ErrDisallowsTrailingSlash
|
return nil, ErrDisallowsTrailingSlash
|
||||||
}
|
}
|
||||||
|
|
||||||
if s != strings.ToLower(s) {
|
if s != strings.ToLower(s) {
|
||||||
return "", ErrRequiresLowercase
|
return nil, ErrRequiresLowercase
|
||||||
}
|
}
|
||||||
|
|
||||||
// The leading slash will result in the first element from strings.Split
|
// The leading slash will result in the first element from strings.Split
|
||||||
// being an empty string which is removed as strings.Join will ignore it.
|
// being an empty string which is removed as strings.Join will ignore it.
|
||||||
return Command(s), nil
|
return &Command{strings.Split(s, "/")[1:]}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustParse is the same as Parse, but panic() if the parsing fail.
|
// MustParse is the same as Parse, but panic() if the parsing fail.
|
||||||
func MustParse(s string) Command {
|
func MustParse(s string) *Command {
|
||||||
c, err := Parse(s)
|
c, err := Parse(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -56,14 +59,15 @@ func MustParse(s string) Command {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top is the most powerful capability.
|
// [Top] is the most powerful capability.
|
||||||
//
|
//
|
||||||
// This function returns a Command that is a wildcard and therefore represents the
|
// This function returns a Command that is a wildcard and therefore represents the
|
||||||
// most powerful ability. As such, it should be handled with care and used sparingly.
|
// most powerful abilily. As such it should be handle with care and used
|
||||||
|
// sparingly.
|
||||||
//
|
//
|
||||||
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
||||||
func Top() Command {
|
func Top() *Command {
|
||||||
return Command(separator)
|
return New()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid returns true if the provided string is a valid UCAN command.
|
// IsValid returns true if the provided string is a valid UCAN command.
|
||||||
@@ -74,35 +78,18 @@ func IsValid(s string) bool {
|
|||||||
|
|
||||||
// Join appends segments to the end of this command using the required
|
// Join appends segments to the end of this command using the required
|
||||||
// segment separator.
|
// segment separator.
|
||||||
func (c Command) Join(segments ...string) Command {
|
func (c *Command) Join(segments ...string) *Command {
|
||||||
size := 0
|
return &Command{append(c.segments, segments...)}
|
||||||
for _, s := range segments {
|
|
||||||
size += len(s)
|
|
||||||
}
|
|
||||||
if size == 0 {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
buf := make([]byte, 0, len(c)+size+len(segments))
|
|
||||||
buf = append(buf, []byte(c)...)
|
|
||||||
for _, s := range segments {
|
|
||||||
if s != "" {
|
|
||||||
if len(buf) > 1 {
|
|
||||||
buf = append(buf, separator...)
|
|
||||||
}
|
|
||||||
buf = append(buf, []byte(s)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Command(buf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Segments returns the ordered segments that comprise the Command as a
|
// Segments returns the ordered segments that comprise the Command as a
|
||||||
// slice of strings.
|
// slice of strings.
|
||||||
func (c Command) Segments() []string {
|
func (c *Command) Segments() []string {
|
||||||
return strings.Split(string(c), separator)
|
return c.segments
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the composed representation the command. This is also
|
// String returns the composed representation the command. This is also
|
||||||
// the required wire representation (before IPLD encoding occurs.)
|
// the required wire representation (before IPLD encoding occurs.)
|
||||||
func (c Command) String() string {
|
func (c *Command) String() string {
|
||||||
return string(c)
|
return "/" + strings.Join(c.segments, "/")
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
"github.com/ucan-wg/go-ucan/capability/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTop(t *testing.T) {
|
func TestTop(t *testing.T) {
|
||||||
@@ -13,66 +13,73 @@ func TestTop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidCommand(t *testing.T) {
|
func TestIsValidCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("succeeds when", func(t *testing.T) {
|
t.Run("succeeds when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range validTestcases(t) {
|
for _, testcase := range validTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
require.True(t, command.IsValid(testcase.inp))
|
require.True(t, command.IsValid(testcase.inp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails when", func(t *testing.T) {
|
t.Run("fails when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range invalidTestcases(t) {
|
for _, testcase := range invalidTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
require.False(t, command.IsValid(testcase.inp))
|
require.False(t, command.IsValid(testcase.inp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
require.Equal(t, command.Top(), command.New())
|
|
||||||
require.Equal(t, "/foo", command.New("foo").String())
|
|
||||||
require.Equal(t, "/foo/bar", command.New("foo", "bar").String())
|
|
||||||
require.Equal(t, "/foo/bar/baz", command.New("foo", "bar/baz").String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseCommand(t *testing.T) {
|
func TestParseCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
t.Run("succeeds when", func(t *testing.T) {
|
t.Run("succeeds when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range validTestcases(t) {
|
for _, testcase := range validTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
cmd, err := command.Parse("/elem0/elem1/elem2")
|
cmd, err := command.Parse("/elem0/elem1/elem2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails when", func(t *testing.T) {
|
t.Run("fails when", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, testcase := range invalidTestcases(t) {
|
for _, testcase := range invalidTestcases(t) {
|
||||||
|
testcase := testcase
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
cmd, err := command.Parse(testcase.inp)
|
cmd, err := command.Parse(testcase.inp)
|
||||||
require.ErrorIs(t, err, testcase.err)
|
require.ErrorIs(t, err, testcase.err)
|
||||||
require.Zero(t, cmd)
|
require.Nil(t, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEquality(t *testing.T) {
|
|
||||||
require.True(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/baz"))
|
|
||||||
require.False(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/bazz"))
|
|
||||||
require.False(t, command.MustParse("/foo/bar") == command.MustParse("/foo/bar/baz"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJoin(t *testing.T) {
|
|
||||||
require.Equal(t, "/foo", command.Top().Join("foo").String())
|
|
||||||
require.Equal(t, "/foo/bar", command.Top().Join("foo/bar").String())
|
|
||||||
require.Equal(t, "/foo/bar", command.Top().Join("foo", "bar").String())
|
|
||||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo/bar").String())
|
|
||||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String())
|
|
||||||
}
|
|
||||||
|
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
inp string
|
inp string
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||||
@@ -61,7 +61,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return negation{statement: statement}, nil
|
return Not(statement), nil
|
||||||
|
|
||||||
case KindAnd, KindOr:
|
case KindAnd, KindOr:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
@@ -93,11 +93,11 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
if pattern.Kind() != datamodel.Kind_String {
|
if pattern.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(combinePath(path, op, 2))
|
return nil, ErrNotAString(combinePath(path, op, 2))
|
||||||
}
|
}
|
||||||
g, err := parseGlob(must.String(pattern))
|
res, err := Like(sel, must.String(pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
||||||
}
|
}
|
||||||
return wildcard{selector: sel, pattern: g}, nil
|
return res, nil
|
||||||
|
|
||||||
case KindAll, KindAny:
|
case KindAll, KindAny:
|
||||||
sel, err := arg2AsSelector(op)
|
sel, err := arg2AsSelector(op)
|
||||||
@@ -232,7 +232,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
|
err = listBuilder.AssembleValue().AssignString(statement.pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -16,9 +16,7 @@ func TestIpldRoundTrip(t *testing.T) {
|
|||||||
["any", ".tags",
|
["any", ".tags",
|
||||||
["or", [
|
["or", [
|
||||||
["==", ".", "news"],
|
["==", ".", "news"],
|
||||||
["==", ".", "press"]
|
["==", ".", "press"]]]
|
||||||
]]
|
|
||||||
]
|
|
||||||
]`
|
]`
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
// Package literal holds a collection of functions to create IPLD types to use in policies, selector and args.
|
|
||||||
package literal
|
package literal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"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/ipld/go-ipld-prime/node/basicnode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func Node(n ipld.Node) ipld.Node {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func Link(cid ipld.Link) ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Link.NewBuilder()
|
||||||
|
nb.AssignLink(cid)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
func Bool(val bool) ipld.Node {
|
func Bool(val bool) ipld.Node {
|
||||||
nb := basicnode.Prototype.Bool.NewBuilder()
|
nb := basicnode.Prototype.Bool.NewBuilder()
|
||||||
nb.AssignBool(val)
|
nb.AssignBool(val)
|
||||||
@@ -38,16 +45,6 @@ func Bytes(val []byte) ipld.Node {
|
|||||||
return nb.Build()
|
return nb.Build()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Link(link ipld.Link) ipld.Node {
|
|
||||||
nb := basicnode.Prototype.Link.NewBuilder()
|
|
||||||
nb.AssignLink(link)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func LinkCid(cid cid.Cid) ipld.Node {
|
|
||||||
return Link(cidlink.Link{Cid: cid})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Null() ipld.Node {
|
func Null() ipld.Node {
|
||||||
nb := basicnode.Prototype.Any.NewBuilder()
|
nb := basicnode.Prototype.Any.NewBuilder()
|
||||||
nb.AssignNull()
|
nb.AssignNull()
|
||||||
164
capability/policy/match.go
Normal file
164
capability/policy/match.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match determines if the IPLD node matches the policy document.
|
||||||
|
func Match(policy Policy, node ipld.Node) bool {
|
||||||
|
for _, stmt := range policy {
|
||||||
|
ok := matchStatement(stmt, node)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchStatement(statement Statement, node ipld.Node) bool {
|
||||||
|
switch statement.Kind() {
|
||||||
|
case KindEqual:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return datamodel.DeepEqual(s.value, one)
|
||||||
|
}
|
||||||
|
case KindGreaterThan:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, gt)
|
||||||
|
}
|
||||||
|
case KindGreaterThanOrEqual:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, gte)
|
||||||
|
}
|
||||||
|
case KindLessThan:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, lt)
|
||||||
|
}
|
||||||
|
case KindLessThanOrEqual:
|
||||||
|
if s, ok := statement.(equality); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isOrdered(s.value, one, lte)
|
||||||
|
}
|
||||||
|
case KindNot:
|
||||||
|
if s, ok := statement.(negation); ok {
|
||||||
|
return !matchStatement(s.statement, node)
|
||||||
|
}
|
||||||
|
case KindAnd:
|
||||||
|
if s, ok := statement.(connective); ok {
|
||||||
|
for _, cs := range s.statements {
|
||||||
|
r := matchStatement(cs, node)
|
||||||
|
if !r {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case KindOr:
|
||||||
|
if s, ok := statement.(connective); ok {
|
||||||
|
if len(s.statements) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, cs := range s.statements {
|
||||||
|
r := matchStatement(cs, node)
|
||||||
|
if r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case KindLike:
|
||||||
|
if s, ok := statement.(wildcard); ok {
|
||||||
|
one, _, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || one == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, err := one.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.glob.Match(v)
|
||||||
|
}
|
||||||
|
case KindAll:
|
||||||
|
if s, ok := statement.(quantifier); ok {
|
||||||
|
_, many, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || many == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, n := range many {
|
||||||
|
ok := matchStatement(s.statement, n)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case KindAny:
|
||||||
|
if s, ok := statement.(quantifier); ok {
|
||||||
|
// FIXME: line below return a single node, not many
|
||||||
|
_, many, err := selector.Select(s.selector, node)
|
||||||
|
if err != nil || many == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, n := range many {
|
||||||
|
ok := matchStatement(s.statement, n)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||||
|
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||||
|
a := must.Int(actual)
|
||||||
|
b := must.Int(expected)
|
||||||
|
return satisfies(cmp.Compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
|
||||||
|
a, err := actual.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("extracting node float: %w", err))
|
||||||
|
}
|
||||||
|
b, err := expected.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("extracting selector float: %w", err))
|
||||||
|
}
|
||||||
|
return satisfies(cmp.Compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func gt(order int) bool { return order == 1 }
|
||||||
|
func gte(order int) bool { return order == 0 || order == 1 }
|
||||||
|
func lt(order int) bool { return order == -1 }
|
||||||
|
func lte(order int) bool { return order == 0 || order == -1 }
|
||||||
@@ -2,7 +2,6 @@ package policy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@@ -12,7 +11,8 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
"github.com/ucan-wg/go-ucan/capability/policy/literal"
|
||||||
|
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMatch(t *testing.T) {
|
func TestMatch(t *testing.T) {
|
||||||
@@ -23,16 +23,16 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignString("test")
|
nb.AssignString("test")
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.String("test")))
|
pol := Policy{Equal(selector.MustParse("."), literal.String("test"))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("test2")))
|
pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Int(138)))
|
pol = Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -42,16 +42,16 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Int(138)))
|
pol := Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,16 +61,16 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignFloat(1.138)
|
nb.AssignFloat(1.138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Float(1.138)))
|
pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -83,16 +83,16 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignLink(l0)
|
nb.AssignLink(l0)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,20 +105,20 @@ func TestMatch(t *testing.T) {
|
|||||||
ma.Finish()
|
ma.Finish()
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -130,12 +130,12 @@ func TestMatch(t *testing.T) {
|
|||||||
la.Finish()
|
la.Finish()
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -147,8 +147,8 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -158,12 +158,12 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -173,8 +173,8 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignFloat(1.38)
|
nb.AssignFloat(1.38)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -184,12 +184,12 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignFloat(1.38)
|
nb.AssignFloat(1.38)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -199,8 +199,8 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -210,12 +210,12 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -226,12 +226,12 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignBool(false)
|
nb.AssignBool(false)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -241,26 +241,26 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(
|
pol := Policy{
|
||||||
And(
|
And(
|
||||||
GreaterThan(".", literal.Int(1)),
|
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
||||||
LessThan(".", literal.Int(1138)),
|
LessThan(selector.MustParse("."), literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(
|
pol = Policy{
|
||||||
And(
|
And(
|
||||||
GreaterThan(".", literal.Int(1)),
|
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
||||||
Equal(".", literal.Int(1138)),
|
Equal(selector.MustParse("."), literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(And())
|
pol = Policy{And()}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -270,26 +270,26 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignInt(138)
|
nb.AssignInt(138)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(
|
pol := Policy{
|
||||||
Or(
|
Or(
|
||||||
GreaterThan(".", literal.Int(138)),
|
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
||||||
LessThan(".", literal.Int(1138)),
|
LessThan(selector.MustParse("."), literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
ok := pol.Match(nd)
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(
|
pol = Policy{
|
||||||
Or(
|
Or(
|
||||||
GreaterThan(".", literal.Int(138)),
|
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
||||||
Equal(".", literal.Int(1138)),
|
Equal(selector.MustParse("."), literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Or())
|
pol = Policy{Or()}
|
||||||
ok = pol.Match(nd)
|
ok = Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -309,8 +309,11 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignString(s)
|
nb.AssignString(s)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Like(".", pattern))
|
statement, err := Like(selector.MustParse("."), pattern)
|
||||||
ok := pol.Match(nd)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pol := Policy{statement}
|
||||||
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
})
|
})
|
||||||
}(s)
|
}(s)
|
||||||
@@ -330,8 +333,11 @@ func TestMatch(t *testing.T) {
|
|||||||
nb.AssignString(s)
|
nb.AssignString(s)
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Like(".", pattern))
|
statement, err := Like(selector.MustParse("."), pattern)
|
||||||
ok := pol.Match(nd)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pol := Policy{statement}
|
||||||
|
ok := Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
}(s)
|
}(s)
|
||||||
@@ -361,12 +367,22 @@ func TestMatch(t *testing.T) {
|
|||||||
la.Finish()
|
la.Finish()
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
pol := Policy{
|
||||||
ok := pol.Match(nd)
|
All(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(2)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
pol = Policy{
|
||||||
ok = pol.Match(nd)
|
All(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(20)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -382,12 +398,22 @@ func TestMatch(t *testing.T) {
|
|||||||
la.Finish()
|
la.Finish()
|
||||||
nd := nb.Build()
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
pol := Policy{
|
||||||
ok := pol.Match(nd)
|
Any(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(60)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok := Match(pol, nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
pol = Policy{
|
||||||
ok = pol.Match(nd)
|
Any(
|
||||||
|
selector.MustParse(".[]"),
|
||||||
|
GreaterThan(selector.MustParse(".value"), literal.Int(100)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
ok = Match(pol, nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -406,7 +432,7 @@ func TestPolicyExamples(t *testing.T) {
|
|||||||
|
|
||||||
pol, err := FromDagJson(policy)
|
pol, err := FromDagJson(policy)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return pol.Match(data)
|
return Match(pol, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("And", func(t *testing.T) {
|
t.Run("And", func(t *testing.T) {
|
||||||
@@ -464,152 +490,3 @@ func TestPolicyExamples(t *testing.T) {
|
|||||||
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
|
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func FuzzMatch(f *testing.F) {
|
|
||||||
// Policy + Data examples
|
|
||||||
f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`))
|
|
||||||
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
|
|
||||||
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
|
|
||||||
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
|
|
||||||
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
|
|
||||||
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
|
|
||||||
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))
|
|
||||||
f.Add([]byte(`[["and", [["==", ".role", "admin"], [">=", ".experience", 5]]]]`), []byte(`{"role": "admin", "experience": 6}`))
|
|
||||||
f.Add([]byte(`[["or", [["==", ".department", "HR"], ["==", ".department", "Finance"]]]]`), []byte(`{"department": "HR"}`))
|
|
||||||
f.Add([]byte(`[["like", ".email", "*@company.com"]]`), []byte(`{"email": "user@company.com"}`))
|
|
||||||
f.Add([]byte(`[["all", ".projects", [">", ".budget", 10000]]]`), []byte(`{"projects": [{"budget": 15000}, {"budget": 8000}]}`))
|
|
||||||
f.Add([]byte(`[["any", ".skills", ["==", ".", "Go"]]]`), []byte(`{"skills": ["Go", "Python", "JavaScript"]}`))
|
|
||||||
f.Add(
|
|
||||||
[]byte(`[["and", [
|
|
||||||
["==", ".name", "Bob"],
|
|
||||||
["or", [[">", ".age", 25],["==", ".status", "active"]]],
|
|
||||||
["all", ".tasks", ["==", ".completed", true]]
|
|
||||||
]]]`),
|
|
||||||
[]byte(`{
|
|
||||||
"name": "Bob",
|
|
||||||
"age": 26,
|
|
||||||
"status": "active",
|
|
||||||
"tasks": [{"completed": true}, {"completed": true}, {"completed": false}]
|
|
||||||
}`),
|
|
||||||
)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) {
|
|
||||||
policyNode, err := ipld.Decode(policyBytes, dagjson.Decode)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
dataNode, err := ipld.Decode(dataBytes, dagjson.Decode)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
// policy node -> policy object
|
|
||||||
policy, err := FromIPLD(policyNode)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
policy.Match(dataNode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPolicyFilter(t *testing.T) {
|
|
||||||
pol := MustConstruct(
|
|
||||||
Any(".http", And(
|
|
||||||
Equal(".method", literal.String("GET")),
|
|
||||||
Equal(".path", literal.String("/foo")),
|
|
||||||
)),
|
|
||||||
Equal(".http", literal.String("foobar")),
|
|
||||||
All(".jsonrpc.foo", Or(
|
|
||||||
Not(Equal(".bar", literal.String("foo"))),
|
|
||||||
Equal(".", literal.String("foo")),
|
|
||||||
Like(".boo", "abcd"),
|
|
||||||
Like(".boo", "*bcd"),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
path string
|
|
||||||
expected Policy
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
path: "http",
|
|
||||||
expected: MustConstruct(
|
|
||||||
Any(".http", And(
|
|
||||||
Equal(".method", literal.String("GET")),
|
|
||||||
Equal(".path", literal.String("/foo")),
|
|
||||||
)),
|
|
||||||
Equal(".http", literal.String("foobar")),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "http,method",
|
|
||||||
expected: MustConstruct(
|
|
||||||
Any(".http", And(
|
|
||||||
Equal(".method", literal.String("GET")),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "http,path",
|
|
||||||
expected: MustConstruct(
|
|
||||||
Any(".http", And(
|
|
||||||
Equal(".path", literal.String("/foo")),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "http,foo",
|
|
||||||
expected: Policy{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "jsonrpc",
|
|
||||||
expected: MustConstruct(
|
|
||||||
All(".jsonrpc.foo", Or(
|
|
||||||
Not(Equal(".bar", literal.String("foo"))),
|
|
||||||
Equal(".", literal.String("foo")),
|
|
||||||
Like(".boo", "abcd"),
|
|
||||||
Like(".boo", "*bcd"),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "jsonrpc,baz",
|
|
||||||
expected: Policy{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "jsonrpc,foo",
|
|
||||||
expected: MustConstruct(
|
|
||||||
All(".jsonrpc.foo", Or(
|
|
||||||
Not(Equal(".bar", literal.String("foo"))),
|
|
||||||
Equal(".", literal.String("foo")),
|
|
||||||
Like(".boo", "abcd"),
|
|
||||||
Like(".boo", "*bcd"),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "jsonrpc,foo,bar",
|
|
||||||
expected: MustConstruct(
|
|
||||||
All(".jsonrpc.foo", Or(
|
|
||||||
Not(Equal(".bar", literal.String("foo"))),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "jsonrpc,foo,boo",
|
|
||||||
expected: MustConstruct(
|
|
||||||
All(".jsonrpc.foo", Or(
|
|
||||||
Like(".boo", "abcd"),
|
|
||||||
Like(".boo", "*bcd"),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.path, func(t *testing.T) {
|
|
||||||
res := pol.Filter(strings.Split(tc.path, ",")...)
|
|
||||||
require.Equal(t, tc.expected.String(), res.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
125
capability/policy/policy.go
Normal file
125
capability/policy/policy.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gobwas/glob"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KindEqual = "==" // implemented by equality
|
||||||
|
KindGreaterThan = ">" // implemented by equality
|
||||||
|
KindGreaterThanOrEqual = ">=" // implemented by equality
|
||||||
|
KindLessThan = "<" // implemented by equality
|
||||||
|
KindLessThanOrEqual = "<=" // implemented by equality
|
||||||
|
KindNot = "not" // implemented by negation
|
||||||
|
KindAnd = "and" // implemented by connective
|
||||||
|
KindOr = "or" // implemented by connective
|
||||||
|
KindLike = "like" // implemented by wildcard
|
||||||
|
KindAll = "all" // implemented by quantifier
|
||||||
|
KindAny = "any" // implemented by quantifier
|
||||||
|
)
|
||||||
|
|
||||||
|
type Policy []Statement
|
||||||
|
|
||||||
|
type Statement interface {
|
||||||
|
Kind() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type equality struct {
|
||||||
|
kind string
|
||||||
|
selector selector.Selector
|
||||||
|
value ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e equality) Kind() string {
|
||||||
|
return e.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func Equal(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{kind: KindEqual, selector: selector, value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{kind: KindGreaterThan, selector: selector, value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{kind: KindGreaterThanOrEqual, selector: selector, value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{kind: KindLessThan, selector: selector, value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||||
|
return equality{kind: KindLessThanOrEqual, selector: selector, value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
type negation struct {
|
||||||
|
statement Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n negation) Kind() string {
|
||||||
|
return KindNot
|
||||||
|
}
|
||||||
|
|
||||||
|
func Not(stmt Statement) Statement {
|
||||||
|
return negation{statement: stmt}
|
||||||
|
}
|
||||||
|
|
||||||
|
type connective struct {
|
||||||
|
kind string
|
||||||
|
statements []Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c connective) Kind() string {
|
||||||
|
return c.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func And(stmts ...Statement) Statement {
|
||||||
|
return connective{kind: KindAnd, statements: stmts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Or(stmts ...Statement) Statement {
|
||||||
|
return connective{kind: KindOr, statements: stmts}
|
||||||
|
}
|
||||||
|
|
||||||
|
type wildcard struct {
|
||||||
|
selector selector.Selector
|
||||||
|
pattern string
|
||||||
|
glob glob.Glob // not serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n wildcard) Kind() string {
|
||||||
|
return KindLike
|
||||||
|
}
|
||||||
|
|
||||||
|
func Like(selector selector.Selector, pattern string) (Statement, error) {
|
||||||
|
g, err := glob.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return wildcard{selector: selector, pattern: pattern, glob: g}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type quantifier struct {
|
||||||
|
kind string
|
||||||
|
selector selector.Selector
|
||||||
|
statement Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n quantifier) Kind() string {
|
||||||
|
return n.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func All(selector selector.Selector, statement Statement) Statement {
|
||||||
|
return quantifier{kind: KindAll, selector: selector, statement: statement}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Any(selector selector.Selector, statement Statement) Statement {
|
||||||
|
return quantifier{kind: KindAny, selector: selector, statement: statement}
|
||||||
|
}
|
||||||
@@ -2,17 +2,8 @@ package selector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
identity = Selector{segment{str: ".", identity: true}}
|
|
||||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
|
||||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
|
||||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Parse(str string) (Selector, error) {
|
func Parse(str string) (Selector, error) {
|
||||||
@@ -22,9 +13,6 @@ func Parse(str string) (Selector, error) {
|
|||||||
if string(str[0]) != "." {
|
if string(str[0]) != "." {
|
||||||
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
||||||
}
|
}
|
||||||
if str == "." {
|
|
||||||
return identity, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
col := 0
|
col := 0
|
||||||
var sel Selector
|
var sel Selector
|
||||||
@@ -39,9 +27,9 @@ func Parse(str string) (Selector, error) {
|
|||||||
if len(sel) > 0 && sel[len(sel)-1].Identity() {
|
if len(sel) > 0 && sel[len(sel)-1].Identity() {
|
||||||
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
|
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
|
||||||
}
|
}
|
||||||
sel = append(sel, segment{str: ".", identity: true})
|
sel = append(sel, Identity)
|
||||||
case "[]":
|
case "[]":
|
||||||
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
|
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
|
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
|
||||||
lookup := seg[1 : len(seg)-1]
|
lookup := seg[1 : len(seg)-1]
|
||||||
@@ -103,10 +91,10 @@ func tokenize(str string) []string {
|
|||||||
ctx := ""
|
ctx := ""
|
||||||
|
|
||||||
for col < len(str) {
|
for col < len(str) {
|
||||||
char, size := utf8.DecodeRuneInString(str[col:])
|
char := string(str[col])
|
||||||
|
|
||||||
if char == '"' && (col == 0 || str[col-1] != '\\') {
|
if char == "\"" && string(str[col-1]) != "\\" {
|
||||||
col += size
|
col++
|
||||||
if ctx == "\"" {
|
if ctx == "\"" {
|
||||||
ctx = ""
|
ctx = ""
|
||||||
} else {
|
} else {
|
||||||
@@ -116,17 +104,17 @@ func tokenize(str string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ctx == "\"" {
|
if ctx == "\"" {
|
||||||
col += size
|
col++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if char == '.' || char == '[' {
|
if char == "." || char == "[" {
|
||||||
if ofs < col {
|
if ofs < col {
|
||||||
toks = append(toks, str[ofs:col])
|
toks = append(toks, str[ofs:col])
|
||||||
}
|
}
|
||||||
ofs = col
|
ofs = col
|
||||||
}
|
}
|
||||||
col += size
|
col++
|
||||||
}
|
}
|
||||||
|
|
||||||
if ofs < col && ctx != "\"" {
|
if ofs < col && ctx != "\"" {
|
||||||
259
capability/policy/selector/selector.go
Normal file
259
capability/policy/selector/selector.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Selector describes a UCAN policy selector, as specified here:
|
||||||
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
||||||
|
type Selector []segment
|
||||||
|
|
||||||
|
func (s Selector) String() string {
|
||||||
|
var res strings.Builder
|
||||||
|
for _, seg := range s {
|
||||||
|
res.WriteString(seg.String())
|
||||||
|
}
|
||||||
|
return res.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var Identity = segment{".", true, false, false, nil, "", 0}
|
||||||
|
|
||||||
|
var (
|
||||||
|
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
||||||
|
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
||||||
|
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type segment struct {
|
||||||
|
str string
|
||||||
|
identity bool
|
||||||
|
optional bool
|
||||||
|
iterator bool
|
||||||
|
slice []int
|
||||||
|
field string
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the segment's string representation.
|
||||||
|
func (s segment) String() string {
|
||||||
|
return s.str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity flags that this selector is the identity selector.
|
||||||
|
func (s segment) Identity() bool {
|
||||||
|
return s.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional flags that this selector is optional.
|
||||||
|
func (s segment) Optional() bool {
|
||||||
|
return s.optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator flags that this selector is an iterator segment.
|
||||||
|
func (s segment) Iterator() bool {
|
||||||
|
return s.iterator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice flags that this segment targets a range of a slice.
|
||||||
|
func (s segment) Slice() []int {
|
||||||
|
return s.slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field is the name of a field in a struct/map.
|
||||||
|
func (s segment) Field() string {
|
||||||
|
return s.field
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index is an index of a slice.
|
||||||
|
func (s segment) Index() int {
|
||||||
|
return s.index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select uses a selector to extract an IPLD node or set of nodes from the
|
||||||
|
// passed subject node.
|
||||||
|
func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) {
|
||||||
|
return resolve(sel, subject, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) {
|
||||||
|
cur := subject
|
||||||
|
for i, seg := range sel {
|
||||||
|
if seg.Identity() {
|
||||||
|
continue
|
||||||
|
} else if seg.Iterator() {
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
||||||
|
var many []ipld.Node
|
||||||
|
it := cur.ListIterator()
|
||||||
|
for {
|
||||||
|
if it.Done() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
k, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%d", k)
|
||||||
|
o, m, err := resolve(sel[i+1:], v, append(at[:], key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m != nil {
|
||||||
|
many = append(many, m...)
|
||||||
|
} else {
|
||||||
|
many = append(many, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, many, nil
|
||||||
|
} else if cur != nil && cur.Kind() == datamodel.Kind_Map {
|
||||||
|
var many []ipld.Node
|
||||||
|
it := cur.MapIterator()
|
||||||
|
for {
|
||||||
|
if it.Done() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
k, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _ := k.AsString()
|
||||||
|
o, m, err := resolve(sel[i+1:], v, append(at[:], key))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if m != nil {
|
||||||
|
many = append(many, m...)
|
||||||
|
} else {
|
||||||
|
many = append(many, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, many, nil
|
||||||
|
} else if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if seg.Field() != "" {
|
||||||
|
at = append(at, seg.Field())
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_Map {
|
||||||
|
n, err := cur.LookupByString(seg.Field())
|
||||||
|
if err != nil {
|
||||||
|
if isMissing(err) {
|
||||||
|
if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = n
|
||||||
|
} else if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
||||||
|
}
|
||||||
|
} else if seg.Slice() != nil {
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
||||||
|
return nil, nil, newResolutionError("list slice selection not yet implemented", at)
|
||||||
|
} else if cur != nil && cur.Kind() == datamodel.Kind_Bytes {
|
||||||
|
return nil, nil, newResolutionError("bytes slice selection not yet implemented", at)
|
||||||
|
} else if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("can not index: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
at = append(at, fmt.Sprintf("%d", seg.Index()))
|
||||||
|
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
||||||
|
idx := int64(seg.Index())
|
||||||
|
if idx < 0 {
|
||||||
|
idx = cur.Length() + idx
|
||||||
|
}
|
||||||
|
if idx < 0 {
|
||||||
|
// necessary until https://github.com/ipld/go-ipld-prime/pull/571
|
||||||
|
// after, isMissing() below will work
|
||||||
|
// TODO: remove
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
||||||
|
}
|
||||||
|
n, err := cur.LookupByIndex(idx)
|
||||||
|
if err != nil {
|
||||||
|
if isMissing(err) {
|
||||||
|
if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cur = n
|
||||||
|
} else if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cur, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func kindString(n datamodel.Node) string {
|
||||||
|
if n == nil {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
return n.Kind().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMissing(err error) bool {
|
||||||
|
if _, ok := err.(datamodel.ErrNotExists); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := err.(schema.ErrNoSuchField); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := err.(schema.ErrInvalidKey); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolutionerr struct {
|
||||||
|
msg string
|
||||||
|
at []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) Name() string {
|
||||||
|
return "ResolutionError"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) Message() string {
|
||||||
|
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) At() []string {
|
||||||
|
return r.at
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) Error() string {
|
||||||
|
return r.Message()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResolutionError(message string, at []string) error {
|
||||||
|
return resolutionerr{message, at}
|
||||||
|
}
|
||||||
@@ -313,7 +313,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".")
|
sel, err := Parse(".")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, one)
|
require.NotEmpty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -328,7 +328,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".name.first")
|
sel, err := Parse(".name.first")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, one)
|
require.NotEmpty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -338,7 +338,7 @@ func TestSelect(t *testing.T) {
|
|||||||
name := must.String(one)
|
name := must.String(one)
|
||||||
require.Equal(t, alice.Name.First, name)
|
require.Equal(t, alice.Name.First, name)
|
||||||
|
|
||||||
one, many, err = sel.Select(bnode)
|
one, many, err = Select(sel, bnode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, one)
|
require.NotEmpty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -353,7 +353,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".name.middle?")
|
sel, err := Parse(".name.middle?")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, one)
|
require.NotEmpty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -363,7 +363,7 @@ func TestSelect(t *testing.T) {
|
|||||||
name := must.String(one)
|
name := must.String(one)
|
||||||
require.Equal(t, *alice.Name.Middle, name)
|
require.Equal(t, *alice.Name.Middle, name)
|
||||||
|
|
||||||
one, many, err = sel.Select(bnode)
|
one, many, err = Select(sel, bnode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, one)
|
require.Empty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -373,7 +373,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".name.foo")
|
sel, err := Parse(".name.foo")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Empty(t, one)
|
require.Empty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -387,7 +387,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".name.foo?")
|
sel, err := Parse(".name.foo?")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, one)
|
require.Empty(t, one)
|
||||||
require.Empty(t, many)
|
require.Empty(t, many)
|
||||||
@@ -397,7 +397,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".interests[]")
|
sel, err := Parse(".interests[]")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, one)
|
require.Empty(t, one)
|
||||||
require.NotEmpty(t, many)
|
require.NotEmpty(t, many)
|
||||||
@@ -417,7 +417,7 @@ func TestSelect(t *testing.T) {
|
|||||||
sel, err := Parse(".interests[0][]")
|
sel, err := Parse(".interests[0][]")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
one, many, err := sel.Select(anode)
|
one, many, err := Select(sel, anode)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Empty(t, one)
|
require.Empty(t, one)
|
||||||
require.NotEmpty(t, many)
|
require.NotEmpty(t, many)
|
||||||
@@ -431,32 +431,6 @@ func TestSelect(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMatch(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
sel string
|
|
||||||
path []string
|
|
||||||
want bool
|
|
||||||
remaining []string
|
|
||||||
}{
|
|
||||||
{sel: ".foo.bar", path: []string{"foo", "bar"}, want: true, remaining: []string{}},
|
|
||||||
{sel: ".foo.bar", path: []string{"foo"}, want: true, remaining: []string{}},
|
|
||||||
{sel: ".foo.bar", path: []string{"foo", "bar", "baz"}, want: true, remaining: []string{"baz"}},
|
|
||||||
{sel: ".foo.bar", path: []string{"foo", "faa"}, want: false},
|
|
||||||
{sel: ".foo.[]", path: []string{"foo", "faa"}, want: false},
|
|
||||||
{sel: ".foo.[]", path: []string{"foo"}, want: true, remaining: []string{}},
|
|
||||||
{sel: ".foo.bar?", path: []string{"foo"}, want: true, remaining: []string{}},
|
|
||||||
{sel: ".foo.bar?", path: []string{"foo", "bar"}, want: true, remaining: []string{}},
|
|
||||||
{sel: ".foo.bar?", path: []string{"foo", "baz"}, want: false},
|
|
||||||
} {
|
|
||||||
t.Run(tc.sel, func(t *testing.T) {
|
|
||||||
sel := MustParse(tc.sel)
|
|
||||||
res, remain := sel.MatchPath(tc.path...)
|
|
||||||
require.Equal(t, tc.want, res)
|
|
||||||
require.EqualValues(t, tc.remaining, remain)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzParse(f *testing.F) {
|
func FuzzParse(f *testing.F) {
|
||||||
selectorCorpus := []string{
|
selectorCorpus := []string{
|
||||||
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
||||||
@@ -520,6 +494,6 @@ func FuzzParseAndSelect(f *testing.F) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// look for panic()
|
// look for panic()
|
||||||
_, _, _ = sel.Select(node)
|
_, _, _ = Select(sel, node)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
"github.com/ucan-wg/go-ucan/capability/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSupported Forms runs tests against the Selector according to the
|
// TestSupported Forms runs tests against the Selector according to the
|
||||||
@@ -30,14 +30,14 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
||||||
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `[]`},
|
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `()`},
|
||||||
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
|
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
|
||||||
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
||||||
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
||||||
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
||||||
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
||||||
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
||||||
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}}`, Output: `0`},
|
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}`, Output: `0`},
|
||||||
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
@@ -52,7 +52,7 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := sel.Select(makeNode(t, tc.Input))
|
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
|
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := sel.Select(makeNode(t, tc.Input))
|
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// TODO: should Select return a single node which is sometimes a list or null?
|
// TODO: should Select return a single node which is sometimes a list or null?
|
||||||
// require.Equal(t, datamodel.Null, node)
|
// require.Equal(t, datamodel.Null, node)
|
||||||
@@ -124,7 +124,7 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := sel.Select(makeNode(t, tc.Input))
|
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, node)
|
assert.Nil(t, node)
|
||||||
assert.Empty(t, nodes)
|
assert.Empty(t, nodes)
|
||||||
46
delegation/delegatiom_options.go
Normal file
46
delegation/delegatiom_options.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
// Code generated by github.com/selesy/go-options. DO NOT EDIT.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Option func(c *config) error
|
||||||
|
|
||||||
|
func newConfig(options ...Option) (config, error) {
|
||||||
|
var c config
|
||||||
|
err := applyConfigOptions(&c, options...)
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyConfigOptions(c *config, options ...Option) error {
|
||||||
|
for _, o := range options {
|
||||||
|
if err := o(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNotBefore(o *time.Time) Option {
|
||||||
|
return func(c *config) error {
|
||||||
|
c.NotBefore = o
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
225
delegation/delegation.go
Normal file
225
delegation/delegation.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"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"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/envelope"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Tag = "ucan/dlg@1.0.0-rc.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
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/ipld/go-ipld-prime/datamodel
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Expiration *time.Time
|
||||||
|
Meta map[string]datamodel.Node
|
||||||
|
NotBefore *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(privKey crypto.PrivKey, aud did.DID, sub *did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, opts ...Option) (*Delegation, error) {
|
||||||
|
cfg, err := newConfig(opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer, err := did.FromPrivKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !aud.Defined() {
|
||||||
|
return nil, fmt.Errorf("%w: %s", token.ErrMissingRequiredDID, "aud")
|
||||||
|
}
|
||||||
|
audience := aud.String()
|
||||||
|
|
||||||
|
var subject *string
|
||||||
|
if sub != nil {
|
||||||
|
s := sub.String()
|
||||||
|
subject = &s
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, err := pol.ToIPLD()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
n := int(cfg.NotBefore.Unix())
|
||||||
|
notBefore = &n
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiration *int
|
||||||
|
if cfg.Expiration != nil {
|
||||||
|
e := int(cfg.Expiration.Unix())
|
||||||
|
expiration = &e
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn := &token.Token{
|
||||||
|
Issuer: issuer.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 Root(privKey crypto.PrivKey, aud did.DID, cmd *command.Command, pol policy.Policy, nonce []byte, opts ...Option) (*Delegation, error) {
|
||||||
|
sub, err := did.FromPrivKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return New(privKey, aud, &sub, cmd, pol, nonce, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
package delegation_test
|
package delegation_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/libp2p/go-libp2p/core/crypto"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gotest.tools/v3/golden"
|
"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"
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
"gotest.tools/v3/golden"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -63,9 +65,6 @@ const (
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
`
|
`
|
||||||
|
|
||||||
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
|
||||||
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConstructors(t *testing.T) {
|
func TestConstructors(t *testing.T) {
|
||||||
@@ -84,20 +83,18 @@ func TestConstructors(t *testing.T) {
|
|||||||
pol, err := policy.FromDagJson(subjectPol)
|
pol, err := policy.FromDagJson(subjectPol)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z")
|
exp := time.Time{}
|
||||||
require.NoError(t, err)
|
|
||||||
|
meta := map[string]datamodel.Node{
|
||||||
|
"foo": basicnode.NewString("fooo"),
|
||||||
|
"bar": basicnode.NewString("barr"),
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("New", func(t *testing.T) {
|
t.Run("New", func(t *testing.T) {
|
||||||
tkn, err := delegation.New(privKey, aud, cmd, pol,
|
dlg, err := delegation.New(privKey, aud, &sub, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
|
||||||
delegation.WithNonce([]byte(nonce)),
|
|
||||||
delegation.WithSubject(sub),
|
|
||||||
delegation.WithExpiration(exp),
|
|
||||||
delegation.WithMeta("foo", "fooo"),
|
|
||||||
delegation.WithMeta("bar", "barr"),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(privKey)
|
data, err := dlg.ToDagJson()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Log(string(data))
|
t.Log(string(data))
|
||||||
@@ -108,15 +105,10 @@ func TestConstructors(t *testing.T) {
|
|||||||
t.Run("Root", func(t *testing.T) {
|
t.Run("Root", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tkn, err := delegation.Root(privKey, aud, cmd, pol,
|
dlg, err := delegation.Root(privKey, aud, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
|
||||||
delegation.WithNonce([]byte(nonce)),
|
|
||||||
delegation.WithExpiration(exp),
|
|
||||||
delegation.WithMeta("foo", "fooo"),
|
|
||||||
delegation.WithMeta("bar", "barr"),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(privKey)
|
data, err := dlg.ToDagJson()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Log(string(data))
|
t.Log(string(data))
|
||||||
@@ -125,7 +117,9 @@ func TestConstructors(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
func privKey(t *testing.T, privKeyCfg string) crypto.PrivKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -134,3 +128,22 @@ func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
|||||||
|
|
||||||
return privKey
|
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()
|
||||||
|
}
|
||||||
113
delegation/encoding.go
Normal file
113
delegation/encoding.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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/ucan-wg/go-ucan/internal/envelope"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode marshals a Delegation to 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||||
|
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Delegation, error) {
|
||||||
|
node, err := ipld.DecodeStreaming(r, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||||
|
func FromDagCborReader(r io.Reader) (*Delegation, error) {
|
||||||
|
return DecodeReader(r, dagcbor.Decode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagJson 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||||
|
func FromDagJsonReader(r io.Reader) (*Delegation, error) {
|
||||||
|
return DecodeReader(r, 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
|
||||||
|
}
|
||||||
101
delegation/encoding_test.go
Normal file
101
delegation/encoding_test.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
1
delegation/testdata/new.dagjson
vendored
Normal file
1
delegation/testdata/new.dagjson
vendored
Normal file
@@ -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"}}]
|
||||||
1
delegation/testdata/root.dagjson
vendored
Normal file
1
delegation/testdata/root.dagjson
vendored
Normal file
@@ -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"}}]
|
||||||
@@ -116,11 +116,3 @@ func Parse(str string) (DID, error) {
|
|||||||
buf = append(buf, suffix...)
|
buf = append(buf, suffix...)
|
||||||
return DID{str: string(buf), code: DIDCore}, nil
|
return DID{str: string(buf), code: DIDCore}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustParse(str string) DID {
|
|
||||||
did, err := Parse(str)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return did
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package did
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseDIDKey(t *testing.T) {
|
func TestParseDIDKey(t *testing.T) {
|
||||||
@@ -17,18 +15,6 @@ func TestParseDIDKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMustParseDIDKey(t *testing.T) {
|
|
||||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
d := MustParse(str)
|
|
||||||
require.Equal(t, str, d.String())
|
|
||||||
})
|
|
||||||
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
|
||||||
require.Panics(t, func() {
|
|
||||||
MustParse(str)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeDIDKey(t *testing.T) {
|
func TestDecodeDIDKey(t *testing.T) {
|
||||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
d0, err := Parse(str)
|
d0, err := Parse(str)
|
||||||
|
|||||||
5
doc.go
Normal file
5
doc.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Package ucan provides the core functionality required to grant and
|
||||||
|
// revoke privileges via [UCAN] tokens.
|
||||||
|
//
|
||||||
|
// [UCAN]: https://ucan.xyz
|
||||||
|
package ucan
|
||||||
17
go.mod
17
go.mod
@@ -1,15 +1,19 @@
|
|||||||
module github.com/ucan-wg/go-ucan
|
module github.com/ucan-wg/go-ucan
|
||||||
|
|
||||||
go 1.23
|
go 1.22.0
|
||||||
|
|
||||||
|
toolchain go1.22.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/ipfs/go-cid v0.4.1
|
github.com/ipfs/go-cid v0.4.1
|
||||||
github.com/ipld/go-ipld-prime v0.21.0
|
github.com/ipld/go-ipld-prime v0.21.0
|
||||||
github.com/libp2p/go-libp2p v0.36.3
|
github.com/libp2p/go-libp2p v0.36.2
|
||||||
github.com/multiformats/go-multibase v0.2.0
|
github.com/multiformats/go-multibase v0.2.0
|
||||||
github.com/multiformats/go-multicodec v0.9.0
|
github.com/multiformats/go-multicodec v0.9.0
|
||||||
github.com/multiformats/go-multihash v0.2.3
|
github.com/multiformats/go-multihash v0.2.3
|
||||||
github.com/multiformats/go-varint v0.0.7
|
github.com/multiformats/go-varint v0.0.7
|
||||||
|
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
gotest.tools/v3 v3.5.1
|
gotest.tools/v3 v3.5.1
|
||||||
)
|
)
|
||||||
@@ -17,7 +21,8 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/fatih/structtag v1.2.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
@@ -27,9 +32,11 @@ require (
|
|||||||
github.com/polydawn/refmt v0.89.0 // indirect
|
github.com/polydawn/refmt v0.89.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/mod v0.21.0 // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
golang.org/x/tools v0.25.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
27
go.sum
27
go.sum
@@ -6,11 +6,15 @@ github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5il
|
|||||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
|
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||||
|
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
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/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/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
||||||
@@ -27,8 +31,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||||
github.com/libp2p/go-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
|
github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U=
|
||||||
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
|
github.com/libp2p/go-libp2p v0.36.2/go.mod h1:XO3joasRE4Eup8yCTTP/+kX+g92mOgRaadk46LmPhHY=
|
||||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
@@ -54,6 +58,8 @@ github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX
|
|||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52 h1:poNWlojS+o3229ZuatLMzK9wFiLuLxo7O170Edggs0o=
|
||||||
|
github.com/selesy/go-options v0.0.0-20240912020512-ed2658318e52/go.mod h1:Cn8TrnJWCWd3dAmejFTpLN8tNVNKNoVVlZzL8ux5EWQ=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
@@ -71,18 +77,23 @@ golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
|||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
|
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
|
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||||
|
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
|||||||
54
internal/cmd/token/main.go
Normal file
54
internal/cmd/token/main.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const header = `// Code generated by internal/cmd/token - DO NOT EDIT.
|
||||||
|
|
||||||
|
package token
|
||||||
|
|
||||||
|
import "github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
slog.Info("Generating Go types for token.ipldsch")
|
||||||
|
|
||||||
|
if err := Run(); err != nil {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
slog.Error("Finished but failed to generate and write token_gen.go")
|
||||||
|
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Finished generating and writing token_gen.go")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run() error {
|
||||||
|
schema, err := os.ReadFile("token.ipldsch")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug(string(schema))
|
||||||
|
|
||||||
|
typeSystem, err := ipld.LoadSchemaBytes(schema)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(header)
|
||||||
|
|
||||||
|
if err := bindnode.ProduceGoTypes(buf, typeSystem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile("token_gen.go", buf.Bytes(), 0o600)
|
||||||
|
}
|
||||||
309
internal/envelope/envelope.go
Normal file
309
internal/envelope/envelope.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package envelope
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/token"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/varsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// [Envelope] is a signed enclosure for a UCAN v1 Token.
|
||||||
|
//
|
||||||
|
// While the types and functions in this package are not exported,
|
||||||
|
// the names used for types, fields, variables, etc generally use the
|
||||||
|
// names from the specification
|
||||||
|
//
|
||||||
|
// [Envelope]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
type Envelope struct {
|
||||||
|
signature []byte
|
||||||
|
sigPayload *sigPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates an Envelope containing a VarsigHeader and Signature for
|
||||||
|
// the data resulting from wrapping the provided Token in an IPLD
|
||||||
|
// datamodel.Node and encoding it using DAG-CBOR.
|
||||||
|
func New(privKey crypto.PrivKey, token *token.Token, tag string) (*Envelope, error) {
|
||||||
|
sigPayload, err := newSigPayload(privKey.Type(), token, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cbor, err := sigPayload.cbor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := privKey.Sign(cbor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Envelope{
|
||||||
|
signature: signature,
|
||||||
|
sigPayload: sigPayload,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap is syntactic sugar for creating an Envelope and wrapping it as an
|
||||||
|
// IPLD datamodel.Node in a single operation.
|
||||||
|
//
|
||||||
|
// Since the Envelope itself isn't returned, use this method only when
|
||||||
|
// the IPLD datamodel.Node is used directly. If the Envelope is also
|
||||||
|
// required, use New followed by Envelope.Wrap to avoid the need to
|
||||||
|
// unwrap the newly created datamodel.Node.
|
||||||
|
func Wrap(privKey crypto.PrivKey, token *token.Token, tag string) (datamodel.Node, error) {
|
||||||
|
env, err := New(privKey, token, tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return env.Wrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap attempts to crate an Envelope from a datamodel.Node
|
||||||
|
//
|
||||||
|
// There are lots of ways that this can fail and therefore there are
|
||||||
|
// an almost excessive number of check included here and while
|
||||||
|
// attempting to extract the token.Token from one of the inner IPLD
|
||||||
|
// nodes.
|
||||||
|
func Unwrap(node datamodel.Node) (*Envelope, error) {
|
||||||
|
signatureNode, err := node.LookupByIndex(0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := signatureNode.AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sigPayloadNode, err := node.LookupByIndex(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sigPayload, err := unwrapSigPayload(sigPayloadNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
envel := &Envelope{
|
||||||
|
signature: signature,
|
||||||
|
sigPayload: sigPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// SigPayload.
|
||||||
|
func (e *Envelope) Signature() []byte {
|
||||||
|
return e.signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag returns the key that's used to reference the TokenPayload within
|
||||||
|
// this Envelope.
|
||||||
|
func (e *Envelope) Tag() string {
|
||||||
|
return e.sigPayload.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenPayload returns the *token.Token enclosed within this Envelope.
|
||||||
|
func (e *Envelope) TokenPayload() *token.Token {
|
||||||
|
return e.sigPayload.tokenPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// VarsigHeader is an accessor that returns the [VarsigHeader] from the
|
||||||
|
// underlying [SigPayload] from the [Envelope].
|
||||||
|
//
|
||||||
|
// [Envelope]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
// [SigPayload]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
// [VarsigHeader]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
func (e *Envelope) VarsigHeader() []byte {
|
||||||
|
return e.sigPayload.varsigHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks that the [Envelope]'s signature is correct for the
|
||||||
|
// data created by encoding the SigPayload as DAG-CBOR and the public
|
||||||
|
// key passed as the only argument.
|
||||||
|
//
|
||||||
|
// Note that for Delegation and Invocation tokens, the public key
|
||||||
|
// is retrieved from the DID's method specific identifier for the
|
||||||
|
// Issuer field.
|
||||||
|
//
|
||||||
|
// [Envelope]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
func (e *Envelope) Verify() (bool, error) {
|
||||||
|
pubKey, err := did.ToPubKey(e.sigPayload.tokenPayload.Issuer)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cbor, err := e.sigPayload.cbor()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubKey.Verify(cbor, e.signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap encodes the Envelope as an IPLD datamodel.Node.
|
||||||
|
func (e *Envelope) Wrap() (datamodel.Node, error) {
|
||||||
|
spn, err := e.sigPayload.wrap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.Bytes(e.signature))
|
||||||
|
qp.ListEntry(la, qp.Node(spn))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// The types below are strictly to make it easier to Wrap and Unwrap the
|
||||||
|
// Envelope with an IPLD datamodel.Node. The Envelope itself provides
|
||||||
|
// accessors to the internals of these types.
|
||||||
|
//
|
||||||
|
|
||||||
|
type sigPayload struct {
|
||||||
|
varsigHeader []byte
|
||||||
|
tokenPayload *token.Token
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSigPayload(keyType pb.KeyType, token *token.Token, tag string) (*sigPayload, error) {
|
||||||
|
varsigHeader, err := varsig.Encode(keyType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sigPayload{
|
||||||
|
varsigHeader: varsigHeader,
|
||||||
|
tokenPayload: token,
|
||||||
|
tag: tag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unwrapSigPayload(node datamodel.Node) (*sigPayload, error) {
|
||||||
|
// Normally we could look up the VarsigHeader and TokenPayload using
|
||||||
|
// node.LookupByString() - this works for the "h" key used for the
|
||||||
|
// VarsigHeader but not for the TokenPayload's key (tag) as all we
|
||||||
|
// know is that it starts with "ucan/" and as explained below, must
|
||||||
|
// decode to a schema.TypedNode for the representation provided by the
|
||||||
|
// token.Prototype().
|
||||||
|
// vvv
|
||||||
|
mi := node.MapIterator()
|
||||||
|
if mi == nil {
|
||||||
|
return nil, fmt.Errorf("the SigPayload node is not a map: %s", node.Kind().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hdrNode datamodel.Node
|
||||||
|
tknNode datamodel.Node
|
||||||
|
tag string
|
||||||
|
)
|
||||||
|
|
||||||
|
keyCount := 0
|
||||||
|
|
||||||
|
for !mi.Done() {
|
||||||
|
k, v, err := mi.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
kStr, err := k.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("the SigPayload keys are not strings: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCount++
|
||||||
|
|
||||||
|
if kStr == "h" {
|
||||||
|
hdrNode = v
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(kStr, "ucan/") {
|
||||||
|
tknNode = v
|
||||||
|
tag = kStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyCount != 2 {
|
||||||
|
return nil, fmt.Errorf("the SigPayload map should have exactly two keys: %d", keyCount)
|
||||||
|
}
|
||||||
|
// ^^^
|
||||||
|
|
||||||
|
// Replaces the datamodel.Node in tokenPayloadNode with a
|
||||||
|
// schema.TypedNode so that we can cast it to a *token.Token after
|
||||||
|
// unwrapping it.
|
||||||
|
// vvv
|
||||||
|
nb := token.Prototype().Representation().NewBuilder()
|
||||||
|
|
||||||
|
err := nb.AssignNode(tknNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tknNode = nb.Build()
|
||||||
|
// ^^^
|
||||||
|
|
||||||
|
tokenPayload := bindnode.Unwrap(tknNode)
|
||||||
|
if tokenPayload == nil {
|
||||||
|
return nil, errors.New("failed to Unwrap the TokenPayload")
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, ok := tokenPayload.(*token.Token)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("failed to assert the TokenPayload type as *token.Token")
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr, err := hdrNode.AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &sigPayload{
|
||||||
|
varsigHeader: hdr,
|
||||||
|
tokenPayload: tkn,
|
||||||
|
tag: tag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *sigPayload) cbor() ([]byte, error) {
|
||||||
|
node, err := sp.wrap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
if err = dagcbor.Encode(node, buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *sigPayload) wrap() (datamodel.Node, error) {
|
||||||
|
tpn := bindnode.Wrap(sp.tokenPayload, token.Prototype().Type())
|
||||||
|
|
||||||
|
return qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||||
|
qp.MapEntry(ma, "h", qp.Bytes(sp.varsigHeader))
|
||||||
|
qp.MapEntry(ma, sp.tag, qp.Node(tpn.Representation()))
|
||||||
|
})
|
||||||
|
}
|
||||||
200
internal/envelope/envelope_test.go
Normal file
200
internal/envelope/envelope_test.go
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package envelope_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"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/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/envelope"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/token"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
|
||||||
|
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
|
||||||
|
exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
|
||||||
|
exampleTag = "ucan/example@v1.0.0-rc.1"
|
||||||
|
exampleVarsigHeaderStr = "NO0BcQ"
|
||||||
|
|
||||||
|
invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK"
|
||||||
|
|
||||||
|
exampleDAGCBORFilename = "example.dagcbor"
|
||||||
|
exampleDAGJSONFilename = "example.dagjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
envel := exampleEnvelope(t)
|
||||||
|
assert.NotZero(t, envel)
|
||||||
|
|
||||||
|
assert.Equal(t, exampleSignature(t), envel.Signature())
|
||||||
|
assert.Equal(t, exampleTag, envel.Tag())
|
||||||
|
assert.Equal(t, exampleVarsigHeader(t), envel.VarsigHeader())
|
||||||
|
assert.EqualValues(t, exampleGoldenTokenPayload(t), envel.TokenPayload())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
node, err := envelope.Wrap(examplePrivKey(t), exampleToken(t), exampleTag)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cbor, err := ipld.Encode(node, dagcbor.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
golden.AssertBytes(t, cbor, exampleDAGCBORFilename)
|
||||||
|
|
||||||
|
json, err := ipld.Encode(node, dagjson.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
golden.Assert(t, string(json), exampleDAGJSONFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvelope_Verify(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("valid signature by issuer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
envel := exampleEnvelope(t)
|
||||||
|
ok, err := envel.Verify()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid signature by wrong issuer", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
envel, err := envelope.Unwrap(invalidNodeFromGolden(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ok, _ := envel.Verify()
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvelope_Wrap(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
envel := exampleEnvelope(t)
|
||||||
|
|
||||||
|
node, err := envel.Wrap()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cbor, err := ipld.Encode(node, dagcbor.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, golden.Get(t, exampleDAGCBORFilename), cbor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleGoldenEnvelope(t *testing.T) *envelope.Envelope {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
envel, err := envelope.Unwrap(exampleGoldenNode(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return envel
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleGoldenNode(t *testing.T) datamodel.Node {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cbor := golden.Get(t, exampleDAGCBORFilename)
|
||||||
|
|
||||||
|
node, err := ipld.Decode(cbor, dagcbor.Decode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleGoldenTokenPayload(t *testing.T) *token.Token {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return exampleGoldenEnvelope(t).TokenPayload()
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePrivKey(t *testing.T) crypto.PrivKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleEnvelope(t *testing.T) *envelope.Envelope {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
envel, err := envelope.New(examplePrivKey(t), exampleToken(t), exampleTag)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return envel
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePubKey(t *testing.T) crypto.PubKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return examplePrivKey(t).GetPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleSignature(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return sig
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleToken(t *testing.T) *token.Token {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(examplePubKey(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &token.Token{
|
||||||
|
Issuer: id.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleVarsigHeader(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
hdr, err := base64.RawStdEncoding.DecodeString(exampleVarsigHeaderStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidNodeFromGolden(t *testing.T) datamodel.Node {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
envelNode := exampleGoldenNode(t)
|
||||||
|
sigPayloadNode, err := envelNode.LookupByIndex(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node, err := qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.Bytes(invalidSig))
|
||||||
|
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
1
internal/envelope/testdata/example.dagcbor
vendored
Normal file
1
internal/envelope/testdata/example.dagcbor
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
‚X@=•zfˆîŒ— ©Ê¦Z.ÈÚàP óú¤‹Jr=n–¬;¡)Dñ÷™¹6îB;ò
|
||||||
20
internal/envelope/testdata/example.dagjson
vendored
Normal file
20
internal/envelope/testdata/example.dagjson
vendored
Normal file
@@ -0,0 +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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
24
internal/token/conversion.go
Normal file
24
internal/token/conversion.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToIPLDMapStringAny(m map[string]datamodel.Node) Map__String__Any {
|
||||||
|
keys := make([]string, len(m))
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for k := range m {
|
||||||
|
keys[i] = k
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return Map__String__Any{
|
||||||
|
Keys: keys,
|
||||||
|
Values: m,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromIPLDMapStringAny(m Map__String__Any) map[string]datamodel.Node {
|
||||||
|
return m.Values
|
||||||
|
}
|
||||||
33
internal/token/doc.go
Normal file
33
internal/token/doc.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Package token provides a generic model of the [TokenPayload] required
|
||||||
|
// within an Envelope.
|
||||||
|
//
|
||||||
|
// # Field requirements
|
||||||
|
//
|
||||||
|
// While the Token object represents the wire format of both a UCAN
|
||||||
|
// Delegation token and a UCAN Invocation token, the delegation and
|
||||||
|
// invocation packages are, respectively, responsible for making sure
|
||||||
|
// required fields are included when creating a new Token or when
|
||||||
|
// validating the contents of an Envelope as it's received from
|
||||||
|
// another party. The following table shows the current (as of
|
||||||
|
// 2024-09-11) relationship between optional and nullable fields in
|
||||||
|
// the delegation and invocation views and the payload model:
|
||||||
|
//
|
||||||
|
// | Name | Delegation | Invocation | Token |
|
||||||
|
// | | Required | Nullable | Required | Nullable | |
|
||||||
|
// | ----- | -------- | -------- | -------- | -------- | -------- |
|
||||||
|
// | iss | Yes | No | Yes | No | |
|
||||||
|
// | aud | Yes | No | No | N/A | Optional |
|
||||||
|
// | sub | Yes | Yes | Yes | No | Nullable |
|
||||||
|
// | cmd | Yes | No | Yes | No | |
|
||||||
|
// | pol | Yes | No | X | X | Optional |
|
||||||
|
// | nonce | Yes | No | No | N/A | Optional |
|
||||||
|
// | meta | No | N/A | No | N/A | Optional |
|
||||||
|
// | nbf | No | N/A | X | X | Optional |
|
||||||
|
// | exp | Yes | Yes | Yes | Yes | |
|
||||||
|
// | args | X | X | Yes | No | Optional |
|
||||||
|
// | prf | X | X | Yes | No | Optional |
|
||||||
|
// | iat | X | X | No | N/A | Optional |
|
||||||
|
// | cause | X | X | No | N/A | Optional |
|
||||||
|
//
|
||||||
|
// [TokenPayload]: https://github.com/ucan-wg/spec?tab=readme-ov-file#envelope
|
||||||
|
package token
|
||||||
11
internal/token/errors.go
Normal file
11
internal/token/errors.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrFailedSchemaLoad = errors.New("failed to load IPLD Schema")
|
||||||
|
|
||||||
|
var ErrNoSchemaType = errors.New("schema does not contain type")
|
||||||
|
|
||||||
|
var ErrNodeNotToken = errors.New("IPLD node is not a Token")
|
||||||
|
|
||||||
|
var ErrMissingRequiredDID = errors.New("a required DID is missing")
|
||||||
46
internal/token/schema.go
Normal file
46
internal/token/schema.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tokenTypeName = "Token"
|
||||||
|
|
||||||
|
//go:embed token.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("%w: %w", ErrFailedSchemaLoad, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
tknType := ts.TypeByName(tokenTypeName)
|
||||||
|
if tknType == nil {
|
||||||
|
panic(fmt.Errorf("%w: %s", ErrNoSchemaType, tokenTypeName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenType() schema.Type {
|
||||||
|
return mustLoadSchema().TypeByName(tokenTypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Prototype() schema.TypedPrototype {
|
||||||
|
return bindnode.Prototype((*Token)(nil), tokenType())
|
||||||
|
}
|
||||||
84
internal/token/schema_test.go
Normal file
84
internal/token/schema_test.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package token_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed token.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":""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
// format: dagJson --> IPLD node --> token --> dagCbor --> IPLD node --> dagJson
|
||||||
|
// function: Unwrap() Wrap()
|
||||||
|
|
||||||
|
n1, err := ipld.DecodeUsingPrototype([]byte(delegationJson), dagjson.Decode, token.Prototype())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cborBytes, err := ipld.Encode(n1, dagcbor.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("cborBytes length", len(cborBytes))
|
||||||
|
fmt.Println("cbor", string(cborBytes))
|
||||||
|
|
||||||
|
n2, err := ipld.DecodeUsingPrototype(cborBytes, dagcbor.Decode, token.Prototype())
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("read Cbor", n2)
|
||||||
|
|
||||||
|
t1, err := token.Unwrap(n2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
n3 := t1.Wrap()
|
||||||
|
|
||||||
|
readJson, err := ipld.Encode(n3, dagjson.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("readJson length", len(readJson))
|
||||||
|
fmt.Println("json: ", string(readJson))
|
||||||
|
|
||||||
|
require.JSONEq(t, delegationJson, string(readJson))
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSchemaLoad(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
internal/token/token.go
Normal file
33
internal/token/token.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package token
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run ../cmd/token/...
|
||||||
|
|
||||||
|
// Unwrap creates a Token from an arbitrary IPLD node or returns an
|
||||||
|
// error if at least the required model fields are not present.
|
||||||
|
//
|
||||||
|
// It is the responsibility of the Delegation and Invocation views
|
||||||
|
// to further validate the presence of the required fields and the
|
||||||
|
// content as needed.
|
||||||
|
func Unwrap(node datamodel.Node) (*Token, error) {
|
||||||
|
iface := bindnode.Unwrap(node)
|
||||||
|
if iface == nil {
|
||||||
|
return nil, ErrNodeNotToken
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, ok := iface.(*Token)
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNodeNotToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap creates an IPLD node representing the Token.
|
||||||
|
func (t *Token) Wrap() datamodel.Node {
|
||||||
|
return bindnode.Wrap(t, tokenType())
|
||||||
|
}
|
||||||
63
internal/token/token.ipldsch
Normal file
63
internal/token/token.ipldsch
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
type CID string
|
||||||
|
|
||||||
|
type Command string
|
||||||
|
|
||||||
|
type DID string
|
||||||
|
|
||||||
|
# Field requirements:
|
||||||
|
#
|
||||||
|
# | Name | Delegation | Invocation | Token |
|
||||||
|
# | | Required | Nullable | Required | Nullable | |
|
||||||
|
# | ----- | -------- | -------- | -------- | -------- | -------- |
|
||||||
|
# | iss | Yes | No | Yes | No | |
|
||||||
|
# | aud | Yes | No | No | N/A | Optional |
|
||||||
|
# | sub | Yes | Yes | Yes | No | Nullable |
|
||||||
|
# | cmd | Yes | No | Yes | No | |
|
||||||
|
# | pol | Yes | No | X | X | Optional |
|
||||||
|
# | nonce | Yes | No | No | N/A | Optional |
|
||||||
|
# | meta | No | N/A | No | N/A | Optional |
|
||||||
|
# | nbf | No | N/A | X | X | Optional |
|
||||||
|
# | exp | Yes | Yes | Yes | Yes | Nullable |
|
||||||
|
# | args | X | X | Yes | No | Optional |
|
||||||
|
# | prf | X | X | Yes | No | Optional |
|
||||||
|
# | iat | X | X | No | N/A | Optional |
|
||||||
|
# | cause | X | X | No | N/A | Optional |
|
||||||
|
|
||||||
|
type Token struct {
|
||||||
|
# Issuer DID (sender)
|
||||||
|
issuer DID (rename "iss")
|
||||||
|
# Audience DID (receiver)
|
||||||
|
audience optional DID (rename "aud")
|
||||||
|
# Principal that the chain is about (the Subject)
|
||||||
|
subject nullable DID (rename "sub")
|
||||||
|
|
||||||
|
# The Command to eventually invoke
|
||||||
|
command Command (rename "cmd")
|
||||||
|
|
||||||
|
# The delegation policy
|
||||||
|
# It doesn't seem possible to represent it with a schema.
|
||||||
|
policy optional Any (rename "pol")
|
||||||
|
|
||||||
|
# The invocation's arguments
|
||||||
|
arguments optional {String: Any} (rename "args")
|
||||||
|
|
||||||
|
# Delegations that prove the chain of authority
|
||||||
|
Proofs optional [CID] (rename "prf")
|
||||||
|
|
||||||
|
# A unique, random nonce
|
||||||
|
nonce optional Bytes
|
||||||
|
|
||||||
|
# Arbitrary Metadata
|
||||||
|
meta optional {String : Any}
|
||||||
|
|
||||||
|
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
|
notBefore optional Int (rename "nbf")
|
||||||
|
# The timestamp at which the delegation becomes invalid
|
||||||
|
expiration nullable Int (rename "exp")
|
||||||
|
# The timestamp at which the invocation was created
|
||||||
|
issuedAt optional Int
|
||||||
|
|
||||||
|
# An optional CID of the receipt that enqueued this invocation
|
||||||
|
cause optional CID
|
||||||
|
}
|
||||||
31
internal/token/token_gen.go
Normal file
31
internal/token/token_gen.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Code generated by internal/cmd/token - DO NOT EDIT.
|
||||||
|
|
||||||
|
package token
|
||||||
|
|
||||||
|
import "github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
|
||||||
|
type Map struct {
|
||||||
|
Keys []string
|
||||||
|
Values map[string]datamodel.Node
|
||||||
|
}
|
||||||
|
type List []datamodel.Node
|
||||||
|
type Map__String__Any struct {
|
||||||
|
Keys []string
|
||||||
|
Values map[string]datamodel.Node
|
||||||
|
}
|
||||||
|
type List__CID []string
|
||||||
|
type Token struct {
|
||||||
|
Issuer string
|
||||||
|
Audience *string
|
||||||
|
Subject *string
|
||||||
|
Command string
|
||||||
|
Policy *datamodel.Node
|
||||||
|
Arguments *Map__String__Any
|
||||||
|
Proofs *List__CID
|
||||||
|
Nonce *[]uint8
|
||||||
|
Meta *Map__String__Any
|
||||||
|
NotBefore *int
|
||||||
|
Expiration *int
|
||||||
|
IssuedAt *int
|
||||||
|
Cause *string
|
||||||
|
}
|
||||||
55
internal/token/token_test.go
Normal file
55
internal/token/token_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package token_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/internal/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn := &token.Token{}
|
||||||
|
|
||||||
|
node := tkn.Wrap()
|
||||||
|
|
||||||
|
json, err := ipld.Encode(node, dagjson.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Log(string(json))
|
||||||
|
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrototype(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn := &token.Token{
|
||||||
|
Issuer: "blah",
|
||||||
|
}
|
||||||
|
n1 := tkn.Wrap()
|
||||||
|
json, err := ipld.Encode(n1, dagjson.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Log(string(json))
|
||||||
|
|
||||||
|
n2, err := ipld.Decode(json, dagjson.Decode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nb := token.Prototype().Representation().NewBuilder()
|
||||||
|
require.NoError(t, nb.AssignNode(n2))
|
||||||
|
|
||||||
|
n3 := nb.Build()
|
||||||
|
|
||||||
|
tkn2, err := token.Unwrap(n3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Log(tkn2)
|
||||||
|
|
||||||
|
require.Equal(t, tkn, tkn2)
|
||||||
|
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
7
internal/tools/tools.go
Normal file
7
internal/tools/tools.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build tools
|
||||||
|
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "github.com/selesy/go-options"
|
||||||
|
)
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
package varsig
|
package varsig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -57,7 +58,7 @@ var (
|
|||||||
//
|
//
|
||||||
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
||||||
func Decode(header []byte) (pb.KeyType, error) {
|
func Decode(header []byte) (pb.KeyType, error) {
|
||||||
keyType, ok := decMap[string(header)]
|
keyType, ok := decMap[base64.RawStdEncoding.EncodeToString(header)]
|
||||||
if !ok {
|
if !ok {
|
||||||
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
|
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
|
||||||
}
|
}
|
||||||
@@ -81,10 +82,10 @@ func Encode(keyType pb.KeyType) ([]byte, error) {
|
|||||||
return []byte(header), nil
|
return []byte(header), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyTypeToHeader() map[pb.KeyType]string {
|
func keyTypeToHeader() map[pb.KeyType][]byte {
|
||||||
const rsaSigLen = 0x100
|
const rsaSigLen = 0x100
|
||||||
|
|
||||||
return map[pb.KeyType]string{
|
return map[pb.KeyType][]byte{
|
||||||
pb.KeyType_RSA: header(
|
pb.KeyType_RSA: header(
|
||||||
Prefix,
|
Prefix,
|
||||||
multicodec.RsaPub,
|
multicodec.RsaPub,
|
||||||
@@ -116,18 +117,18 @@ func headerToKeyType() map[string]pb.KeyType {
|
|||||||
out := make(map[string]pb.KeyType, len(encMap))
|
out := make(map[string]pb.KeyType, len(encMap))
|
||||||
|
|
||||||
for keyType, header := range encMap {
|
for keyType, header := range encMap {
|
||||||
out[header] = keyType
|
out[base64.RawStdEncoding.EncodeToString(header)] = keyType
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func header(vals ...multicodec.Code) string {
|
func header(vals ...multicodec.Code) []byte {
|
||||||
var buf []byte
|
var buf []byte
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
buf = binary.AppendUvarint(buf, uint64(val))
|
buf = binary.AppendUvarint(buf, uint64(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(buf)
|
return buf
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/varsig"
|
"github.com/ucan-wg/go-ucan/internal/varsig"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecode(t *testing.T) {
|
func TestDecode(t *testing.T) {
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Token container
|
|
||||||
|
|
||||||
## Why do I need that?
|
|
||||||
|
|
||||||
Some common situation asks to package multiple tokens together:
|
|
||||||
- calling a service requires sending an invocation, alongside the matching delegations
|
|
||||||
- sending a series of revocations
|
|
||||||
- \<insert your application specific scenario here>
|
|
||||||
|
|
||||||
The UCAN specification defines how a single token is serialized (envelope with signature, IPLD encoded as Dag-cbor), but it's entirely left open how to package multiple tokens together. To be clear, this is a correct thing to do for a specification, as different ways equally valid to solve that problem exists and can coexist. Any wire format holding a list of bytes would do (cbor, json, csv ...).
|
|
||||||
|
|
||||||
**go-ucan** however, provide an opinionated implementation, which may or may not work in your situation.
|
|
||||||
|
|
||||||
Some experiment has been done over which format is appropriate, and two have been selected:
|
|
||||||
- **DAG-CBOR** of a list of bytes, as a low overhead option
|
|
||||||
- **CAR** file, as a somewhat common ways to cary arbitrary blocks of data
|
|
||||||
|
|
||||||
Notably, **compression is not included**, even though it does work reasonably well. This is because your transport medium might already do it, or should.
|
|
||||||
|
|
||||||
## Wire format consideration
|
|
||||||
|
|
||||||
Several possible formats have been explored:
|
|
||||||
- CAR files (binary or base64)
|
|
||||||
- DAG-CBOR (binary or base64)
|
|
||||||
|
|
||||||
Additionally, gzip and deflate compression has been experimented with.
|
|
||||||
|
|
||||||
Below are the results in terms of storage used, as percentage and byte overhead over the raw tokens:
|
|
||||||
|
|
||||||
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|
|
||||||
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
|
|
||||||
| 1 | 15 | 54 | 7 | 42 | 0 | 35 | \-8 | 22 | \-12 | 16 |
|
|
||||||
| 2 | 12 | 49 | \-12 | 15 | 0 | 34 | \-25 | 0 | \-28 | \-3 |
|
|
||||||
| 3 | 11 | 48 | \-21 | 4 | 0 | 34 | \-32 | \-10 | \-34 | \-11 |
|
|
||||||
| 4 | 10 | 47 | \-26 | \-1 | 0 | 34 | \-36 | \-15 | \-37 | \-17 |
|
|
||||||
| 5 | 10 | 47 | \-28 | \-4 | 0 | 34 | \-38 | \-18 | \-40 | \-20 |
|
|
||||||
| 6 | 10 | 47 | \-30 | \-7 | 0 | 34 | \-40 | \-20 | \-40 | \-20 |
|
|
||||||
| 7 | 10 | 46 | \-31 | \-8 | 0 | 34 | \-41 | \-21 | \-42 | \-22 |
|
|
||||||
| 8 | 9 | 46 | \-32 | \-10 | 0 | 34 | \-42 | \-22 | \-42 | \-23 |
|
|
||||||
| 9 | 9 | 46 | \-33 | \-11 | 0 | 34 | \-43 | \-23 | \-43 | \-24 |
|
|
||||||
| 10 | 9 | 46 | \-34 | \-12 | 0 | 34 | \-43 | \-25 | \-44 | \-25 |
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|
|
||||||
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
|
|
||||||
| 1 | 64 | 226 | 29 | 178 | 4 | 146 | \-35 | 94 | \-52 | 70 |
|
|
||||||
| 2 | 102 | 412 | \-107 | 128 | 7 | 288 | \-211 | 0 | \-234 | \-32 |
|
|
||||||
| 3 | 140 | 602 | \-270 | 58 | 10 | 430 | \-405 | \-126 | \-429 | \-146 |
|
|
||||||
| 4 | 178 | 792 | \-432 | \-28 | 13 | 572 | \-602 | \-252 | \-617 | \-288 |
|
|
||||||
| 5 | 216 | 978 | \-582 | \-94 | 16 | 714 | \-805 | \-386 | \-839 | \-418 |
|
|
||||||
| 6 | 254 | 1168 | \-759 | \-176 | 19 | 856 | \-1001 | \-508 | \-1018 | \-520 |
|
|
||||||
| 7 | 292 | 1358 | \-908 | \-246 | 22 | 998 | \-1204 | \-634 | \-1229 | \-650 |
|
|
||||||
| 8 | 330 | 1544 | \-1085 | \-332 | 25 | 1140 | \-1398 | \-756 | \-1423 | \-792 |
|
|
||||||
| 9 | 368 | 1734 | \-1257 | \-414 | 28 | 1282 | \-1614 | \-894 | \-1625 | \-930 |
|
|
||||||
| 10 | 406 | 1924 | \-1408 | \-508 | 31 | 1424 | \-1804 | \-1040 | \-1826 | \-1060 |
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Following is the performance aspect, with CPU usage and memory allocation:
|
|
||||||
|
|
||||||
| | Write ns/op | Read ns/op | Write B/op | Read B/op | Write allocs/op | Read allocs/op |
|
|
||||||
|-----------------|-------------|------------|------------|-----------|-----------------|----------------|
|
|
||||||
| car | 8451 | 1474630 | 17928 | 149437 | 59 | 2631 |
|
|
||||||
| carBase64 | 16750 | 1437678 | 24232 | 151502 | 61 | 2633 |
|
|
||||||
| carGzip | 320253 | 1581412 | 823887 | 192272 | 76 | 2665 |
|
|
||||||
| carGzipBase64 | 343305 | 1486269 | 828782 | 198543 | 77 | 2669 |
|
|
||||||
| cbor | 6419 | 1301554 | 16368 | 138891 | 25 | 2534 |
|
|
||||||
| cborBase64 | 12860 | 1386728 | 20720 | 140962 | 26 | 2536 |
|
|
||||||
| cborGzip | 310106 | 1379146 | 822742 | 182003 | 42 | 2585 |
|
|
||||||
| cborGzipBase64 | 317001 | 1462548 | 827640 | 189283 | 43 | 2594 |
|
|
||||||
| cborFlate | 327112 | 1555007 | 822473 | 181537 | 40 | 2591 |
|
|
||||||
| cborFlateBase64 | 311276 | 1456562 | 826042 | 188665 | 41 | 2596 |
|
|
||||||
|
|
||||||
(BEWARE: logarithmic scale)
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Conclusion:
|
|
||||||
- CAR files are heavy for this usage, notably because they carry the CIDs of the tokens
|
|
||||||
- compression works quite well and warrants its usage even with a single token
|
|
||||||
- DAG-CBOR outperform CAR files everywhere, and comes with a tiny ~3 bytes per token overhead.
|
|
||||||
|
|
||||||
**Formats beside DAG-CBOR and CAR, with or without base64, have been removed. They are in the git history though.**
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"iter"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Note: below is essentially a re-implementation of the CAR file v1 read and write.
|
|
||||||
This exists here for two reasons:
|
|
||||||
- go-car's API forces to go through an IPLD getter or through a blockstore API
|
|
||||||
- generally, go-car is a very complex and large dependency
|
|
||||||
*/
|
|
||||||
|
|
||||||
// EmptyCid is a "zero" Cid: zero-length "identity" multihash with "raw" codec
|
|
||||||
// It can be used to have at least one root in a CARv1 file (making it legal), yet
|
|
||||||
// denote that it can be ignored.
|
|
||||||
var EmptyCid = cid.MustParse([]byte{01, 55, 00, 00})
|
|
||||||
|
|
||||||
type carBlock struct {
|
|
||||||
c cid.Cid
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeCar writes a CARv1 file containing the blocks from the iterator.
|
|
||||||
// If no roots are provided, a single EmptyCid is used as root to make the file
|
|
||||||
// spec compliant.
|
|
||||||
func writeCar(w io.Writer, roots []cid.Cid, blocks iter.Seq2[carBlock, error]) error {
|
|
||||||
if len(roots) == 0 {
|
|
||||||
roots = []cid.Cid{EmptyCid}
|
|
||||||
}
|
|
||||||
h := carHeader{
|
|
||||||
Roots: roots,
|
|
||||||
Version: 1,
|
|
||||||
}
|
|
||||||
hb, err := h.Write()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ldWrite(w, hb)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for block, err := range blocks {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = ldWrite(w, block.c.Bytes(), block.data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readCar reads a CARv1 file from the reader, and return a block iterator.
|
|
||||||
// Roots are ignored.
|
|
||||||
func readCar(r io.Reader) (roots []cid.Cid, blocks iter.Seq2[carBlock, error], err error) {
|
|
||||||
br := bufio.NewReader(r)
|
|
||||||
|
|
||||||
hb, err := ldRead(br)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
h, err := readHeader(hb)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if h.Version != 1 {
|
|
||||||
return nil, nil, fmt.Errorf("invalid car version: %d", h.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Roots, func(yield func(block carBlock, err error) bool) {
|
|
||||||
for {
|
|
||||||
block, err := readBlock(br)
|
|
||||||
if err == io.EOF {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if !yield(carBlock{}, err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !yield(block, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readBlock reads a section from the reader and decode a (cid+data) block.
|
|
||||||
func readBlock(r *bufio.Reader) (carBlock, error) {
|
|
||||||
raw, err := ldRead(r)
|
|
||||||
if err != nil {
|
|
||||||
return carBlock{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, c, err := cid.CidFromReader(bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
return carBlock{}, err
|
|
||||||
}
|
|
||||||
data := raw[n:]
|
|
||||||
|
|
||||||
// integrity check
|
|
||||||
hashed, err := c.Prefix().Sum(data)
|
|
||||||
if err != nil {
|
|
||||||
return carBlock{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hashed.Equals(c) {
|
|
||||||
return carBlock{}, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", c, hashed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return carBlock{c: c, data: data}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxAllowedSectionSize dictates the maximum number of bytes that a CARv1 header
|
|
||||||
// or section is allowed to occupy without causing a decode to error.
|
|
||||||
// This cannot be supplied as an option, only adjusted as a global. You should
|
|
||||||
// use v2#NewReader instead since it allows for options to be passed in.
|
|
||||||
var maxAllowedSectionSize uint = 32 << 20 // 32MiB
|
|
||||||
|
|
||||||
// ldRead performs a length-delimited read of a section from the reader.
|
|
||||||
// A section is composed of an uint length followed by the data.
|
|
||||||
func ldRead(r *bufio.Reader) ([]byte, error) {
|
|
||||||
if _, err := r.Peek(1); err != nil { // no more blocks, likely clean io.EOF
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l, err := binary.ReadUvarint(r)
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
return nil, io.ErrUnexpectedEOF // don't silently pretend this is a clean EOF
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if l == 0 {
|
|
||||||
return nil, fmt.Errorf("invalid zero size section")
|
|
||||||
}
|
|
||||||
|
|
||||||
if l > uint64(maxAllowedSectionSize) { // Don't OOM
|
|
||||||
return nil, fmt.Errorf("malformed car; header is bigger than MaxAllowedSectionSize")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, l)
|
|
||||||
if _, err := io.ReadFull(r, buf); err != nil {
|
|
||||||
if err == io.EOF {
|
|
||||||
// we should be able to read the promised bytes, this is not normal
|
|
||||||
return nil, io.ErrUnexpectedEOF
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ldWrite performs a length-delimited write of a section on the writer.
|
|
||||||
// A section is composed of an uint length followed by the data.
|
|
||||||
func ldWrite(w io.Writer, d ...[]byte) error {
|
|
||||||
var sum uint64
|
|
||||||
for _, s := range d {
|
|
||||||
sum += uint64(len(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 8)
|
|
||||||
n := binary.PutUvarint(buf, sum)
|
|
||||||
_, err := w.Write(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range d {
|
|
||||||
_, err = w.Write(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type carHeader struct {
|
|
||||||
Roots []cid.Cid
|
|
||||||
Version uint64
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootsKey = "roots"
|
|
||||||
const versionKey = "version"
|
|
||||||
|
|
||||||
func readHeader(data []byte) (*carHeader, error) {
|
|
||||||
var header carHeader
|
|
||||||
|
|
||||||
nd, err := ipld.Decode(data, dagcbor.Decode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if nd.Length() != 2 {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
rootsNd, err := nd.LookupByString(rootsKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
it := rootsNd.ListIterator()
|
|
||||||
if it == nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
header.Roots = make([]cid.Cid, 0, rootsNd.Length())
|
|
||||||
for !it.Done() {
|
|
||||||
_, nd, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
lk, err := nd.AsLink()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch lk := lk.(type) {
|
|
||||||
case cidlink.Link:
|
|
||||||
header.Roots = append(header.Roots, lk.Cid)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
versionNd, err := nd.LookupByString(versionKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
version, err := versionNd.AsInt()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("malformed car header")
|
|
||||||
}
|
|
||||||
header.Version = uint64(version)
|
|
||||||
return &header, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *carHeader) Write() ([]byte, error) {
|
|
||||||
nd, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
|
||||||
qp.MapEntry(ma, rootsKey, qp.List(int64(len(ch.Roots)), func(la datamodel.ListAssembler) {
|
|
||||||
for _, root := range ch.Roots {
|
|
||||||
qp.ListEntry(la, qp.Link(cidlink.Link{Cid: root}))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
qp.MapEntry(ma, versionKey, qp.Int(1))
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ipld.Encode(nd, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCarRoundTrip(t *testing.T) {
|
|
||||||
// this car file is a complex and legal CARv1 file
|
|
||||||
original, err := os.ReadFile("testdata/sample-v1.car")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
roots, it, err := readCar(bytes.NewReader(original))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var blks []carBlock
|
|
||||||
for blk, err := range it {
|
|
||||||
require.NoError(t, err)
|
|
||||||
blks = append(blks, blk)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Len(t, blks, 1049)
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
err = writeCar(buf, roots, func(yield func(carBlock, error) bool) {
|
|
||||||
for _, blk := range blks {
|
|
||||||
if !yield(blk, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Bytes equal after the round-trip
|
|
||||||
require.Equal(t, original, buf.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzCarRoundTrip(f *testing.F) {
|
|
||||||
example, err := os.ReadFile("testdata/sample-v1.car")
|
|
||||||
require.NoError(f, err)
|
|
||||||
|
|
||||||
f.Add(example)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
roots, blocksIter, err := readCar(bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
// skip invalid binary
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
// reading all the blocks, which force reading and verifying the full file
|
|
||||||
var blocks []carBlock
|
|
||||||
for block, err := range blocksIter {
|
|
||||||
if err != nil {
|
|
||||||
// error reading, invalid data
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
blocks = append(blocks, block)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
err = writeCar(&buf, roots, func(yield func(carBlock, error) bool) {
|
|
||||||
for _, blk := range blocks {
|
|
||||||
if !yield(blk, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// test if the round-trip produce a byte-equal CAR
|
|
||||||
require.Equal(t, data, buf.Bytes())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,122 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrNotFound = fmt.Errorf("not found")
|
|
||||||
|
|
||||||
// Reader is a token container reader. It exposes the tokens conveniently decoded.
|
|
||||||
type Reader map[cid.Cid]token.Token
|
|
||||||
|
|
||||||
// GetToken returns an arbitrary decoded token, from its CID.
|
|
||||||
// If not found, ErrNotFound is returned.
|
|
||||||
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
|
|
||||||
tkn, ok := ctn[cid]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
|
||||||
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
|
||||||
tkn, err := ctn.GetToken(cid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("not a delegation token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInvocation returns the first found invocation.Token.
|
|
||||||
// If none are found, ErrNotFound is returned.
|
|
||||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
|
||||||
for _, t := range ctn {
|
|
||||||
if inv, ok := t.(*invocation.Token); ok {
|
|
||||||
return inv, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCar(r io.Reader) (Reader, error) {
|
|
||||||
_, it, err := readCar(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctn := make(Reader)
|
|
||||||
|
|
||||||
for block, err := range it {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ctn.addToken(block.data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCarBase64(r io.Reader) (Reader, error) {
|
|
||||||
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCbor(r io.Reader) (Reader, error) {
|
|
||||||
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if n.Kind() != datamodel.Kind_List {
|
|
||||||
return nil, fmt.Errorf("not a list")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctn := make(Reader, n.Length())
|
|
||||||
|
|
||||||
it := n.ListIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, val, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
data, err := val.AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = ctn.addToken(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ctn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FromCborBase64(r io.Reader) (Reader, error) {
|
|
||||||
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Reader) addToken(data []byte) error {
|
|
||||||
tkn, c, err := token.FromSealed(data)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ctn[c] = tkn
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestContainerRoundTrip(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
writer func(ctn Writer, w io.Writer) error
|
|
||||||
reader func(io.Reader) (Reader, error)
|
|
||||||
}{
|
|
||||||
{"car", Writer.ToCar, FromCar},
|
|
||||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
|
||||||
{"cbor", Writer.ToCbor, FromCbor},
|
|
||||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
tokens := make(map[cid.Cid]*delegation.Token)
|
|
||||||
var dataSize int
|
|
||||||
|
|
||||||
writer := NewWriter()
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
dlg, c, data := randToken()
|
|
||||||
writer.AddSealed(c, data)
|
|
||||||
tokens[c] = dlg
|
|
||||||
dataSize += len(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
|
|
||||||
err := tc.writer(writer, buf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Logf("data size %d", dataSize)
|
|
||||||
t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize)
|
|
||||||
|
|
||||||
reader, err := tc.reader(bytes.NewReader(buf.Bytes()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
for c, dlg := range tokens {
|
|
||||||
tknRead, err := reader.GetToken(c)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// require.Equal fails as time.Time holds a wall time that is going to be
|
|
||||||
// different, even if it represents the same event.
|
|
||||||
// We need to do the following instead.
|
|
||||||
|
|
||||||
dlgRead := tknRead.(*delegation.Token)
|
|
||||||
require.Equal(t, dlg.Issuer(), dlgRead.Issuer())
|
|
||||||
require.Equal(t, dlg.Audience(), dlgRead.Audience())
|
|
||||||
require.Equal(t, dlg.Subject(), dlgRead.Subject())
|
|
||||||
require.Equal(t, dlg.Command(), dlgRead.Command())
|
|
||||||
require.Equal(t, dlg.Policy(), dlgRead.Policy())
|
|
||||||
require.Equal(t, dlg.Nonce(), dlgRead.Nonce())
|
|
||||||
require.True(t, dlg.Meta().Equals(dlgRead.Meta()))
|
|
||||||
if dlg.NotBefore() != nil {
|
|
||||||
// within 1s as the original value gets truncated to seconds when serialized
|
|
||||||
require.WithinDuration(t, *dlg.NotBefore(), *dlgRead.NotBefore(), time.Second)
|
|
||||||
}
|
|
||||||
if dlg.Expiration() != nil {
|
|
||||||
// within 1s as the original value gets truncated to seconds when serialized
|
|
||||||
require.WithinDuration(t, *dlg.Expiration(), *dlgRead.Expiration(), time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkContainerSerialisation(b *testing.B) {
|
|
||||||
var duration strings.Builder
|
|
||||||
var allocByte strings.Builder
|
|
||||||
var allocCount strings.Builder
|
|
||||||
|
|
||||||
for _, builder := range []strings.Builder{duration, allocByte, allocCount} {
|
|
||||||
builder.WriteString("car\tcarBase64\tcarGzip\tcarGzipBase64\tcbor\tcborBase64\tcborGzip\tcborGzipBase64\tcborFlate\tcborFlateBase64\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
writer func(ctn Writer, w io.Writer) error
|
|
||||||
reader func(io.Reader) (Reader, error)
|
|
||||||
}{
|
|
||||||
{"car", Writer.ToCar, FromCar},
|
|
||||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
|
||||||
{"cbor", Writer.ToCbor, FromCbor},
|
|
||||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
|
||||||
} {
|
|
||||||
writer := NewWriter()
|
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
_, c, data := randToken()
|
|
||||||
writer.AddSealed(c, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
_ = tc.writer(writer, buf)
|
|
||||||
|
|
||||||
b.Run(tc.name+"_write", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
_ = tc.writer(writer, buf)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run(tc.name+"_read", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = tc.reader(bytes.NewReader(buf.Bytes()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func randBytes(n int) []byte {
|
|
||||||
b := make([]byte, n)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func randDID() (crypto.PrivKey, did.DID) {
|
|
||||||
privKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
d, err := did.FromPrivKey(privKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return privKey, d
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomString(length int) string {
|
|
||||||
b := make([]byte, length/2+1)
|
|
||||||
rand.Read(b)
|
|
||||||
return fmt.Sprintf("%x", b)[0:length]
|
|
||||||
}
|
|
||||||
|
|
||||||
func randToken() (*delegation.Token, cid.Cid, []byte) {
|
|
||||||
priv, iss := randDID()
|
|
||||||
_, aud := randDID()
|
|
||||||
cmd := command.New("foo", "bar")
|
|
||||||
pol := policy.MustConstruct(
|
|
||||||
policy.All(".[]",
|
|
||||||
policy.GreaterThan(".value", literal.Int(2)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
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(priv, aud, cmd, pol, opts...)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
b, c, err := t.ToSealed(priv)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return t, c, b
|
|
||||||
}
|
|
||||||
BIN
pkg/container/testdata/sample-v1.car
vendored
BIN
pkg/container/testdata/sample-v1.car
vendored
Binary file not shown.
@@ -1,61 +0,0 @@
|
|||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
|
|
||||||
|
|
||||||
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
|
||||||
type Writer map[cid.Cid][]byte
|
|
||||||
|
|
||||||
func NewWriter() Writer {
|
|
||||||
return make(Writer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
|
|
||||||
func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
|
|
||||||
ctn[cid] = data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) ToCar(w io.Writer) error {
|
|
||||||
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
|
|
||||||
for c, bytes := range ctn {
|
|
||||||
if !yield(carBlock{c: c, data: bytes}, nil) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) ToCarBase64(w io.Writer) error {
|
|
||||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
|
||||||
defer w2.Close()
|
|
||||||
return ctn.ToCar(w2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) ToCbor(w io.Writer) error {
|
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
|
|
||||||
for _, bytes := range ctn {
|
|
||||||
qp.ListEntry(la, qp.Bytes(bytes))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctn Writer) ToCborBase64(w io.Writer) error {
|
|
||||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
|
||||||
defer w2.Close()
|
|
||||||
return ctn.ToCbor(w2)
|
|
||||||
}
|
|
||||||
173
pkg/meta/meta.go
173
pkg/meta/meta.go
@@ -1,173 +0,0 @@
|
|||||||
package meta
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrUnsupported = errors.New("failure adding unsupported type to meta")
|
|
||||||
|
|
||||||
var ErrNotFound = errors.New("key-value not found in meta")
|
|
||||||
|
|
||||||
// Meta is a container for meta key-value pairs in a UCAN token.
|
|
||||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations and transformations,
|
|
||||||
// while hiding the IPLD complexity from the caller.
|
|
||||||
type Meta struct {
|
|
||||||
Keys []string
|
|
||||||
Values map[string]ipld.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMeta constructs a new Meta.
|
|
||||||
func NewMeta() *Meta {
|
|
||||||
return &Meta{Values: map[string]ipld.Node{}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBool retrieves a value as a bool.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetBool(key string) (bool, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return false, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsBool()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetString retrieves a value as a string.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetString(key string) (string, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return "", ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInt64 retrieves a value as an int64.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetInt64(key string) (int64, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFloat64 retrieves a value as a float64.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetFloat64(key string) (float64, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return 0, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBytes retrieves a value as a []byte.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetBytes(key string) ([]byte, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return v.AsBytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode retrieves a value as a raw IPLD node.
|
|
||||||
// Returns ErrNotFound if the given key is missing.
|
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
|
||||||
func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
|
||||||
v, ok := m.Values[key]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a key/value pair in the meta set.
|
|
||||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
|
||||||
// and ipld.Node.
|
|
||||||
func (m *Meta) Add(key string, val any) error {
|
|
||||||
switch val := val.(type) {
|
|
||||||
case bool:
|
|
||||||
m.Values[key] = basicnode.NewBool(val)
|
|
||||||
case string:
|
|
||||||
m.Values[key] = basicnode.NewString(val)
|
|
||||||
case int:
|
|
||||||
m.Values[key] = basicnode.NewInt(int64(val))
|
|
||||||
case int32:
|
|
||||||
m.Values[key] = basicnode.NewInt(int64(val))
|
|
||||||
case int64:
|
|
||||||
m.Values[key] = basicnode.NewInt(val)
|
|
||||||
case float32:
|
|
||||||
m.Values[key] = basicnode.NewFloat(float64(val))
|
|
||||||
case float64:
|
|
||||||
m.Values[key] = basicnode.NewFloat(val)
|
|
||||||
case []byte:
|
|
||||||
m.Values[key] = basicnode.NewBytes(val)
|
|
||||||
case datamodel.Node:
|
|
||||||
m.Values[key] = val
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
|
|
||||||
}
|
|
||||||
m.Keys = append(m.Keys, key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals tells if two Meta hold the same key/values.
|
|
||||||
func (m *Meta) Equals(other *Meta) bool {
|
|
||||||
if len(m.Keys) != len(other.Keys) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(m.Values) != len(other.Values) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, key := range m.Keys {
|
|
||||||
if !ipld.DeepEqual(m.Values[key], other.Values[key]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Meta) String() string {
|
|
||||||
buf := strings.Builder{}
|
|
||||||
buf.WriteString("{")
|
|
||||||
|
|
||||||
var i int
|
|
||||||
for key, node := range m.Values {
|
|
||||||
if i > 0 {
|
|
||||||
buf.WriteString(", ")
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
buf.WriteString(key)
|
|
||||||
buf.WriteString(":")
|
|
||||||
buf.WriteString(printer.Sprint(node))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString("}")
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func fqtn(val any) string {
|
|
||||||
var name string
|
|
||||||
|
|
||||||
t := reflect.TypeOf(val)
|
|
||||||
for t.Kind() == reflect.Pointer {
|
|
||||||
name += "*"
|
|
||||||
t = t.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
return name + t.PkgPath() + "." + t.Name()
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package meta_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMeta_Add(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
type Unsupported struct{}
|
|
||||||
|
|
||||||
t.Run("error if not primative or Node", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
|
||||||
require.ErrorIs(t, err, meta.ErrUnsupported)
|
|
||||||
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type glob string
|
|
||||||
|
|
||||||
// parseGlob ensures that the pattern conforms to the spec: only '*' and escaped '\*' are allowed.
|
|
||||||
func parseGlob(pattern string) (glob, error) {
|
|
||||||
for i := 0; i < len(pattern); i++ {
|
|
||||||
if pattern[i] == '*' {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == '*' {
|
|
||||||
i++ // skip the escaped '*'
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pattern[i] == '\\' && i+1 < len(pattern) {
|
|
||||||
i++ // skip the escaped character
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pattern[i] == '\\' {
|
|
||||||
return "", fmt.Errorf("invalid escape sequence")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return glob(pattern), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustParseGlob(pattern string) glob {
|
|
||||||
g, err := parseGlob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return g
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match matches a string against the glob pattern with * wildcards, handling escaped '\*' literals.
|
|
||||||
func (pattern glob) Match(str string) bool {
|
|
||||||
// i is the index for the pattern
|
|
||||||
// j is the index for the string
|
|
||||||
var i, j int
|
|
||||||
|
|
||||||
// starIdx keeps track of the position of the last * in the pattern.
|
|
||||||
// matchIdx keeps track of the position in the string where the last * matched.
|
|
||||||
var starIdx, matchIdx int = -1, -1
|
|
||||||
|
|
||||||
for j < len(str) {
|
|
||||||
if i < len(pattern) && (pattern[i] == str[j] || pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == str[j]) {
|
|
||||||
// characters match or if there's an escaped character that matches
|
|
||||||
if pattern[i] == '\\' {
|
|
||||||
// skip the escape character
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
j++
|
|
||||||
} else if i < len(pattern) && pattern[i] == '*' {
|
|
||||||
// there's a * wildcard in the pattern
|
|
||||||
starIdx = i
|
|
||||||
matchIdx = j
|
|
||||||
i++
|
|
||||||
} else if starIdx != -1 {
|
|
||||||
// there's a previous * wildcard, backtrack
|
|
||||||
i = starIdx + 1
|
|
||||||
matchIdx++
|
|
||||||
j = matchIdx
|
|
||||||
} else {
|
|
||||||
// no match found
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for remaining characters in the pattern
|
|
||||||
for i < len(pattern) && pattern[i] == '*' {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
// the entire pattern is processed, it's a match
|
|
||||||
return i == len(pattern)
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSimpleGlobMatch(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
pattern string
|
|
||||||
str string
|
|
||||||
matches bool
|
|
||||||
}{
|
|
||||||
// Basic matching
|
|
||||||
{"*", "anything", true},
|
|
||||||
{"a*", "abc", true},
|
|
||||||
{"*c", "abc", true},
|
|
||||||
{"a*c", "abc", true},
|
|
||||||
{"a*c", "abxc", true},
|
|
||||||
{"a*c", "ac", true},
|
|
||||||
{"a*c", "a", false},
|
|
||||||
{"a*c", "ab", false},
|
|
||||||
|
|
||||||
// Escaped characters
|
|
||||||
{"a\\*c", "a*c", true},
|
|
||||||
{"a\\*c", "abc", false},
|
|
||||||
|
|
||||||
// Mixed wildcards and literals
|
|
||||||
{"a*b*c", "abc", true},
|
|
||||||
{"a*b*c", "aXbYc", true},
|
|
||||||
{"a*b*c", "aXbY", false},
|
|
||||||
{"a*b*c", "abYc", true},
|
|
||||||
{"a*b*c", "aXbc", true},
|
|
||||||
{"a*b*c", "aXbYcZ", false},
|
|
||||||
|
|
||||||
// Edge cases
|
|
||||||
{"", "", true},
|
|
||||||
{"", "a", false},
|
|
||||||
{"*", "", true},
|
|
||||||
{"*", "a", true},
|
|
||||||
{"\\*", "*", true},
|
|
||||||
{"\\*", "a", false},
|
|
||||||
|
|
||||||
// Specified test cases
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Dan, Erin, Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob , Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol.", true},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol!", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice, Bob, Carol.", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", "Alice Cooper, Bob, Carol.", false},
|
|
||||||
{"Alice\\*, Bob*, Carol.", " Alice*, Bob, Carol. ", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) {
|
|
||||||
g, err := parseGlob(tt.pattern)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tt.matches, g.Match(tt.str))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkGlob(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
g := mustParseGlob("Alice\\*, Bob*, Carol.")
|
|
||||||
g.Match("Alice*, Bob*, Carol!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Match determines if the IPLD node satisfies the policy.
|
|
||||||
func (p Policy) Match(node datamodel.Node) bool {
|
|
||||||
for _, stmt := range p {
|
|
||||||
ok := matchStatement(stmt, node)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path
|
|
||||||
func (p Policy) Filter(path ...string) Policy {
|
|
||||||
var filtered Policy
|
|
||||||
|
|
||||||
for _, stmt := range p {
|
|
||||||
newChild, remain := filter(stmt, path)
|
|
||||||
if newChild != nil && len(remain) == 0 {
|
|
||||||
filtered = append(filtered, newChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchStatement(statement Statement, node ipld.Node) bool {
|
|
||||||
switch statement.Kind() {
|
|
||||||
case KindEqual:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, many, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if one != nil {
|
|
||||||
return datamodel.DeepEqual(s.value, one)
|
|
||||||
}
|
|
||||||
if many != nil {
|
|
||||||
for _, n := range many {
|
|
||||||
if eq := datamodel.DeepEqual(s.value, n); eq {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case KindGreaterThan:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := s.selector.Select(node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, gt)
|
|
||||||
}
|
|
||||||
case KindGreaterThanOrEqual:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := s.selector.Select(node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, gte)
|
|
||||||
}
|
|
||||||
case KindLessThan:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := s.selector.Select(node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, lt)
|
|
||||||
}
|
|
||||||
case KindLessThanOrEqual:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := s.selector.Select(node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, lte)
|
|
||||||
}
|
|
||||||
case KindNot:
|
|
||||||
if s, ok := statement.(negation); ok {
|
|
||||||
return !matchStatement(s.statement, node)
|
|
||||||
}
|
|
||||||
case KindAnd:
|
|
||||||
if s, ok := statement.(connective); ok {
|
|
||||||
for _, cs := range s.statements {
|
|
||||||
r := matchStatement(cs, node)
|
|
||||||
if !r {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case KindOr:
|
|
||||||
if s, ok := statement.(connective); ok {
|
|
||||||
if len(s.statements) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, cs := range s.statements {
|
|
||||||
r := matchStatement(cs, node)
|
|
||||||
if r {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case KindLike:
|
|
||||||
if s, ok := statement.(wildcard); ok {
|
|
||||||
one, _, err := s.selector.Select(node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
v, err := one.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.pattern.Match(v)
|
|
||||||
}
|
|
||||||
case KindAll:
|
|
||||||
if s, ok := statement.(quantifier); ok {
|
|
||||||
_, many, err := s.selector.Select(node)
|
|
||||||
if err != nil || many == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, n := range many {
|
|
||||||
ok := matchStatement(s.statement, n)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case KindAny:
|
|
||||||
if s, ok := statement.(quantifier); ok {
|
|
||||||
one, many, err := s.selector.Select(node)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if one != nil {
|
|
||||||
ok := matchStatement(s.statement, one)
|
|
||||||
if ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if many != nil {
|
|
||||||
for _, n := range many {
|
|
||||||
ok := matchStatement(s.statement, n)
|
|
||||||
if ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path
|
|
||||||
func filter(stmt Statement, path []string) (Statement, []string) {
|
|
||||||
// For each kind, we do some of the following if it applies:
|
|
||||||
// - test the path against the selector, consuming segments
|
|
||||||
// - for terminal statements (equality, wildcard), require all the segments to have been consumed
|
|
||||||
// - recursively filter child (negation, quantifier) or children (connective) statements with the remaining path
|
|
||||||
switch stmt.(type) {
|
|
||||||
case equality:
|
|
||||||
match, remain := stmt.(equality).selector.MatchPath(path...)
|
|
||||||
if match && len(remain) == 0 {
|
|
||||||
return stmt, remain
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
case negation:
|
|
||||||
newChild, remain := filter(stmt.(negation).statement, path)
|
|
||||||
if newChild != nil && len(remain) == 0 {
|
|
||||||
return negation{
|
|
||||||
statement: newChild,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
case connective:
|
|
||||||
var newChildren []Statement
|
|
||||||
for _, child := range stmt.(connective).statements {
|
|
||||||
newChild, remain := filter(child, path)
|
|
||||||
if newChild != nil && len(remain) == 0 {
|
|
||||||
newChildren = append(newChildren, newChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(newChildren) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return connective{
|
|
||||||
kind: stmt.(connective).kind,
|
|
||||||
statements: newChildren,
|
|
||||||
}, nil
|
|
||||||
case wildcard:
|
|
||||||
match, remain := stmt.(wildcard).selector.MatchPath(path...)
|
|
||||||
if match && len(remain) == 0 {
|
|
||||||
return stmt, remain
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
case quantifier:
|
|
||||||
match, remain := stmt.(quantifier).selector.MatchPath(path...)
|
|
||||||
if match && len(remain) == 0 {
|
|
||||||
return stmt, remain
|
|
||||||
}
|
|
||||||
if !match {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
newChild, remain := filter(stmt.(quantifier).statement, remain)
|
|
||||||
if newChild != nil && len(remain) == 0 {
|
|
||||||
return quantifier{
|
|
||||||
kind: stmt.(quantifier).kind,
|
|
||||||
selector: stmt.(quantifier).selector,
|
|
||||||
statement: newChild,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("unimplemented statement kind: %s", stmt.Kind()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
|
||||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
|
||||||
a := must.Int(actual)
|
|
||||||
b := must.Int(expected)
|
|
||||||
return satisfies(cmp.Compare(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
|
|
||||||
a, err := actual.AsFloat()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("extracting node float: %w", err))
|
|
||||||
}
|
|
||||||
b, err := expected.AsFloat()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("extracting selector float: %w", err))
|
|
||||||
}
|
|
||||||
return satisfies(cmp.Compare(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func gt(order int) bool { return order == 1 }
|
|
||||||
func gte(order int) bool { return order == 0 || order == 1 }
|
|
||||||
func lt(order int) bool { return order == -1 }
|
|
||||||
func lte(order int) bool { return order == 0 || order == -1 }
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
|
|
||||||
selpkg "github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
KindEqual = "==" // implemented by equality
|
|
||||||
KindGreaterThan = ">" // implemented by equality
|
|
||||||
KindGreaterThanOrEqual = ">=" // implemented by equality
|
|
||||||
KindLessThan = "<" // implemented by equality
|
|
||||||
KindLessThanOrEqual = "<=" // implemented by equality
|
|
||||||
KindNot = "not" // implemented by negation
|
|
||||||
KindAnd = "and" // implemented by connective
|
|
||||||
KindOr = "or" // implemented by connective
|
|
||||||
KindLike = "like" // implemented by wildcard
|
|
||||||
KindAll = "all" // implemented by quantifier
|
|
||||||
KindAny = "any" // implemented by quantifier
|
|
||||||
)
|
|
||||||
|
|
||||||
type Policy []Statement
|
|
||||||
|
|
||||||
type Constructor func() (Statement, error)
|
|
||||||
|
|
||||||
func Construct(cstors ...Constructor) (Policy, error) {
|
|
||||||
stmts, err := assemble(cstors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return stmts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustConstruct(cstors ...Constructor) Policy {
|
|
||||||
pol, err := Construct(cstors...)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return pol
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Policy) String() string {
|
|
||||||
if len(p) == 0 {
|
|
||||||
return "[]"
|
|
||||||
}
|
|
||||||
childs := make([]string, len(p))
|
|
||||||
for i, statement := range p {
|
|
||||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[\n %s\n]", strings.Join(childs, ",\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Statement interface {
|
|
||||||
Kind() string
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type equality struct {
|
|
||||||
kind string
|
|
||||||
selector selpkg.Selector
|
|
||||||
value ipld.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e equality) Kind() string {
|
|
||||||
return e.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e equality) String() string {
|
|
||||||
child, err := ipld.Encode(e.value, dagjson.Encode)
|
|
||||||
if err != nil {
|
|
||||||
return "ERROR: INVALID VALUE"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(`["%s", "%s", %s]`, e.kind, e.selector, strings.ReplaceAll(string(child), "\n", "\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Equal(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThan(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindGreaterThan, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThanOrEqual(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindGreaterThanOrEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThan(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindLessThan, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThanOrEqual(selector string, value ipld.Node) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return equality{kind: KindLessThanOrEqual, selector: sel, value: value}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type negation struct {
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n negation) Kind() string {
|
|
||||||
return KindNot
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n negation) String() string {
|
|
||||||
child := n.statement.String()
|
|
||||||
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Not(cstor Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmt, err := cstor()
|
|
||||||
return negation{statement: stmt}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type connective struct {
|
|
||||||
kind string
|
|
||||||
statements []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connective) Kind() string {
|
|
||||||
return c.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connective) String() string {
|
|
||||||
childs := make([]string, len(c.statements))
|
|
||||||
for i, statement := range c.statements {
|
|
||||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func And(cstors ...Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmts, err := assemble(cstors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return connective{kind: KindAnd, statements: stmts}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Or(cstors ...Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmts, err := assemble(cstors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return connective{kind: KindOr, statements: stmts}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type wildcard struct {
|
|
||||||
selector selpkg.Selector
|
|
||||||
pattern glob
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n wildcard) Kind() string {
|
|
||||||
return KindLike
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n wildcard) String() string {
|
|
||||||
return fmt.Sprintf(`["%s", "%s", "%s"]`, n.Kind(), n.selector, n.pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Like(selector string, pattern string) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
g, err := parseGlob(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return wildcard{selector: sel, pattern: g}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type quantifier struct {
|
|
||||||
kind string
|
|
||||||
selector selpkg.Selector
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n quantifier) Kind() string {
|
|
||||||
return n.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n quantifier) String() string {
|
|
||||||
child := n.statement.String()
|
|
||||||
return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func All(selector string, cstor Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmt, err := cstor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return quantifier{kind: KindAll, selector: sel, statement: stmt}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Any(selector string, cstor Constructor) Constructor {
|
|
||||||
return func() (Statement, error) {
|
|
||||||
stmt, err := cstor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sel, err := selpkg.Parse(selector)
|
|
||||||
return quantifier{kind: KindAny, selector: sel, statement: stmt}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func assemble(cstors []Constructor) ([]Statement, error) {
|
|
||||||
stmts := make([]Statement, 0, len(cstors))
|
|
||||||
for _, cstor := range cstors {
|
|
||||||
stmt, err := cstor()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stmts = append(stmts, stmt)
|
|
||||||
}
|
|
||||||
return stmts, nil
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package policy_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExamplePolicy() {
|
|
||||||
pol := policy.MustConstruct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
),
|
|
||||||
policy.Any(".tags", policy.Or(
|
|
||||||
policy.Equal(".", literal.String("news")),
|
|
||||||
policy.Equal(".", literal.String("press")),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
fmt.Println(pol)
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// [
|
|
||||||
// ["==", ".status", "draft"],
|
|
||||||
// ["all", ".reviewer",
|
|
||||||
// ["like", ".email", "*@example.com"]],
|
|
||||||
// ["any", ".tags",
|
|
||||||
// ["or", [
|
|
||||||
// ["==", ".", "news"],
|
|
||||||
// ["==", ".", "press"]]]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConstruct(t *testing.T) {
|
|
||||||
pol, err := policy.Construct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, pol)
|
|
||||||
|
|
||||||
// check if errors cascade correctly
|
|
||||||
pol, err = policy.Construct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer", policy.Or(
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
policy.Like(".", "\\"), // invalid pattern
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, pol)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTokenizeUTF8(t *testing.T) {
|
|
||||||
t.Run("simple UTF-8", func(t *testing.T) {
|
|
||||||
str := ".こんにちは[0]"
|
|
||||||
expected := []string{".", "こんにちは", "[0]"}
|
|
||||||
actual := tokenize(str)
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("UTF-8 with quotes", func(t *testing.T) {
|
|
||||||
str := ".こんにちは[\"привет\"]"
|
|
||||||
expected := []string{".", "こんにちは", "[\"привет\"]"}
|
|
||||||
actual := tokenize(str)
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("UTF-8 with escaped quotes", func(t *testing.T) {
|
|
||||||
str := ".こんにちは[\"привет \\\"мир\\\"\"]"
|
|
||||||
expected := []string{".", "こんにちは", "[\"привет \\\"мир\\\"\"]"}
|
|
||||||
actual := tokenize(str)
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Selector describes a UCAN policy selector, as specified here:
|
|
||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
|
||||||
type Selector []segment
|
|
||||||
|
|
||||||
// Select perform the selection described by the selector on the input IPLD DAG.
|
|
||||||
// If no error, Select returns either one ipld.Node or a []ipld.Node.
|
|
||||||
func (s Selector) Select(subject ipld.Node) (ipld.Node, []ipld.Node, error) {
|
|
||||||
return resolve(s, subject, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MatchPath tells if the selector operates on the given (string only) path segments.
|
|
||||||
// It returns the segments that didn't get consumed by the matching.
|
|
||||||
func (s Selector) MatchPath(pathSegment ...string) (bool, []string) {
|
|
||||||
return matchPath(s, pathSegment)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Selector) String() string {
|
|
||||||
var res strings.Builder
|
|
||||||
for _, seg := range s {
|
|
||||||
res.WriteString(seg.String())
|
|
||||||
}
|
|
||||||
return res.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type segment struct {
|
|
||||||
str string
|
|
||||||
identity bool
|
|
||||||
optional bool
|
|
||||||
iterator bool
|
|
||||||
slice []int
|
|
||||||
field string
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the segment's string representation.
|
|
||||||
func (s segment) String() string {
|
|
||||||
return s.str
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identity flags that this selector is the identity selector.
|
|
||||||
func (s segment) Identity() bool {
|
|
||||||
return s.identity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional flags that this selector is optional.
|
|
||||||
func (s segment) Optional() bool {
|
|
||||||
return s.optional
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterator flags that this selector is an iterator segment.
|
|
||||||
func (s segment) Iterator() bool {
|
|
||||||
return s.iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slice flags that this segment targets a range of a slice.
|
|
||||||
func (s segment) Slice() []int {
|
|
||||||
return s.slice
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field is the name of a field in a struct/map.
|
|
||||||
func (s segment) Field() string {
|
|
||||||
return s.field
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index is an index of a slice.
|
|
||||||
func (s segment) Index() int {
|
|
||||||
return s.index
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) {
|
|
||||||
cur := subject
|
|
||||||
for i, seg := range sel {
|
|
||||||
if seg.Identity() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1st level: handle the different segment types (iterator, field, slice, index)
|
|
||||||
// 2nd level: handle different node kinds (list, map, string, bytes)
|
|
||||||
switch {
|
|
||||||
case seg.Iterator():
|
|
||||||
if cur == nil || cur.Kind() == datamodel.Kind_Null {
|
|
||||||
if seg.Optional() {
|
|
||||||
// build empty list
|
|
||||||
nb := basicnode.Prototype.List.NewBuilder()
|
|
||||||
assembler, err := nb.BeginList(0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = assembler.Finish(); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nb.Build(), nil, nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var many []ipld.Node
|
|
||||||
switch cur.Kind() {
|
|
||||||
case datamodel.Kind_List:
|
|
||||||
it := cur.ListIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if there are more iterator segments
|
|
||||||
if len(sel) > i+1 && sel[i+1].Iterator() {
|
|
||||||
if v.Kind() == datamodel.Kind_List {
|
|
||||||
// recursively resolve the remaining selector segments
|
|
||||||
var o ipld.Node
|
|
||||||
var m []ipld.Node
|
|
||||||
o, m, err = resolve(sel[i+1:], v, at)
|
|
||||||
if err != nil {
|
|
||||||
// if the segment is optional and an error occurs, skip the current iteration.
|
|
||||||
if seg.Optional() {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m != nil {
|
|
||||||
many = append(many, m...)
|
|
||||||
} else if o != nil {
|
|
||||||
many = append(many, o)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if the current value is not a list and the next segment is optional, skip the current iteration
|
|
||||||
if sel[i+1].Optional() {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if there are no more iterator segments, append the current value to the result
|
|
||||||
many = append(many, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case datamodel.Kind_Map:
|
|
||||||
it := cur.MapIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sel) > i+1 && sel[i+1].Iterator() {
|
|
||||||
if v.Kind() == datamodel.Kind_List {
|
|
||||||
var o ipld.Node
|
|
||||||
var m []ipld.Node
|
|
||||||
o, m, err = resolve(sel[i+1:], v, at)
|
|
||||||
if err != nil {
|
|
||||||
if seg.Optional() {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if m != nil {
|
|
||||||
many = append(many, m...)
|
|
||||||
} else if o != nil {
|
|
||||||
many = append(many, o)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if sel[i+1].Optional() {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
many = append(many, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, many, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
case seg.Field() != "":
|
|
||||||
at = append(at, seg.Field())
|
|
||||||
if cur == nil {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch cur.Kind() {
|
|
||||||
case datamodel.Kind_Map:
|
|
||||||
n, err := cur.LookupByString(seg.Field())
|
|
||||||
if err != nil {
|
|
||||||
if isMissing(err) {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cur = n
|
|
||||||
}
|
|
||||||
case datamodel.Kind_List:
|
|
||||||
var many []ipld.Node
|
|
||||||
it := cur.ListIterator()
|
|
||||||
for !it.Done() {
|
|
||||||
_, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
if v.Kind() == datamodel.Kind_Map {
|
|
||||||
n, err := v.LookupByString(seg.Field())
|
|
||||||
if err == nil {
|
|
||||||
many = append(many, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(many) > 0 {
|
|
||||||
cur = nil
|
|
||||||
return nil, many, nil
|
|
||||||
} else if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("no elements in list have field named: %s", seg.Field()), at)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case seg.Slice() != nil:
|
|
||||||
if cur == nil {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
slice := seg.Slice()
|
|
||||||
var start, end, length int64
|
|
||||||
switch cur.Kind() {
|
|
||||||
case datamodel.Kind_List:
|
|
||||||
length = cur.Length()
|
|
||||||
start, end = resolveSliceIndices(slice, length)
|
|
||||||
case datamodel.Kind_Bytes:
|
|
||||||
b, _ := cur.AsBytes()
|
|
||||||
length = int64(len(b))
|
|
||||||
start, end = resolveSliceIndices(slice, length)
|
|
||||||
case datamodel.Kind_String:
|
|
||||||
str, _ := cur.AsString()
|
|
||||||
length = int64(len(str))
|
|
||||||
start, end = resolveSliceIndices(slice, length)
|
|
||||||
default:
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
|
||||||
}
|
|
||||||
|
|
||||||
if start < 0 || end < start || end > length {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("slice out of bounds: [%d:%d]", start, end), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch cur.Kind() {
|
|
||||||
case datamodel.Kind_List:
|
|
||||||
if end > cur.Length() {
|
|
||||||
end = cur.Length()
|
|
||||||
}
|
|
||||||
nb := basicnode.Prototype.List.NewBuilder()
|
|
||||||
assembler, _ := nb.BeginList(int64(end - start))
|
|
||||||
for i := start; i < end; i++ {
|
|
||||||
item, _ := cur.LookupByIndex(int64(i))
|
|
||||||
assembler.AssembleValue().AssignNode(item)
|
|
||||||
}
|
|
||||||
assembler.Finish()
|
|
||||||
cur = nb.Build()
|
|
||||||
case datamodel.Kind_Bytes:
|
|
||||||
b, _ := cur.AsBytes()
|
|
||||||
l := int64(len(b))
|
|
||||||
if end > l {
|
|
||||||
end = l
|
|
||||||
}
|
|
||||||
cur = basicnode.NewBytes(b[start:end])
|
|
||||||
case datamodel.Kind_String:
|
|
||||||
str, _ := cur.AsString()
|
|
||||||
l := int64(len(str))
|
|
||||||
if end > l {
|
|
||||||
end = l
|
|
||||||
}
|
|
||||||
cur = basicnode.NewString(str[start:end])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
default: // Index()
|
|
||||||
at = append(at, fmt.Sprintf("%d", seg.Index()))
|
|
||||||
if cur == nil {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
idx := seg.Index()
|
|
||||||
switch cur.Kind() {
|
|
||||||
case datamodel.Kind_List:
|
|
||||||
if idx < 0 {
|
|
||||||
idx = int(cur.Length()) + idx
|
|
||||||
}
|
|
||||||
if idx < 0 || idx >= int(cur.Length()) {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cur, _ = cur.LookupByIndex(int64(idx))
|
|
||||||
}
|
|
||||||
case datamodel.Kind_String:
|
|
||||||
str, _ := cur.AsString()
|
|
||||||
if idx < 0 {
|
|
||||||
idx = len(str) + idx
|
|
||||||
}
|
|
||||||
if idx < 0 || idx >= len(str) {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cur = basicnode.NewString(string(str[idx]))
|
|
||||||
}
|
|
||||||
case datamodel.Kind_Bytes:
|
|
||||||
b, _ := cur.AsBytes()
|
|
||||||
if idx < 0 {
|
|
||||||
idx = len(b) + idx
|
|
||||||
}
|
|
||||||
if idx < 0 || idx >= len(b) {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cur = basicnode.NewInt(int64(b[idx]))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cur, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchPath(sel Selector, path []string) (bool, []string) {
|
|
||||||
for _, seg := range sel {
|
|
||||||
if len(path) == 0 {
|
|
||||||
return true, path
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case seg.Identity():
|
|
||||||
continue
|
|
||||||
|
|
||||||
case seg.Iterator():
|
|
||||||
// we have reached a [] iterator, it should have matched earlier
|
|
||||||
return false, nil
|
|
||||||
|
|
||||||
case seg.Field() != "":
|
|
||||||
// if exact match on the segment, we continue
|
|
||||||
if path[0] == seg.Field() {
|
|
||||||
path = path[1:]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
|
|
||||||
case seg.Slice() != nil:
|
|
||||||
// we have reached a [<int>:<int>] slicing, it should have matched earlier
|
|
||||||
return false, nil
|
|
||||||
|
|
||||||
default: // Index()
|
|
||||||
// we have reached a [<int>] indexing, it should have matched earlier
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true, path
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveSliceIndices resolves the start and end indices for slicing a list or byte array.
|
|
||||||
//
|
|
||||||
// It takes the slice indices from the selector segment and the length of the list or byte array,
|
|
||||||
// and returns the resolved start and end indices. Negative indices are supported.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - slice: The slice indices from the selector segment.
|
|
||||||
// - length: The length of the list or byte array being sliced.
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - start: The resolved start index for slicing.
|
|
||||||
// - end: The resolved end index for slicing.
|
|
||||||
func resolveSliceIndices(slice []int, length int64) (int64, int64) {
|
|
||||||
start, end := int64(0), length
|
|
||||||
if len(slice) > 0 {
|
|
||||||
start = int64(slice[0])
|
|
||||||
if start < 0 {
|
|
||||||
start = length + start
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(slice) > 1 {
|
|
||||||
end = int64(slice[1])
|
|
||||||
if end <= 0 {
|
|
||||||
end = length + end
|
|
||||||
if end < start {
|
|
||||||
end = start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return start, end
|
|
||||||
}
|
|
||||||
|
|
||||||
func kindString(n datamodel.Node) string {
|
|
||||||
if n == nil {
|
|
||||||
return "null"
|
|
||||||
}
|
|
||||||
return n.Kind().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMissing(err error) bool {
|
|
||||||
if _, ok := err.(datamodel.ErrNotExists); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := err.(schema.ErrNoSuchField); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := err.(schema.ErrInvalidKey); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type resolutionerr struct {
|
|
||||||
msg string
|
|
||||||
at []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) Name() string {
|
|
||||||
return "ResolutionError"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) Message() string {
|
|
||||||
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) At() []string {
|
|
||||||
return r.at
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) Error() string {
|
|
||||||
return r.Message()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newResolutionError(message string, at []string) error {
|
|
||||||
return resolutionerr{message, at}
|
|
||||||
}
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
// Package delegation implements the UCAN [delegation] specification with
|
|
||||||
// an immutable Token type as well as methods to convert the Token to and
|
|
||||||
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
|
||||||
// should most commonly be used for transport and storage.
|
|
||||||
//
|
|
||||||
// [delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
|
||||||
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
package delegation
|
|
||||||
|
|
||||||
// TODO: change the "delegation" link above when the specification is merged
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token is an immutable type that holds the fields of a UCAN delegation.
|
|
||||||
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.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
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a validated Token from the provided parameters and options.
|
|
||||||
//
|
|
||||||
// When creating a delegated token, the Issuer's (iss) DID is assembed
|
|
||||||
// using the public key associated with the private key sent as the first
|
|
||||||
// parameter.
|
|
||||||
func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
||||||
iss, err := did.FromPrivKey(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn := &Token{
|
|
||||||
issuer: iss,
|
|
||||||
audience: aud,
|
|
||||||
subject: did.Undef,
|
|
||||||
command: cmd,
|
|
||||||
policy: pol,
|
|
||||||
meta: meta.NewMeta(),
|
|
||||||
nonce: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, opt := range opts {
|
|
||||||
if err := opt(tkn); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tkn.nonce) == 0 {
|
|
||||||
tkn.nonce, err = generateNonce()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tkn.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root creates a validated UCAN delegation Token from the provided
|
|
||||||
// parameters and options.
|
|
||||||
//
|
|
||||||
// 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(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
|
||||||
sub, err := did.FromPrivKey(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, WithSubject(sub))
|
|
||||||
|
|
||||||
return New(privKey, aud, cmd, pol, opts...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
|
||||||
func (t *Token) Issuer() did.DID {
|
|
||||||
return t.issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audience returns the did.DID representing the Token's audience.
|
|
||||||
func (t *Token) Audience() did.DID {
|
|
||||||
return t.audience
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subject returns the did.DID representing the Token's subject.
|
|
||||||
//
|
|
||||||
// This field may be did.Undef for delegations that are [Powerlined] but
|
|
||||||
// must be equal to the value returned by the Issuer method for root
|
|
||||||
// tokens.
|
|
||||||
func (t *Token) Subject() did.DID {
|
|
||||||
return t.subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command returns the capability's command.Command.
|
|
||||||
func (t *Token) Command() command.Command {
|
|
||||||
return t.command
|
|
||||||
}
|
|
||||||
|
|
||||||
// Policy returns the capability's policy.Policy.
|
|
||||||
func (t *Token) Policy() policy.Policy {
|
|
||||||
return t.policy
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nonce returns the random Nonce encapsulated in this Token.
|
|
||||||
func (t *Token) Nonce() []byte {
|
|
||||||
return t.nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meta returns the Token's metadata.
|
|
||||||
func (t *Token) Meta() *meta.Meta {
|
|
||||||
return t.meta
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotBefore returns the time at which the Token becomes "active".
|
|
||||||
func (t *Token) NotBefore() *time.Time {
|
|
||||||
return t.notBefore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expiration returns the time at which the Token expires.
|
|
||||||
func (t *Token) Expiration() *time.Time {
|
|
||||||
return t.expiration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) validate() error {
|
|
||||||
var errs error
|
|
||||||
|
|
||||||
requiredDID := func(id did.DID, fieldname string) {
|
|
||||||
if !id.Defined() {
|
|
||||||
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredDID(t.issuer, "Issuer")
|
|
||||||
requiredDID(t.audience, "Audience")
|
|
||||||
|
|
||||||
if len(t.nonce) < 12 {
|
|
||||||
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenFromModel build a decoded view of the raw IPLD data.
|
|
||||||
// This function also serves as validation.
|
|
||||||
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|
||||||
var (
|
|
||||||
tkn Token
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
tkn.issuer, err = did.Parse(m.Iss)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse iss: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn.audience, err = did.Parse(m.Aud)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse audience: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Sub != nil {
|
|
||||||
tkn.subject, err = did.Parse(*m.Sub)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse subject: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tkn.subject = did.Undef
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn.command, err = command.Parse(m.Cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn.policy, err = policy.FromIPLD(m.Pol)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse policy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Nonce) == 0 {
|
|
||||||
return nil, fmt.Errorf("nonce is required")
|
|
||||||
}
|
|
||||||
tkn.nonce = m.Nonce
|
|
||||||
|
|
||||||
tkn.meta = &m.Meta
|
|
||||||
|
|
||||||
if m.Nbf != nil {
|
|
||||||
t := time.Unix(*m.Nbf, 0)
|
|
||||||
tkn.notBefore = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Exp != nil {
|
|
||||||
t := time.Unix(*m.Exp, 0)
|
|
||||||
tkn.expiration = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tkn.validate(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateNonce creates a 12-byte random nonce.
|
|
||||||
// TODO: some crypto scheme require more, is that our case?
|
|
||||||
func generateNonce() ([]byte, error) {
|
|
||||||
res := make([]byte, 12)
|
|
||||||
_, err := rand.Read(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +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
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
package delegation_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The following example shows how to create a delegation.Token with
|
|
||||||
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
|
|
||||||
func ExampleNew() {
|
|
||||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
issDid, err := did.FromPubKey(issPub)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
fmt.Println("issDid:", issDid)
|
|
||||||
|
|
||||||
audDid := did.MustParse(AudienceDID)
|
|
||||||
subDid := did.MustParse(subjectDID)
|
|
||||||
|
|
||||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
|
||||||
cmd := command.MustParse("/foo/bar")
|
|
||||||
|
|
||||||
// The policy defines what is allowed to do.
|
|
||||||
pol := policy.MustConstruct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
),
|
|
||||||
policy.Any(".tags", policy.Or(
|
|
||||||
policy.Equal(".", literal.String("news")),
|
|
||||||
policy.Equal(".", literal.String("press")),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
tkn, err := delegation.New(issPriv, audDid, cmd, pol,
|
|
||||||
delegation.WithSubject(subDid),
|
|
||||||
delegation.WithExpirationIn(time.Hour),
|
|
||||||
delegation.WithNotBeforeIn(time.Minute),
|
|
||||||
delegation.WithMeta("foo", "bar"),
|
|
||||||
delegation.WithMeta("baz", 123),
|
|
||||||
)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
// "Seal", meaning encode and wrap into a signed envelope.
|
|
||||||
data, id, err := tkn.ToSealed(issPriv)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
printCIDAndSealed(id, data)
|
|
||||||
|
|
||||||
// Example output:
|
|
||||||
//
|
|
||||||
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe
|
|
||||||
//
|
|
||||||
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW
|
|
||||||
//
|
|
||||||
// DAG-CBOR (base64) out: glhA5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDWzqY2lzc3g4ZGlkOmtleTp6Nk1raFZGem5QZVI1NzJyVEs1MVVqb1ROcG5GOGN4dVdmUG05b0JNUHI3eThBQmVjbmJmGmcNXxZjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3RBMXVCZENwcTR1SkJxRTlqak1pTHl4WkJnOWE2eGdQUEtKak1xc3M2WmMyZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMu0HMgJ5Y+M84I/66
|
|
||||||
//
|
|
||||||
// Converted to DAG-JSON out:
|
|
||||||
// [
|
|
||||||
// {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "h": {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "NO0BcQ"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "ucan/dlg@1.0.0-rc.1": {
|
|
||||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
|
||||||
// "cmd": "/foo/bar",
|
|
||||||
// "exp": 1728933098,
|
|
||||||
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
|
||||||
// "meta": {
|
|
||||||
// "baz": 123,
|
|
||||||
// "foo": "bar"
|
|
||||||
// },
|
|
||||||
// "nbf": 1728929558,
|
|
||||||
// "nonce": {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "u0HMgJ5Y+M84I/66"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "pol": [
|
|
||||||
// [
|
|
||||||
// "==",
|
|
||||||
// ".status",
|
|
||||||
// "draft"
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "all",
|
|
||||||
// ".reviewer",
|
|
||||||
// [
|
|
||||||
// "like",
|
|
||||||
// ".email",
|
|
||||||
// "*@example.com"
|
|
||||||
// ]
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "any",
|
|
||||||
// ".tags",
|
|
||||||
// [
|
|
||||||
// "or",
|
|
||||||
// [
|
|
||||||
// [
|
|
||||||
// "==",
|
|
||||||
// ".",
|
|
||||||
// "news"
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "==",
|
|
||||||
// ".",
|
|
||||||
// "press"
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// ],
|
|
||||||
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following example shows how to create a UCAN root delegation.Token
|
|
||||||
// - a delegation.Token with the subject (sub) set to the value of issuer
|
|
||||||
// (iss).
|
|
||||||
func ExampleRoot() {
|
|
||||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
issDid, err := did.FromPubKey(issPub)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
fmt.Println("issDid:", issDid)
|
|
||||||
|
|
||||||
audDid := did.MustParse(AudienceDID)
|
|
||||||
|
|
||||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
|
||||||
cmd := command.MustParse("/foo/bar")
|
|
||||||
|
|
||||||
// The policy defines what is allowed to do.
|
|
||||||
pol := policy.MustConstruct(
|
|
||||||
policy.Equal(".status", literal.String("draft")),
|
|
||||||
policy.All(".reviewer",
|
|
||||||
policy.Like(".email", "*@example.com"),
|
|
||||||
),
|
|
||||||
policy.Any(".tags", policy.Or(
|
|
||||||
policy.Equal(".", literal.String("news")),
|
|
||||||
policy.Equal(".", literal.String("press")),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
|
|
||||||
tkn, err := delegation.Root(issPriv, audDid, cmd, pol,
|
|
||||||
delegation.WithExpirationIn(time.Hour),
|
|
||||||
delegation.WithNotBeforeIn(time.Minute),
|
|
||||||
delegation.WithMeta("foo", "bar"),
|
|
||||||
delegation.WithMeta("baz", 123),
|
|
||||||
)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
// "Seal", meaning encode and wrap into a signed envelope.
|
|
||||||
data, id, err := tkn.ToSealed(issPriv)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
printCIDAndSealed(id, data)
|
|
||||||
|
|
||||||
// Example output:
|
|
||||||
//
|
|
||||||
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
|
|
||||||
//
|
|
||||||
// CID (base58BTC): zdpuAwLojgfvFCbjz2FsKrvN1khDQ9mFGT6b6pxjMfz73Roed
|
|
||||||
//
|
|
||||||
// DAG-CBOR (base64) out: glhA6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDW0wY2lzc3g4ZGlkOmtleTp6Nk1rbldKcXoxN1k0QWZzWFNKVUZLb211QlI0R1RrVmlNN2tKWXV0elRNa0N5RkZjbmJmGmcNX1xjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa25XSnF6MTdZNEFmc1hTSlVGS29tdUJSNEdUa1ZpTTdrSll1dHpUTWtDeUZGZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMJOsjYi1Pq3OIB0La
|
|
||||||
//
|
|
||||||
// Converted to DAG-JSON out:
|
|
||||||
// [
|
|
||||||
// {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "h": {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "NO0BcQ"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "ucan/dlg@1.0.0-rc.1": {
|
|
||||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
|
||||||
// "cmd": "/foo/bar",
|
|
||||||
// "exp": 1728933168,
|
|
||||||
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
|
|
||||||
// "meta": {
|
|
||||||
// "baz": 123,
|
|
||||||
// "foo": "bar"
|
|
||||||
// },
|
|
||||||
// "nbf": 1728929628,
|
|
||||||
// "nonce": {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "JOsjYi1Pq3OIB0La"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "pol": [
|
|
||||||
// [
|
|
||||||
// "==",
|
|
||||||
// ".status",
|
|
||||||
// "draft"
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "all",
|
|
||||||
// ".reviewer",
|
|
||||||
// [
|
|
||||||
// "like",
|
|
||||||
// ".email",
|
|
||||||
// "*@example.com"
|
|
||||||
// ]
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "any",
|
|
||||||
// ".tags",
|
|
||||||
// [
|
|
||||||
// "or",
|
|
||||||
// [
|
|
||||||
// [
|
|
||||||
// "==",
|
|
||||||
// ".",
|
|
||||||
// "news"
|
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "==",
|
|
||||||
// ".",
|
|
||||||
// "press"
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// ],
|
|
||||||
// "sub": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following example demonstrates how to get a delegation.Token from
|
|
||||||
// a DAG-CBOR []byte.
|
|
||||||
func ExampleToken_FromSealed() {
|
|
||||||
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
|
||||||
|
|
||||||
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
tkn, c, err := delegation.FromSealed(cborBytes)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(c))
|
|
||||||
fmt.Println("Issuer (iss):", tkn.Issuer().String())
|
|
||||||
fmt.Println("Audience (aud):", tkn.Audience().String())
|
|
||||||
fmt.Println("Subject (sub):", tkn.Subject().String())
|
|
||||||
fmt.Println("Command (cmd):", tkn.Command().String())
|
|
||||||
fmt.Println("Policy (pol):", tkn.Policy().String())
|
|
||||||
fmt.Println("Nonce (nonce):", hex.EncodeToString(tkn.Nonce()))
|
|
||||||
fmt.Println("Meta (meta):", tkn.Meta().String())
|
|
||||||
fmt.Println("NotBefore (nbf):", tkn.NotBefore())
|
|
||||||
fmt.Println("Expiration (exp):", tkn.Expiration())
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
|
|
||||||
// Issuer (iss): did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2
|
|
||||||
// Audience (aud): did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv
|
|
||||||
// Subject (sub): did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2
|
|
||||||
// Command (cmd): /foo/bar
|
|
||||||
// Policy (pol): [
|
|
||||||
// ["==", ".status", "draft"],
|
|
||||||
// ["all", ".reviewer",
|
|
||||||
// ["like", ".email", "*@example.com"]],
|
|
||||||
// ["any", ".tags",
|
|
||||||
// ["or", [
|
|
||||||
// ["==", ".", "news"],
|
|
||||||
// ["==", ".", "press"]]]
|
|
||||||
// ]
|
|
||||||
// ]
|
|
||||||
// Nonce (nonce): 000102030405060708090a0b
|
|
||||||
// Meta (meta): {}
|
|
||||||
// NotBefore (nbf): <nil>
|
|
||||||
// Expiration (exp): <nil>
|
|
||||||
}
|
|
||||||
|
|
||||||
func printCIDAndSealed(id cid.Cid, data []byte) {
|
|
||||||
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(id))
|
|
||||||
fmt.Println("DAG-CBOR (base64) out:", base64.StdEncoding.EncodeToString(data))
|
|
||||||
fmt.Println("Converted to DAG-JSON out:")
|
|
||||||
|
|
||||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
rawJSON, err := ipld.Encode(node, dagjson.Encode)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
prettyJSON := &bytes.Buffer{}
|
|
||||||
err = json.Indent(prettyJSON, rawJSON, "", "\t")
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
fmt.Println(prettyJSON.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func printThenPanicOnErr(err error) {
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"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/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToSealed wraps the delegation token in an envelope, generates the
|
|
||||||
// signature, encodes the result to DAG-CBOR and calculates the CID of
|
|
||||||
// the resulting binary data.
|
|
||||||
func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
|
|
||||||
data, err := t.ToDagCbor(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := envelope.CIDFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
|
||||||
func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error) {
|
|
||||||
cidWriter := envelope.NewCIDWriter(w)
|
|
||||||
|
|
||||||
if err := t.ToDagCborWriter(cidWriter, privKey); err != nil {
|
|
||||||
return cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cidWriter.CID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromSealed decodes the provided binary data from the DAG-CBOR format,
|
|
||||||
// verifies that the envelope's signature is correct based on the public
|
|
||||||
// key taken from the issuer (iss) field and calculates the CID of the
|
|
||||||
// incoming data.
|
|
||||||
func FromSealed(data []byte) (*Token, cid.Cid, error) {
|
|
||||||
tkn, err := FromDagCbor(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := envelope.CIDFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
|
||||||
func FromSealedReader(r io.Reader) (*Token, cid.Cid, error) {
|
|
||||||
cidReader := envelope.NewCIDReader(r)
|
|
||||||
|
|
||||||
tkn, err := FromDagCborReader(cidReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := cidReader.CID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode marshals a Token to the format specified by the provided
|
|
||||||
// codec.Encoder.
|
|
||||||
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
|
|
||||||
node, err := t.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 (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error {
|
|
||||||
node, err := t.toIPLD(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipld.EncodeStreaming(w, node, encFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
|
||||||
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
|
|
||||||
return t.Encode(privKey, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
|
|
||||||
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
|
|
||||||
return t.EncodeWriter(w, privKey, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJson marshals the Token to the DAG-JSON format.
|
|
||||||
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
|
|
||||||
return t.Encode(privKey, dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
|
||||||
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
|
|
||||||
return t.EncodeWriter(w, privKey, dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode unmarshals the input data using the format specified by the
|
|
||||||
// provided codec.Decoder into a Token.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
func Decode(b []byte, decFn codec.Decoder) (*Token, 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) (*Token, error) {
|
|
||||||
node, err := ipld.DecodeStreaming(r, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return FromIPLD(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCbor unmarshals the input data into a Token.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
func FromDagCbor(data []byte) (*Token, error) {
|
|
||||||
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, err := tokenFromModel(*pay)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
|
||||||
func FromDagCborReader(r io.Reader) (*Token, error) {
|
|
||||||
return DecodeReader(r, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJson unmarshals the input data into a Token.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
func FromDagJson(data []byte) (*Token, error) {
|
|
||||||
return Decode(data, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
|
||||||
func FromDagJsonReader(r io.Reader) (*Token, error) {
|
|
||||||
return DecodeReader(r, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromIPLD decode the given IPLD representation into a Token.
|
|
||||||
func FromIPLD(node datamodel.Node) (*Token, error) {
|
|
||||||
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, err := tokenFromModel(*pay)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
|
||||||
var sub *string
|
|
||||||
|
|
||||||
if t.subject != did.Undef {
|
|
||||||
s := t.subject.String()
|
|
||||||
sub = &s
|
|
||||||
}
|
|
||||||
|
|
||||||
pol, err := t.policy.ToIPLD()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var nbf *int64
|
|
||||||
if t.notBefore != nil {
|
|
||||||
u := t.notBefore.Unix()
|
|
||||||
nbf = &u
|
|
||||||
}
|
|
||||||
|
|
||||||
var exp *int64
|
|
||||||
if t.expiration != nil {
|
|
||||||
u := t.expiration.Unix()
|
|
||||||
exp = &u
|
|
||||||
}
|
|
||||||
|
|
||||||
model := &tokenPayloadModel{
|
|
||||||
Iss: t.issuer.String(),
|
|
||||||
Aud: t.audience.String(),
|
|
||||||
Sub: sub,
|
|
||||||
Cmd: t.command.String(),
|
|
||||||
Pol: pol,
|
|
||||||
Nonce: t.nonce,
|
|
||||||
Meta: *t.meta,
|
|
||||||
Nbf: nbf,
|
|
||||||
Exp: exp,
|
|
||||||
}
|
|
||||||
|
|
||||||
return envelope.ToIPLD(privKey, model)
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
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
|
|
||||||
// creation of a Token.
|
|
||||||
type Option func(*Token) error
|
|
||||||
|
|
||||||
// WithExpiration set's the Token's optional "expiration" field to the
|
|
||||||
// value of the provided time.Time.
|
|
||||||
func WithExpiration(exp time.Time) Option {
|
|
||||||
return func(t *Token) error {
|
|
||||||
if exp.Before(time.Now()) {
|
|
||||||
return fmt.Errorf("a Token's expiration should be set to a time in the future: %s", exp.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.expiration = &exp
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithExpirationIn set's the Token's optional "expiration" field to Now() plus the given duration.
|
|
||||||
func WithExpirationIn(exp time.Duration) Option {
|
|
||||||
return func(t *Token) error {
|
|
||||||
expTime := time.Now().Add(exp)
|
|
||||||
t.expiration = &expTime
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMeta adds a key/value pair in the "meta" field.
|
|
||||||
//
|
|
||||||
// WithMeta can be used multiple times in the same call.
|
|
||||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
|
||||||
// and ipld.Node.
|
|
||||||
func WithMeta(key string, val any) Option {
|
|
||||||
return func(t *Token) error {
|
|
||||||
return t.meta.Add(key, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
|
||||||
// of the provided time.Time.
|
|
||||||
func WithNotBefore(nbf time.Time) Option {
|
|
||||||
return func(t *Token) error {
|
|
||||||
if nbf.Before(time.Now()) {
|
|
||||||
return fmt.Errorf("a Token's \"not before\" field should be set to a time in the future: %s", nbf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.notBefore = &nbf
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithNotBeforeIn set's the Token's optional "notBefore" field to the value
|
|
||||||
// of the provided time.Time.
|
|
||||||
func WithNotBeforeIn(nbf time.Duration) Option {
|
|
||||||
return func(t *Token) error {
|
|
||||||
nbfTime := time.Now().Add(nbf)
|
|
||||||
t.notBefore = &nbfTime
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return func(t *Token) error {
|
|
||||||
t.nonce = nonce
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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/node/bindnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
// [Tag] is the string used as a key within the SigPayload that identifies
|
|
||||||
// that the TokenPayload is a delegation.
|
|
||||||
//
|
|
||||||
// [Tag]: https://github.com/ucan-wg/delegation/tree/v1_ipld#type-tag
|
|
||||||
const Tag = "ucan/dlg@1.0.0-rc.1"
|
|
||||||
|
|
||||||
// TODO: update the above Tag URL once the delegation specification is merged.
|
|
||||||
|
|
||||||
//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")
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
|
||||||
|
|
||||||
type tokenPayloadModel 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 datamodel.Node
|
|
||||||
|
|
||||||
// A unique, random nonce
|
|
||||||
Nonce []byte
|
|
||||||
|
|
||||||
// Arbitrary Metadata
|
|
||||||
Meta meta.Meta
|
|
||||||
|
|
||||||
// "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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
|
||||||
return bindnode.Prototype((*tokenPayloadModel)(nil), payloadType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*tokenPayloadModel) Tag() string {
|
|
||||||
return Tag
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package delegation_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gotest.tools/v3/golden"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed delegation.ipldsch
|
|
||||||
var schemaBytes []byte
|
|
||||||
|
|
||||||
func TestSchemaRoundTrip(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
delegationJson := golden.Get(t, "new.dagjson")
|
|
||||||
privKey := privKey(t, issuerPrivKeyCfg)
|
|
||||||
|
|
||||||
t.Run("via buffers", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
|
||||||
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
|
|
||||||
|
|
||||||
p1, err := delegation.FromDagJson(delegationJson)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cborBytes, id, err := p1.ToSealed(privKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
|
||||||
fmt.Println("cborBytes length", len(cborBytes))
|
|
||||||
fmt.Println("cbor", string(cborBytes))
|
|
||||||
|
|
||||||
p2, c2, err := delegation.FromSealed(cborBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, id, c2)
|
|
||||||
fmt.Println("read Cbor", p2)
|
|
||||||
|
|
||||||
readJson, err := p2.ToDagJson(privKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("readJson length", len(readJson))
|
|
||||||
fmt.Println("json: ", string(readJson))
|
|
||||||
|
|
||||||
assert.JSONEq(t, string(delegationJson), string(readJson))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("via streaming", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(delegationJson)
|
|
||||||
|
|
||||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
|
||||||
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
|
|
||||||
|
|
||||||
p1, err := delegation.FromDagJsonReader(buf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cborBytes := &bytes.Buffer{}
|
|
||||||
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
|
||||||
t.Log(len(id.Bytes()), id.Bytes())
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
|
||||||
|
|
||||||
// buf = bytes.NewBuffer(cborBytes.Bytes())
|
|
||||||
p2, c2, err := delegation.FromSealedReader(cborBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(c2))
|
|
||||||
|
|
||||||
readJson := &bytes.Buffer{}
|
|
||||||
require.NoError(t, p2.ToDagJsonWriter(readJson, privKey))
|
|
||||||
|
|
||||||
assert.JSONEq(t, string(delegationJson), readJson.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkSchemaLoad(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkRoundTrip(b *testing.B) {
|
|
||||||
delegationJson := golden.Get(b, "new.dagjson")
|
|
||||||
privKey := privKey(b, issuerPrivKeyCfg)
|
|
||||||
|
|
||||||
b.Run("via buffers", func(b *testing.B) {
|
|
||||||
p1, _ := delegation.FromDagJson(delegationJson)
|
|
||||||
cborBytes, _, _ := p1.ToSealed(privKey)
|
|
||||||
p2, _, _ := delegation.FromSealed(cborBytes)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
b.Run("FromDagJson", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = delegation.FromDagJson(delegationJson)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("Seal", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _, _ = p1.ToSealed(privKey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("Unseal", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _, _ = delegation.FromSealed(cborBytes)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("ToDagJson", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = p2.ToDagJson(privKey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("via streaming", func(b *testing.B) {
|
|
||||||
p1, _ := delegation.FromDagJsonReader(bytes.NewReader(delegationJson))
|
|
||||||
cborBuf := &bytes.Buffer{}
|
|
||||||
_, _ = p1.ToSealedWriter(cborBuf, privKey)
|
|
||||||
cborBytes := cborBuf.Bytes()
|
|
||||||
p2, _, _ := delegation.FromSealedReader(bytes.NewReader(cborBytes))
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
b.Run("FromDagJsonReader", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
reader := bytes.NewReader(delegationJson)
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = reader.Seek(0, 0)
|
|
||||||
_, _ = delegation.FromDagJsonReader(reader)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("SealWriter", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
buf.Reset()
|
|
||||||
_, _ = p1.ToSealedWriter(buf, privKey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("UnsealReader", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
reader := bytes.NewReader(cborBytes)
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = reader.Seek(0, 0)
|
|
||||||
_, _, _ = delegation.FromSealedReader(reader)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("ToDagJsonReader", func(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
buf.Reset()
|
|
||||||
_ = p2.ToDagJsonWriter(buf, privKey)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
1
token/delegation/testdata/new.dagjson
vendored
1
token/delegation/testdata/new.dagjson
vendored
@@ -1 +0,0 @@
|
|||||||
[{"/":{"bytes":"FM6otj0r/noJWiGAC5WV86xAazxrF173IihuHJgEt35CtSzjeaelrR3UwaSr8xbE9sLpo5xJhUbo0QLI273hDA"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":7258118400,"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"}}]
|
|
||||||
1
token/delegation/testdata/root.dagjson
vendored
1
token/delegation/testdata/root.dagjson
vendored
@@ -1 +0,0 @@
|
|||||||
[{"/":{"bytes":"aYBq08tfm0zQZnPg/5tB9kM5mklRU9PPIkV7CK68jEgbd76JbCGuu75vfLyBu3WTqKzLSJ583pbwu668m/7MBQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":7258118400,"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"}}]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Info = envelope.Info
|
|
||||||
|
|
||||||
// Inspect inspects the given token IPLD representation and extract some envelope facts.
|
|
||||||
func Inspect(node datamodel.Node) (Info, error) {
|
|
||||||
return envelope.Inspect(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindTag inspect the given token IPLD representation and extract the token tag.
|
|
||||||
func FindTag(node datamodel.Node) (string, error) {
|
|
||||||
return envelope.FindTag(node)
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Token interface {
|
|
||||||
Marshaller
|
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
|
||||||
Issuer() did.DID
|
|
||||||
// Meta returns the Token's metadata.
|
|
||||||
Meta() *meta.Meta
|
|
||||||
}
|
|
||||||
|
|
||||||
type Marshaller interface {
|
|
||||||
// ToSealed wraps the token in an envelope, generates the signature, encodes
|
|
||||||
// the result to DAG-CBOR and calculates the CID of the resulting binary data.
|
|
||||||
ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error)
|
|
||||||
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
|
||||||
ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error)
|
|
||||||
// Encode marshals a Token to the format specified by the provided codec.Encoder.
|
|
||||||
Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error)
|
|
||||||
// EncodeWriter is the same as Encode, but accepts an io.Writer.
|
|
||||||
EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error
|
|
||||||
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
|
||||||
ToDagCbor(privKey crypto.PrivKey) ([]byte, error)
|
|
||||||
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
|
|
||||||
ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error
|
|
||||||
// ToDagJson marshals the Token to the DAG-JSON format.
|
|
||||||
ToDagJson(privKey crypto.PrivKey) ([]byte, error)
|
|
||||||
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
|
||||||
ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
package envelope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/multiformats/go-multibase"
|
|
||||||
"github.com/multiformats/go-multicodec"
|
|
||||||
"github.com/multiformats/go-multihash"
|
|
||||||
)
|
|
||||||
|
|
||||||
var b58BTCEnc = multibase.MustNewEncoder(multibase.Base58BTC)
|
|
||||||
|
|
||||||
// CIDToBase56BTC is a utility method to convert a CIDv1 to the canonical
|
|
||||||
// string representation used by UCAN.
|
|
||||||
func CIDToBase58BTC(id cid.Cid) string {
|
|
||||||
return id.Encode(b58BTCEnc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CIDFromBytes returns the UCAN content identifier for an arbitrary slice
|
|
||||||
// of bytes.
|
|
||||||
func CIDFromBytes(b []byte) (cid.Cid, error) {
|
|
||||||
return cid.V1Builder{
|
|
||||||
Codec: uint64(multicodec.DagCbor),
|
|
||||||
MhType: multihash.SHA2_256,
|
|
||||||
MhLength: 0,
|
|
||||||
}.Sum(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ io.Reader = (*CIDReader)(nil)
|
|
||||||
|
|
||||||
// CIDReader wraps an io.Reader and includes a hash.Hash that is
|
|
||||||
// incrementally updated as data is read from the child io.Reader.
|
|
||||||
type CIDReader struct {
|
|
||||||
hash hash.Hash
|
|
||||||
r io.Reader
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCIDReader initializes a hash.Hash to calculate the CID's hash and
|
|
||||||
// returns the wrapped io.Reader.
|
|
||||||
func NewCIDReader(r io.Reader) *CIDReader {
|
|
||||||
h := sha256.New()
|
|
||||||
h.Reset()
|
|
||||||
|
|
||||||
return &CIDReader{
|
|
||||||
hash: h,
|
|
||||||
r: r,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CID returns the UCAN-formatted cid.Cid created from the hash calculated
|
|
||||||
// as bytes were read from the inner io.Reader.
|
|
||||||
func (r *CIDReader) CID() (cid.Cid, error) {
|
|
||||||
if r.err != nil {
|
|
||||||
return cid.Undef, r.err // TODO: Wrap to say it's an error during streaming?
|
|
||||||
}
|
|
||||||
|
|
||||||
return cidFromHash(r.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read implements io.Reader.
|
|
||||||
func (r *CIDReader) Read(p []byte) (n int, err error) {
|
|
||||||
n, err = r.r.Read(p)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
r.err = err
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = r.hash.Write(p[:n])
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ io.Writer = (*CIDWriter)(nil)
|
|
||||||
|
|
||||||
// CIDWriter wraps an io.Writer and includes a hash.Hash that is
|
|
||||||
// incrementally updated as data is written to the child io.Writer.
|
|
||||||
type CIDWriter struct {
|
|
||||||
hash hash.Hash
|
|
||||||
w io.Writer
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCIDWriter initializes a hash.Hash to calculate the CID's hash and
|
|
||||||
// returns the wrapped io.Writer.
|
|
||||||
func NewCIDWriter(w io.Writer) *CIDWriter {
|
|
||||||
h := sha256.New()
|
|
||||||
h.Reset()
|
|
||||||
|
|
||||||
return &CIDWriter{
|
|
||||||
hash: h,
|
|
||||||
w: w,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CID returns the UCAN-formatted cid.Cid created from the hash calculated
|
|
||||||
// as bytes were written from the inner io.Reader.
|
|
||||||
func (w *CIDWriter) CID() (cid.Cid, error) {
|
|
||||||
return cidFromHash(w.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write implements io.Writer.
|
|
||||||
func (w *CIDWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if _, err = w.hash.Write(p); err != nil {
|
|
||||||
w.err = err
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.w.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func cidFromHash(hash hash.Hash) (cid.Cid, error) {
|
|
||||||
mh, err := multihash.Encode(hash.Sum(nil), multihash.SHA2_256)
|
|
||||||
if err != nil {
|
|
||||||
return cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cid.NewCidV1(uint64(multicodec.DagCbor), mh), nil
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package envelope_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"github.com/multiformats/go-multicodec"
|
|
||||||
"github.com/multiformats/go-multihash"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gotest.tools/v3/golden"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCidFromBytes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
expData := golden.Get(t, "example.dagcbor")
|
|
||||||
expHash, err := multihash.Sum(expData, uint64(multicodec.Sha2_256), -1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
id, err := envelope.CIDFromBytes(data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleCID, envelope.CIDToBase58BTC(id))
|
|
||||||
assert.Equal(t, expHash, id.Hash())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStreaming(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
expData := []byte("this is a test")
|
|
||||||
|
|
||||||
expCID, err := cid.V1Builder{
|
|
||||||
Codec: uint64(multicodec.DagCbor),
|
|
||||||
MhType: multihash.SHA2_256,
|
|
||||||
MhLength: 0,
|
|
||||||
}.Sum(expData)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("CIDReader()", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
r, w := io.Pipe() //nolint:varnamelen
|
|
||||||
cidReader := envelope.NewCIDReader(r)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_, err := w.Write(expData)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NoError(t, w.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
actData, err := io.ReadAll(cidReader)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, expData, actData)
|
|
||||||
|
|
||||||
actCID, err := cidReader.CID()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, expCID, actCID)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("CIDWriter", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
r, w := io.Pipe() //nolint:varnamelen
|
|
||||||
cidWriter := envelope.NewCIDWriter(w)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_, err := cidWriter.Write(expData)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.NoError(t, w.Close())
|
|
||||||
}()
|
|
||||||
|
|
||||||
actData, err := io.ReadAll(r)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, expData, actData)
|
|
||||||
|
|
||||||
actCID, err := cidWriter.CID()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, expCID, actCID)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package envelope_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gotest.tools/v3/golden"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
exampleCID = "zdpuAyw6R5HvKSPzztuzXNYFx3ZGoMHMuAsXL6u3xLGQriRXQ"
|
|
||||||
exampleDID = "did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"
|
|
||||||
exampleGreeting = "world"
|
|
||||||
examplePrivKeyCfg = "CAESQP9v2uqECTuIi45dyg3znQvsryvf2IXmOF/6aws6aCehm0FVrj0zHR5RZSDxWNjcpcJqsGym3sjCungX9Zt5oA4="
|
|
||||||
exampleSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBg"
|
|
||||||
exampleTag = "ucan/example@v1.0.0-rc.1"
|
|
||||||
exampleTypeName = "Example"
|
|
||||||
exampleVarsigHeaderStr = "NO0BcQ"
|
|
||||||
|
|
||||||
invalidSignatureStr = "PZV6A2aI7n+MlyADqcqmWhkuyNrgUCDz+qSLSnI9bpasOwOhKUTx95m5Nu5CO/INa1LqzHGioD9+PVf6qdtTBK"
|
|
||||||
|
|
||||||
exampleDAGCBORFilename = "example.dagcbor"
|
|
||||||
exampleDAGJSONFilename = "example.dagjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed testdata/example.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 exampleType() schema.Type {
|
|
||||||
return mustLoadSchema().TypeByName(exampleTypeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ envelope.Tokener = (*Example)(nil)
|
|
||||||
|
|
||||||
type Example struct {
|
|
||||||
Hello string
|
|
||||||
Issuer string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newExample(t *testing.T) *Example {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
return &Example{
|
|
||||||
Hello: exampleGreeting,
|
|
||||||
Issuer: exampleDID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Example) Prototype() schema.TypedPrototype {
|
|
||||||
return bindnode.Prototype(e, exampleType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*Example) Tag() string {
|
|
||||||
return exampleTag
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleGoldenNode(t *testing.T) datamodel.Node {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
cbor := golden.Get(t, exampleDAGCBORFilename)
|
|
||||||
|
|
||||||
node, err := ipld.Decode(cbor, dagcbor.Decode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func examplePrivKey(t *testing.T) crypto.PrivKey {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
privKeyEnc, err := crypto.ConfigDecodeKey(examplePrivKeyCfg)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyEnc)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return privKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func exampleSignature(t *testing.T) []byte {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
sig, err := base64.RawStdEncoding.DecodeString(exampleSignatureStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return sig
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidNodeFromGolden(t *testing.T) datamodel.Node {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
invalidSig, err := base64.RawStdEncoding.DecodeString(invalidSignatureStr)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
envelNode := exampleGoldenNode(t)
|
|
||||||
sigPayloadNode, err := envelNode.LookupByIndex(1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Bytes(invalidSig))
|
|
||||||
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
// Package envelope provides functions that convert between wire-format
|
|
||||||
// encoding of a [UCAN] token's [Envelope] and the Go type representing
|
|
||||||
// a verified [TokenPayload].
|
|
||||||
//
|
|
||||||
// Encoding functions in this package require a private key as a
|
|
||||||
// parameter so the VarsigHeader can be set and so that a
|
|
||||||
// cryptographic signature can be generated.
|
|
||||||
//
|
|
||||||
// Decoding functions in this package likewise perform the signature
|
|
||||||
// verification using a public key extracted from the TokenPayload as
|
|
||||||
// described by requirement two below.
|
|
||||||
//
|
|
||||||
// Types that wish to be marshaled and unmarshaled from the using
|
|
||||||
// is package have two requirements.
|
|
||||||
//
|
|
||||||
// 1. The type must implement the Tokener interface.
|
|
||||||
//
|
|
||||||
// 2. The IPLD Representation of the type must include an "iss"
|
|
||||||
// field when the TokenPayload is extracted from the Envelope.
|
|
||||||
// This field must contain the string representation of a
|
|
||||||
// "did:key" so that a public key can be extracted from the
|
|
||||||
//
|
|
||||||
// [Envelope]:https://github.com/ucan-wg/spec#envelope
|
|
||||||
// [TokenPayload]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
// [UCAN]: https://ucan.xyz
|
|
||||||
package envelope
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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/ipld/go-ipld-prime/fluent/qp"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"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/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/varsig"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
VarsigHeaderKey = "h"
|
|
||||||
UCANTagPrefix = "ucan/"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Tokener must be implemented by types that wish to be enclosed in a
|
|
||||||
// UCAN Envelope (presumbably one of the UCAN token types).
|
|
||||||
type Tokener interface {
|
|
||||||
// Prototype provides the schema representation for an IPLD type so
|
|
||||||
// that the incoming datamodel.Kinds can be mapped to the appropriate
|
|
||||||
// schema.Kinds.
|
|
||||||
Prototype() schema.TypedPrototype
|
|
||||||
|
|
||||||
// Tag returns the expected key denoting the name of the IPLD node
|
|
||||||
// that should be processed as the token payload while decoding
|
|
||||||
// incoming bytes.
|
|
||||||
Tag() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode unmarshals the input data using the format specified by the
|
|
||||||
// provided codec.Decoder into a Tokener.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Tokener is invalid.
|
|
||||||
func Decode[T Tokener](b []byte, decFn codec.Decoder) (T, error) {
|
|
||||||
node, err := ipld.Decode(b, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return *new(T), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return FromIPLD[T](node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
|
||||||
func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, error) {
|
|
||||||
node, err := ipld.DecodeStreaming(r, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return *new(T), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return FromIPLD[T](node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCbor unmarshals the input data into a Tokener.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Tokener is invalid.
|
|
||||||
func FromDagCbor[T Tokener](b []byte) (T, error) {
|
|
||||||
return Decode[T](b, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
|
||||||
func FromDagCborReader[T Tokener](r io.Reader) (T, error) {
|
|
||||||
return DecodeReader[T](r, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJson unmarshals the input data into a Tokener.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Tokener is invalid.
|
|
||||||
func FromDagJson[T Tokener](b []byte) (T, error) {
|
|
||||||
return Decode[T](b, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
|
||||||
func FromDagJsonReader[T Tokener](r io.Reader) (T, error) {
|
|
||||||
return DecodeReader[T](r, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromIPLD unwraps a Tokener from the provided IPLD datamodel.Node.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Tokener is invalid.
|
|
||||||
func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
|
||||||
zero := *new(T)
|
|
||||||
|
|
||||||
info, err := Inspect(node)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Tag != zero.Tag() {
|
|
||||||
return zero, errors.New("data doesn't match the expected type")
|
|
||||||
}
|
|
||||||
|
|
||||||
// This needs to be done before converting this node to its schema
|
|
||||||
// representation (afterwards, the field might be renamed os it's safer
|
|
||||||
// to use the wire name).
|
|
||||||
issuerNode, err := info.tokenPayloadNode.LookupByString("iss")
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replaces the datamodel.Node in tokenPayloadNode with a
|
|
||||||
// schema.TypedNode so that we can cast it to a *token.Token after
|
|
||||||
// unwrapping it.
|
|
||||||
nb := zero.Prototype().Representation().NewBuilder()
|
|
||||||
|
|
||||||
err = nb.AssignNode(info.tokenPayloadNode)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenPayloadNode := nb.Build()
|
|
||||||
|
|
||||||
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
|
|
||||||
if tokenPayload == nil {
|
|
||||||
return zero, errors.New("failed to Unwrap the TokenPayload")
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, ok := tokenPayload.(T)
|
|
||||||
if !ok {
|
|
||||||
return zero, errors.New("failed to assert the TokenPayload type as *token.Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the issuer's DID contains a public key with a type that
|
|
||||||
// matches the VarsigHeader and then verify the SigPayload.
|
|
||||||
issuer, err := issuerNode.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
issuerDID, err := did.Parse(issuer)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
issuerPubKey, err := issuerDID.PubKey()
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(info.VarsigHeader) != string(issuerVarsigHeader) {
|
|
||||||
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
|
|
||||||
if err != nil {
|
|
||||||
return zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err = issuerPubKey.Verify(data, info.Signature)
|
|
||||||
if err != nil || !ok {
|
|
||||||
return zero, errors.New("failed to verify the token's signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode marshals a Tokener to the format specified by the provided
|
|
||||||
// codec.Encoder.
|
|
||||||
func Encode(privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) ([]byte, error) {
|
|
||||||
node, err := ToIPLD(privKey, token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipld.Encode(node, encFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeWriter is the same as Encode but outputs to an io.Writer instead
|
|
||||||
// of encoding into a []byte.
|
|
||||||
func EncodeWriter(w io.Writer, privKey crypto.PrivKey, token Tokener, encFn codec.Encoder) error {
|
|
||||||
node, err := ToIPLD(privKey, token)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipld.EncodeStreaming(w, node, encFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCbor marshals the Tokener to the DAG-CBOR format.
|
|
||||||
func ToDagCbor(privKey crypto.PrivKey, token Tokener) ([]byte, error) {
|
|
||||||
return Encode(privKey, token, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCborWriter is the same as ToDagCbor but outputs to an io.Writer
|
|
||||||
// instead of encoding into a []byte.
|
|
||||||
func ToDagCborWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error {
|
|
||||||
return EncodeWriter(w, privKey, token, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJson marshals the Tokener to the DAG-JSON format.
|
|
||||||
func ToDagJson(privKey crypto.PrivKey, token Tokener) ([]byte, error) {
|
|
||||||
return Encode(privKey, token, dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJsonWriter is the same as ToDagJson but outputs to an io.Writer
|
|
||||||
// instead of encoding into a []byte.
|
|
||||||
func ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey, token Tokener) error {
|
|
||||||
return EncodeWriter(w, privKey, token, dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToIPLD wraps the Tokener in an IPLD datamodel.Node.
|
|
||||||
func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
|
|
||||||
tokenPayloadNode := bindnode.Wrap(token, token.Prototype().Type()).Representation()
|
|
||||||
|
|
||||||
varsigHeader, err := varsig.Encode(privKey.Type())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
|
||||||
qp.MapEntry(ma, VarsigHeaderKey, qp.Bytes(varsigHeader))
|
|
||||||
qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode))
|
|
||||||
})
|
|
||||||
|
|
||||||
data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
signature, err := privKey.Sign(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return qp.BuildList(basicnode.Prototype.Any, 2, func(la datamodel.ListAssembler) {
|
|
||||||
qp.ListEntry(la, qp.Bytes(signature))
|
|
||||||
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindTag inspects the given token IPLD representation and extract the token tag.
|
|
||||||
func FindTag(node datamodel.Node) (string, error) {
|
|
||||||
sigPayloadNode, err := node.LookupByIndex(1)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if sigPayloadNode.Kind() != datamodel.Kind_Map {
|
|
||||||
return "", fmt.Errorf("unexpected type instead of map")
|
|
||||||
}
|
|
||||||
|
|
||||||
it := sigPayloadNode.MapIterator()
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for !it.Done() {
|
|
||||||
if i >= 2 {
|
|
||||||
return "", fmt.Errorf("expected two and only two fields in SigPayload")
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
|
|
||||||
k, _, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := k.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(key, UCANTagPrefix) {
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("no token tag found")
|
|
||||||
}
|
|
||||||
|
|
||||||
type Info struct {
|
|
||||||
Tag string
|
|
||||||
Signature []byte
|
|
||||||
VarsigHeader []byte
|
|
||||||
sigPayloadNode datamodel.Node // private, we don't want to expose that
|
|
||||||
tokenPayloadNode datamodel.Node // private, we don't want to expose that
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect inspects the given token IPLD representation and extract some envelope facts.
|
|
||||||
func Inspect(node datamodel.Node) (Info, error) {
|
|
||||||
var res Info
|
|
||||||
|
|
||||||
signatureNode, err := node.LookupByIndex(0)
|
|
||||||
if err != nil {
|
|
||||||
return Info{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Signature, err = signatureNode.AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return Info{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res.sigPayloadNode, err = node.LookupByIndex(1)
|
|
||||||
if err != nil {
|
|
||||||
return Info{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.sigPayloadNode.Kind() != datamodel.Kind_Map {
|
|
||||||
return Info{}, fmt.Errorf("unexpected type instead of map")
|
|
||||||
}
|
|
||||||
|
|
||||||
it := res.sigPayloadNode.MapIterator()
|
|
||||||
foundVarsigHeader := false
|
|
||||||
foundTokenPayload := false
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for !it.Done() {
|
|
||||||
if i >= 2 {
|
|
||||||
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload")
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
|
|
||||||
k, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return Info{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := k.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return Info{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case key == VarsigHeaderKey:
|
|
||||||
foundVarsigHeader = true
|
|
||||||
res.VarsigHeader, err = v.AsBytes()
|
|
||||||
if err != nil {
|
|
||||||
return Info{}, err
|
|
||||||
}
|
|
||||||
case strings.HasPrefix(key, UCANTagPrefix):
|
|
||||||
foundTokenPayload = true
|
|
||||||
res.Tag = key
|
|
||||||
res.tokenPayloadNode = v
|
|
||||||
default:
|
|
||||||
return Info{}, fmt.Errorf("unexpected key type %q", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if i != 2 {
|
|
||||||
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
|
|
||||||
}
|
|
||||||
if !foundVarsigHeader {
|
|
||||||
return Info{}, errors.New("failed to find VarsigHeader field")
|
|
||||||
}
|
|
||||||
if !foundTokenPayload {
|
|
||||||
return Info{}, errors.New("failed to find TokenPayload field")
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package envelope_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gotest.tools/v3/golden"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDecode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("via FromDagCbor", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data := golden.Get(t, "example.dagcbor")
|
|
||||||
|
|
||||||
tkn, err := envelope.FromDagCbor[*Example](data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
|
||||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("via FromDagJson", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data := golden.Get(t, "example.dagjson")
|
|
||||||
|
|
||||||
tkn, err := envelope.FromDagJson[*Example](data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
|
||||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEncode(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("via ToDagCbor", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
golden.AssertBytes(t, data, exampleDAGCBORFilename)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("via ToDagJson", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data, err := envelope.ToDagJson(examplePrivKey(t), newExample(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
golden.Assert(t, string(data), exampleDAGJSONFilename)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRoundtrip(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("via FromDagCbor/ToDagCbor", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dataIn := golden.Get(t, exampleDAGCBORFilename)
|
|
||||||
|
|
||||||
tkn, err := envelope.FromDagCbor[*Example](dataIn)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
|
||||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
|
||||||
|
|
||||||
dataOut, err := envelope.ToDagCbor(examplePrivKey(t), newExample(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, dataIn, dataOut)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("via FromDagCborReader/ToDagCborWriter", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data := golden.Get(t, exampleDAGCBORFilename)
|
|
||||||
|
|
||||||
tkn, err := envelope.FromDagCborReader[*Example](bytes.NewReader(data))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
|
||||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
|
||||||
|
|
||||||
w := &bytes.Buffer{}
|
|
||||||
require.NoError(t, envelope.ToDagCborWriter(w, examplePrivKey(t), newExample(t)))
|
|
||||||
assert.Equal(t, data, w.Bytes())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("via FromDagJson/ToDagJson", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dataIn := golden.Get(t, exampleDAGJSONFilename)
|
|
||||||
|
|
||||||
tkn, err := envelope.FromDagJson[*Example](dataIn)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
|
||||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
|
||||||
|
|
||||||
dataOut, err := envelope.ToDagJson(examplePrivKey(t), newExample(t))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, dataIn, dataOut)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("via FromDagJsonReader/ToDagJsonrWriter", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data := golden.Get(t, exampleDAGJSONFilename)
|
|
||||||
|
|
||||||
tkn, err := envelope.FromDagJsonReader[*Example](bytes.NewReader(data))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, exampleGreeting, tkn.Hello)
|
|
||||||
assert.Equal(t, exampleDID, tkn.Issuer)
|
|
||||||
|
|
||||||
w := &bytes.Buffer{}
|
|
||||||
require.NoError(t, envelope.ToDagJsonWriter(w, examplePrivKey(t), newExample(t)))
|
|
||||||
assert.Equal(t, data, w.Bytes())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFromIPLD_with_invalid_signature(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
node := invalidNodeFromGolden(t)
|
|
||||||
tkn, err := envelope.FromIPLD[*Example](node)
|
|
||||||
assert.Nil(t, tkn)
|
|
||||||
require.EqualError(t, err, "failed to verify the token's signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHash(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
msg := []byte("this is a test")
|
|
||||||
|
|
||||||
hash1 := sha256.Sum256(msg)
|
|
||||||
|
|
||||||
hasher := sha256.New()
|
|
||||||
|
|
||||||
for _, b := range msg {
|
|
||||||
hasher.Write([]byte{b})
|
|
||||||
}
|
|
||||||
|
|
||||||
hash2 := hasher.Sum(nil)
|
|
||||||
hash3 := hasher.Sum(nil)
|
|
||||||
|
|
||||||
require.Equal(t, hash1[:], hash2)
|
|
||||||
require.Equal(t, hash1[:], hash3)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInspect(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
data := golden.Get(t, "example.dagcbor")
|
|
||||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
info, err := envelope.Inspect(node)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, expSig, info.Signature)
|
|
||||||
assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
|
|
||||||
assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzInspect(f *testing.F) {
|
|
||||||
data, err := os.ReadFile("testdata/example.dagcbor")
|
|
||||||
require.NoError(f, err)
|
|
||||||
|
|
||||||
f.Add(data)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
_, err = envelope.Inspect(node)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzFindTag(f *testing.F) {
|
|
||||||
data, err := os.ReadFile("testdata/example.dagcbor")
|
|
||||||
require.NoError(f, err)
|
|
||||||
|
|
||||||
f.Add(data)
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
_, err = envelope.FindTag(node)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
‚X@|úŸÀ½â–ðõ+ÁŠ!µ.®ÿhéÍúGO-ü¬”jÉsyÖsY¨quëiþ“ä°¬Íuý#ò¼’ç˜c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
type DID string
|
|
||||||
|
|
||||||
type Example struct {
|
|
||||||
hello String
|
|
||||||
issuer DID (rename "iss")
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
// Package invocation implements the UCAN [invocation] specification with
|
|
||||||
// an immutable Token type as well as methods to convert the Token to and
|
|
||||||
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
|
||||||
// should most commonly be used for transport and storage.
|
|
||||||
//
|
|
||||||
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
|
||||||
// [invocation]: https://github.com/ucan-wg/invocation
|
|
||||||
package invocation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Token is an immutable type that holds the fields of a UCAN invocation.
|
|
||||||
type Token struct {
|
|
||||||
// Issuer DID (invoker)
|
|
||||||
issuer did.DID
|
|
||||||
// Audience DID (receiver/executor)
|
|
||||||
audience did.DID
|
|
||||||
// Subject DID (subject being invoked)
|
|
||||||
subject did.DID
|
|
||||||
// The Command to invoke
|
|
||||||
command command.Command
|
|
||||||
// TODO: args
|
|
||||||
// TODO: prf
|
|
||||||
// A unique, random nonce
|
|
||||||
nonce []byte
|
|
||||||
// Arbitrary Metadata
|
|
||||||
meta *meta.Meta
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
|
||||||
expiration *time.Time
|
|
||||||
// The timestamp at which the Invocation was created
|
|
||||||
invokedAt *time.Time
|
|
||||||
// TODO: cause
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
|
||||||
func (t *Token) Issuer() did.DID {
|
|
||||||
return t.issuer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audience returns the did.DID representing the Token's audience.
|
|
||||||
func (t *Token) Audience() did.DID {
|
|
||||||
return t.audience
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subject returns the did.DID representing the Token's subject.
|
|
||||||
//
|
|
||||||
// This field may be did.Undef for delegations that are [Powerlined] but
|
|
||||||
// must be equal to the value returned by the Issuer method for root
|
|
||||||
// tokens.
|
|
||||||
func (t *Token) Subject() did.DID {
|
|
||||||
return t.subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command returns the capability's command.Command.
|
|
||||||
func (t *Token) Command() command.Command {
|
|
||||||
return t.command
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nonce returns the random Nonce encapsulated in this Token.
|
|
||||||
func (t *Token) Nonce() []byte {
|
|
||||||
return t.nonce
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meta returns the Token's metadata.
|
|
||||||
func (t *Token) Meta() *meta.Meta {
|
|
||||||
return t.meta
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expiration returns the time at which the Token expires.
|
|
||||||
func (t *Token) Expiration() *time.Time {
|
|
||||||
return t.expiration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) validate() error {
|
|
||||||
var errs error
|
|
||||||
|
|
||||||
requiredDID := func(id did.DID, fieldname string) {
|
|
||||||
if !id.Defined() {
|
|
||||||
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requiredDID(t.issuer, "Issuer")
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
if len(t.nonce) < 12 {
|
|
||||||
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenFromModel build a decoded view of the raw IPLD data.
|
|
||||||
// This function also serves as validation.
|
|
||||||
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|
||||||
var (
|
|
||||||
tkn Token
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
return &tkn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateNonce creates a 12-byte random nonce.
|
|
||||||
// TODO: some crypto scheme require more, is that our case?
|
|
||||||
func generateNonce() ([]byte, error) {
|
|
||||||
res := make([]byte, 12)
|
|
||||||
_, err := rand.Read(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
type DID string
|
|
||||||
|
|
||||||
# The Invocation Payload attaches sender, receiver, and provenance to the Task.
|
|
||||||
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
|
|
||||||
|
|
||||||
# A unique, random nonce
|
|
||||||
nonce Bytes
|
|
||||||
|
|
||||||
# Arbitrary Metadata
|
|
||||||
meta {String : Any}
|
|
||||||
|
|
||||||
# The timestamp at which the Invocation becomes invalid
|
|
||||||
exp nullable Int
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
package invocation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"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/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToSealed wraps the invocation token in an envelope, generates the
|
|
||||||
// signature, encodes the result to DAG-CBOR and calculates the CID of
|
|
||||||
// the resulting binary data.
|
|
||||||
func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
|
|
||||||
data, err := t.ToDagCbor(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := envelope.CIDFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
|
||||||
func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error) {
|
|
||||||
cidWriter := envelope.NewCIDWriter(w)
|
|
||||||
|
|
||||||
if err := t.ToDagCborWriter(cidWriter, privKey); err != nil {
|
|
||||||
return cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cidWriter.CID()
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromSealed decodes the provided binary data from the DAG-CBOR format,
|
|
||||||
// verifies that the envelope's signature is correct based on the public
|
|
||||||
// key taken from the issuer (iss) field and calculates the CID of the
|
|
||||||
// incoming data.
|
|
||||||
func FromSealed(data []byte) (*Token, cid.Cid, error) {
|
|
||||||
tkn, err := FromDagCbor(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := envelope.CIDFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
|
||||||
func FromSealedReader(r io.Reader) (*Token, cid.Cid, error) {
|
|
||||||
cidReader := envelope.NewCIDReader(r)
|
|
||||||
|
|
||||||
tkn, err := FromDagCborReader(cidReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := cidReader.CID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encode marshals a Token to the format specified by the provided
|
|
||||||
// codec.Encoder.
|
|
||||||
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
|
|
||||||
node, err := t.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 (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error {
|
|
||||||
node, err := t.toIPLD(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipld.EncodeStreaming(w, node, encFn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
|
||||||
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
|
|
||||||
return t.Encode(privKey, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
|
|
||||||
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
|
|
||||||
return t.EncodeWriter(w, privKey, dagcbor.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJson marshals the Token to the DAG-JSON format.
|
|
||||||
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
|
|
||||||
return t.Encode(privKey, dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
|
||||||
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
|
|
||||||
return t.EncodeWriter(w, privKey, dagjson.Encode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode unmarshals the input data using the format specified by the
|
|
||||||
// provided codec.Decoder into a Token.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
func Decode(b []byte, decFn codec.Decoder) (*Token, 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) (*Token, error) {
|
|
||||||
node, err := ipld.DecodeStreaming(r, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return FromIPLD(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCbor unmarshals the input data into a Token.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
func FromDagCbor(data []byte) (*Token, error) {
|
|
||||||
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, err := tokenFromModel(*pay)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
|
||||||
func FromDagCborReader(r io.Reader) (*Token, error) {
|
|
||||||
return DecodeReader(r, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJson unmarshals the input data into a Token.
|
|
||||||
//
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
func FromDagJson(data []byte) (*Token, error) {
|
|
||||||
return Decode(data, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
|
||||||
func FromDagJsonReader(r io.Reader) (*Token, error) {
|
|
||||||
return DecodeReader(r, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromIPLD decode the given IPLD representation into a Token.
|
|
||||||
func FromIPLD(node datamodel.Node) (*Token, error) {
|
|
||||||
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn, err := tokenFromModel(*pay)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
|
||||||
var sub *string
|
|
||||||
|
|
||||||
if t.subject != did.Undef {
|
|
||||||
s := t.subject.String()
|
|
||||||
sub = &s
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
var exp *int64
|
|
||||||
if t.expiration != nil {
|
|
||||||
u := t.expiration.Unix()
|
|
||||||
exp = &u
|
|
||||||
}
|
|
||||||
|
|
||||||
model := &tokenPayloadModel{
|
|
||||||
Iss: t.issuer.String(),
|
|
||||||
Aud: t.audience.String(),
|
|
||||||
Sub: sub,
|
|
||||||
Cmd: t.command.String(),
|
|
||||||
Nonce: t.nonce,
|
|
||||||
Meta: *t.meta,
|
|
||||||
Exp: exp,
|
|
||||||
}
|
|
||||||
|
|
||||||
return envelope.ToIPLD(privKey, model)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package invocation
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
)
|
|
||||||
|
|
||||||
// [Tag] is the string used as a key within the SigPayload that identifies
|
|
||||||
// that the TokenPayload is an invocation.
|
|
||||||
//
|
|
||||||
// [Tag]: https://github.com/ucan-wg/invocation#type-tag
|
|
||||||
const Tag = "ucan/inv@1.0.0-rc.1"
|
|
||||||
|
|
||||||
//go:embed invocation.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")
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
type tokenPayloadModel 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
|
|
||||||
|
|
||||||
// A unique, random nonce
|
|
||||||
Nonce []byte
|
|
||||||
|
|
||||||
// Arbitrary Metadata
|
|
||||||
Meta meta.Meta
|
|
||||||
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
|
||||||
// optional: can be nil
|
|
||||||
Exp *int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
|
||||||
return bindnode.Prototype((*tokenPayloadModel)(nil), payloadType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*tokenPayloadModel) Tag() string {
|
|
||||||
return Tag
|
|
||||||
}
|
|
||||||
125
token/read.go
125
token/read.go
@@ -1,125 +0,0 @@
|
|||||||
package token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
|
||||||
"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/token/delegation"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
|
||||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FromSealed decodes an arbitrary token type from the binary data,
|
|
||||||
// verifies that the envelope's signature is correct based on the public
|
|
||||||
// key taken from the issuer (iss) field and calculates the CID of the
|
|
||||||
// incoming data.
|
|
||||||
// Supported and returned types are:
|
|
||||||
// - delegation.Token
|
|
||||||
// - invocation.Token
|
|
||||||
func FromSealed(data []byte) (Token, cid.Cid, error) {
|
|
||||||
tkn, err := FromDagCbor(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := envelope.CIDFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
|
||||||
func FromSealedReader(r io.Reader) (Token, cid.Cid, error) {
|
|
||||||
cidReader := envelope.NewCIDReader(r)
|
|
||||||
|
|
||||||
tkn, err := FromDagCborReader(cidReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := cidReader.CID()
|
|
||||||
if err != nil {
|
|
||||||
return nil, cid.Undef, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tkn, id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode unmarshals the input data using the format specified by the
|
|
||||||
// provided codec.Decoder into an arbitrary UCAN token.
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
// Supported and returned types are:
|
|
||||||
// - delegation.Token
|
|
||||||
// - invocation.Token
|
|
||||||
func Decode(b []byte, decFn codec.Decoder) (Token, 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) (Token, error) {
|
|
||||||
node, err := ipld.DecodeStreaming(r, decFn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return fromIPLD(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCbor unmarshals an arbitrary DagCbor encoded UCAN token.
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
// Supported and returned types are:
|
|
||||||
// - delegation.Token
|
|
||||||
// - invocation.Token
|
|
||||||
func FromDagCbor(b []byte) (Token, error) {
|
|
||||||
return Decode(b, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
|
||||||
func FromDagCborReader(r io.Reader) (Token, error) {
|
|
||||||
return DecodeReader(r, dagcbor.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagCbor unmarshals an arbitrary DagJson encoded UCAN token.
|
|
||||||
// An error is returned if the conversion fails, or if the resulting
|
|
||||||
// Token is invalid.
|
|
||||||
// Supported and returned types are:
|
|
||||||
// - delegation.Token
|
|
||||||
// - invocation.Token
|
|
||||||
func FromDagJson(b []byte) (Token, error) {
|
|
||||||
return Decode(b, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
|
||||||
func FromDagJsonReader(r io.Reader) (Token, error) {
|
|
||||||
return DecodeReader(r, dagjson.Decode)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fromIPLD(node datamodel.Node) (Token, error) {
|
|
||||||
tag, err := envelope.FindTag(node)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch tag {
|
|
||||||
case delegation.Tag:
|
|
||||||
return delegation.FromIPLD(node)
|
|
||||||
case invocation.Tag:
|
|
||||||
return invocation.FromIPLD(node)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf(`unknown tag "%s"`, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user