23 Commits

Author SHA1 Message Date
Michael Muré
1ca17ea63d delegation: add decode function with an io.Reader 2024-09-17 16:51:26 +02:00
Steve Moyer
658794041e fix(delegation): make Expiration an Option 2024-09-17 08:08:13 -04:00
Steve Moyer
f85ece49fa feat(delegation): add validation/accessors 2024-09-16 17:18:16 -04:00
Steve Moyer
da9f2e7bec chore(ucan): clean up repository root directory 2024-09-16 14:02:01 -04:00
Steve Moyer
bd1775b2f5 Merge branch 'envelope' of github.com:ucan-wg/go-ucan into envelope 2024-09-16 13:55:23 -04:00
Steve Moyer
285cb5f6e7 feat(did): add accessor to report whether this DID is a did:key 2024-09-16 13:54:18 -04:00
Steve Moyer
4c05d866f2 fix(delegation): simplify package API and restore convenient encoding 2024-09-16 13:50:11 -04:00
Michael Muré
646127abe7 policy formatting 2024-09-16 11:07:57 +02:00
Steve Moyer
7cead1bf8d chore(envelope): refactor to decode token.Token to schema.TypedNode 2024-09-13 12:01:51 -04:00
Steve Moyer
16f0f38b43 feat(token): add generator for Go types included in schema 2024-09-11 09:39:32 -04:00
Steve Moyer
2f183aa6f4 docs(token): move schema comment to Go doc for package 2024-09-11 09:38:13 -04:00
Steve Moyer
6d1b7ee01f feat(envelope): no longer generic - handles only token.Token 2024-09-11 09:07:24 -04:00
Steve Moyer
599c5d30b0 feat(token) combined TokenPayload model for both Delegation and Invocation tokens 2024-09-11 08:50:24 -04:00
Steve Moyer
1c2f602f4d feat(did): add ToPubKey() and improve crypto tests 2024-09-11 07:12:54 -04:00
Steve Moyer
8441f99d5d fix(delegation): redelete view.go 2024-09-09 09:44:35 -04:00
Steve Moyer
1525aaa139 chore: fix module imports 2024-09-09 09:27:04 -04:00
Steve Moyer
ab4018d218 chore(v1): merge in other changes 2024-09-09 09:09:17 -04:00
Steve Moyer
39987eadaa feat(varsig): not specification compliant precalculated headers' 2024-09-09 08:56:33 -04:00
Steve Moyer
b77f8d6bb0 feat(ucan): functions to issue/delegate UCAN tokens 2024-09-09 08:55:14 -04:00
Steve Moyer
719837e3cd feat(envelope): wrap a Tokener in an Envelope 2024-09-09 08:52:57 -04:00
Steve Moyer
2205d5d4ce feat(did): add to/from public key 2024-09-09 08:50:15 -04:00
Steve Moyer
3a542ecc85 feat(delegation): use bindnode with typed prototype 2024-09-09 08:49:35 -04:00
Steve Moyer
ba1c45c088 chore(envelope): add codec/crypto dependencies 2024-09-04 08:55:32 -04:00
88 changed files with 2302 additions and 5499 deletions

View File

@@ -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

View File

@@ -16,12 +16,15 @@ var _ fmt.Stringer = (*Command)(nil)
// by one or more slash-separated Segments of lowercase characters.
//
// [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.
// An error is returned if an invalid Command would be formed
func New(segments ...string) Command {
return Top().Join(segments...)
// New creates a validated command from the provided list of segment
// strings. An error is returned if an invalid Command would be
// formed
func New(segments ...string) *Command {
return &Command{segments: segments}
}
// Parse verifies that the provided string contains the required
@@ -29,26 +32,26 @@ func New(segments ...string) Command {
// Command.
//
// [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, "/") {
return "", ErrRequiresLeadingSlash
return nil, ErrRequiresLeadingSlash
}
if len(s) > 1 && strings.HasSuffix(s, "/") {
return "", ErrDisallowsTrailingSlash
return nil, ErrDisallowsTrailingSlash
}
if s != strings.ToLower(s) {
return "", ErrRequiresLowercase
return nil, ErrRequiresLowercase
}
// 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.
return Command(s), nil
return &Command{strings.Split(s, "/")[1:]}, nil
}
// 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)
if err != nil {
panic(err)
@@ -56,14 +59,15 @@ func MustParse(s string) Command {
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
// 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
func Top() Command {
return Command(separator)
func Top() *Command {
return New()
}
// 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
// segment separator.
func (c Command) Join(segments ...string) Command {
size := 0
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)
func (c *Command) Join(segments ...string) *Command {
return &Command{append(c.segments, segments...)}
}
// Segments returns the ordered segments that comprise the Command as a
// slice of strings.
func (c Command) Segments() []string {
return strings.Split(string(c), separator)
func (c *Command) Segments() []string {
return c.segments
}
// String returns the composed representation the command. This is also
// the required wire representation (before IPLD encoding occurs.)
func (c Command) String() string {
return string(c)
func (c *Command) String() string {
return "/" + strings.Join(c.segments, "/")
}

View File

@@ -5,7 +5,7 @@ import (
"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) {
@@ -13,66 +13,73 @@ func TestTop(t *testing.T) {
}
func TestIsValidCommand(t *testing.T) {
t.Parallel()
t.Run("succeeds when", func(t *testing.T) {
t.Parallel()
for _, testcase := range validTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
require.True(t, command.IsValid(testcase.inp))
})
}
})
t.Run("fails when", func(t *testing.T) {
t.Parallel()
for _, testcase := range invalidTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
t.Run("succeeds when", func(t *testing.T) {
t.Parallel()
for _, testcase := range validTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
cmd, err := command.Parse("/elem0/elem1/elem2")
require.NoError(t, err)
require.NotEmpty(t, cmd)
require.NotNil(t, cmd)
})
}
})
t.Run("fails when", func(t *testing.T) {
t.Parallel()
for _, testcase := range invalidTestcases(t) {
testcase := testcase
t.Run(testcase.name, func(t *testing.T) {
t.Parallel()
cmd, err := command.Parse(testcase.inp)
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 {
name string
inp string

View File

@@ -9,7 +9,7 @@ import (
"github.com/ipld/go-ipld-prime/must"
"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) {
@@ -61,7 +61,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
if err != nil {
return nil, err
}
return negation{statement: statement}, nil
return Not(statement), nil
case KindAnd, KindOr:
arg2, _ := node.LookupByIndex(1)
@@ -93,11 +93,11 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
if pattern.Kind() != datamodel.Kind_String {
return nil, ErrNotAString(combinePath(path, op, 2))
}
g, err := parseGlob(must.String(pattern))
res, err := Like(sel, must.String(pattern))
if err != nil {
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
}
return wildcard{selector: sel, pattern: g}, nil
return res, nil
case KindAll, KindAny:
sel, err := arg2AsSelector(op)
@@ -232,7 +232,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
if err != nil {
return nil, err
}
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
err = listBuilder.AssembleValue().AssignString(statement.pattern)
if err != nil {
return nil, err
}

View File

@@ -16,9 +16,7 @@ func TestIpldRoundTrip(t *testing.T) {
["any", ".tags",
["or", [
["==", ".", "news"],
["==", ".", "press"]
]]
]
["==", ".", "press"]]]
]`
for _, tc := range []struct {

View File

@@ -1,13 +1,20 @@
// Package literal holds a collection of functions to create IPLD types to use in policies, selector and args.
package literal
import (
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"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 {
nb := basicnode.Prototype.Bool.NewBuilder()
nb.AssignBool(val)
@@ -38,16 +45,6 @@ func Bytes(val []byte) ipld.Node {
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 {
nb := basicnode.Prototype.Any.NewBuilder()
nb.AssignNull()

164
capability/policy/match.go Normal file
View 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 }

View File

@@ -2,7 +2,6 @@ package policy
import (
"fmt"
"strings"
"testing"
"github.com/ipfs/go-cid"
@@ -12,7 +11,8 @@ import (
"github.com/ipld/go-ipld-prime/node/basicnode"
"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) {
@@ -23,16 +23,16 @@ func TestMatch(t *testing.T) {
nb.AssignString("test")
nd := nb.Build()
pol := MustConstruct(Equal(".", literal.String("test")))
ok := pol.Match(nd)
pol := Policy{Equal(selector.MustParse("."), literal.String("test"))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".", literal.String("test2")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(Equal(".", literal.Int(138)))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.Int(138))}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -42,16 +42,16 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(Equal(".", literal.Int(138)))
ok := pol.Match(nd)
pol := Policy{Equal(selector.MustParse("."), literal.Int(138))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".", literal.Int(1138)))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(Equal(".", literal.String("138")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -61,16 +61,16 @@ func TestMatch(t *testing.T) {
nb.AssignFloat(1.138)
nd := nb.Build()
pol := MustConstruct(Equal(".", literal.Float(1.138)))
ok := pol.Match(nd)
pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".", literal.Float(11.38)))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(Equal(".", literal.String("138")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -83,16 +83,16 @@ func TestMatch(t *testing.T) {
nb.AssignLink(l0)
nd := nb.Build()
pol := MustConstruct(Equal(".", literal.Link(l0)))
ok := pol.Match(nd)
pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".", literal.Link(l1)))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -105,20 +105,20 @@ func TestMatch(t *testing.T) {
ma.Finish()
nd := nb.Build()
pol := MustConstruct(Equal(".foo", literal.String("bar")))
ok := pol.Match(nd)
pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))}
ok = Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".foo", literal.String("baz")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -130,12 +130,12 @@ func TestMatch(t *testing.T) {
la.Finish()
nd := nb.Build()
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
ok := pol.Match(nd)
pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
ok = pol.Match(nd)
pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))}
ok = Match(pol, nd)
require.False(t, ok)
})
})
@@ -147,8 +147,8 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
ok := pol.Match(nd)
pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))}
ok := Match(pol, nd)
require.True(t, ok)
})
@@ -158,12 +158,12 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
ok := pol.Match(nd)
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
ok = pol.Match(nd)
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))}
ok = Match(pol, nd)
require.True(t, ok)
})
@@ -173,8 +173,8 @@ func TestMatch(t *testing.T) {
nb.AssignFloat(1.38)
nd := nb.Build()
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
ok := pol.Match(nd)
pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))}
ok := Match(pol, nd)
require.True(t, ok)
})
@@ -184,12 +184,12 @@ func TestMatch(t *testing.T) {
nb.AssignFloat(1.38)
nd := nb.Build()
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
ok := pol.Match(nd)
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
ok = pol.Match(nd)
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))}
ok = Match(pol, nd)
require.True(t, ok)
})
@@ -199,8 +199,8 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(LessThan(".", literal.Int(1138)))
ok := pol.Match(nd)
pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))}
ok := Match(pol, nd)
require.True(t, ok)
})
@@ -210,12 +210,12 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
ok := pol.Match(nd)
pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
ok = pol.Match(nd)
pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))}
ok = Match(pol, nd)
require.True(t, ok)
})
})
@@ -226,12 +226,12 @@ func TestMatch(t *testing.T) {
nb.AssignBool(false)
nd := nb.Build()
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
ok := pol.Match(nd)
pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
ok = pol.Match(nd)
pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -241,26 +241,26 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(
pol := Policy{
And(
GreaterThan(".", literal.Int(1)),
LessThan(".", literal.Int(1138)),
GreaterThan(selector.MustParse("."), literal.Int(1)),
LessThan(selector.MustParse("."), literal.Int(1138)),
),
)
ok := pol.Match(nd)
}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(
pol = Policy{
And(
GreaterThan(".", literal.Int(1)),
Equal(".", literal.Int(1138)),
GreaterThan(selector.MustParse("."), literal.Int(1)),
Equal(selector.MustParse("."), literal.Int(1138)),
),
)
ok = pol.Match(nd)
}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(And())
ok = pol.Match(nd)
pol = Policy{And()}
ok = Match(pol, nd)
require.True(t, ok)
})
@@ -270,26 +270,26 @@ func TestMatch(t *testing.T) {
nb.AssignInt(138)
nd := nb.Build()
pol := MustConstruct(
pol := Policy{
Or(
GreaterThan(".", literal.Int(138)),
LessThan(".", literal.Int(1138)),
GreaterThan(selector.MustParse("."), literal.Int(138)),
LessThan(selector.MustParse("."), literal.Int(1138)),
),
)
ok := pol.Match(nd)
}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(
pol = Policy{
Or(
GreaterThan(".", literal.Int(138)),
Equal(".", literal.Int(1138)),
GreaterThan(selector.MustParse("."), literal.Int(138)),
Equal(selector.MustParse("."), literal.Int(1138)),
),
)
ok = pol.Match(nd)
}
ok = Match(pol, nd)
require.False(t, ok)
pol = MustConstruct(Or())
ok = pol.Match(nd)
pol = Policy{Or()}
ok = Match(pol, nd)
require.True(t, ok)
})
@@ -309,8 +309,11 @@ func TestMatch(t *testing.T) {
nb.AssignString(s)
nd := nb.Build()
pol := MustConstruct(Like(".", pattern))
ok := pol.Match(nd)
statement, err := Like(selector.MustParse("."), pattern)
require.NoError(t, err)
pol := Policy{statement}
ok := Match(pol, nd)
require.True(t, ok)
})
}(s)
@@ -330,8 +333,11 @@ func TestMatch(t *testing.T) {
nb.AssignString(s)
nd := nb.Build()
pol := MustConstruct(Like(".", pattern))
ok := pol.Match(nd)
statement, err := Like(selector.MustParse("."), pattern)
require.NoError(t, err)
pol := Policy{statement}
ok := Match(pol, nd)
require.False(t, ok)
})
}(s)
@@ -361,12 +367,22 @@ func TestMatch(t *testing.T) {
la.Finish()
nd := nb.Build()
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
ok := pol.Match(nd)
pol := Policy{
All(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(2)),
),
}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
ok = pol.Match(nd)
pol = Policy{
All(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(20)),
),
}
ok = Match(pol, nd)
require.False(t, ok)
})
@@ -382,12 +398,22 @@ func TestMatch(t *testing.T) {
la.Finish()
nd := nb.Build()
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
ok := pol.Match(nd)
pol := Policy{
Any(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(60)),
),
}
ok := Match(pol, nd)
require.True(t, ok)
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
ok = pol.Match(nd)
pol = Policy{
Any(
selector.MustParse(".[]"),
GreaterThan(selector.MustParse(".value"), literal.Int(100)),
),
}
ok = Match(pol, nd)
require.False(t, ok)
})
})
@@ -406,7 +432,7 @@ func TestPolicyExamples(t *testing.T) {
pol, err := FromDagJson(policy)
require.NoError(t, err)
return pol.Match(data)
return Match(pol, data)
}
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))
})
}
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
View 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}
}

View File

@@ -2,18 +2,10 @@ package selector
import (
"fmt"
"regexp"
"strconv"
"strings"
)
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) {
if len(str) == 0 {
return nil, newParseError("empty selector", str, 0, "")
@@ -21,9 +13,6 @@ func Parse(str string) (Selector, error) {
if string(str[0]) != "." {
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
}
if str == "." {
return identity, nil
}
col := 0
var sel Selector
@@ -38,9 +27,9 @@ func Parse(str string) (Selector, error) {
if len(sel) > 0 && sel[len(sel)-1].Identity() {
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
}
sel = append(sel, segment{str: ".", identity: true})
sel = append(sel, Identity)
case "[]":
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
default:
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
lookup := seg[1 : len(seg)-1]

View 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}
}

View File

@@ -313,7 +313,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
@@ -328,7 +328,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".name.first")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
@@ -338,7 +338,7 @@ func TestSelect(t *testing.T) {
name := must.String(one)
require.Equal(t, alice.Name.First, name)
one, many, err = sel.Select(bnode)
one, many, err = Select(sel, bnode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
@@ -353,7 +353,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".name.middle?")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.NotEmpty(t, one)
require.Empty(t, many)
@@ -363,7 +363,7 @@ func TestSelect(t *testing.T) {
name := must.String(one)
require.Equal(t, *alice.Name.Middle, name)
one, many, err = sel.Select(bnode)
one, many, err = Select(sel, bnode)
require.NoError(t, err)
require.Empty(t, one)
require.Empty(t, many)
@@ -373,7 +373,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".name.foo")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.Error(t, err)
require.Empty(t, one)
require.Empty(t, many)
@@ -387,7 +387,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".name.foo?")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.Empty(t, one)
require.Empty(t, many)
@@ -397,7 +397,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".interests[]")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.Empty(t, one)
require.NotEmpty(t, many)
@@ -417,7 +417,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".interests[0][]")
require.NoError(t, err)
one, many, err := sel.Select(anode)
one, many, err := Select(sel, anode)
require.NoError(t, err)
require.Empty(t, one)
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) {
selectorCorpus := []string{
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
@@ -520,6 +494,6 @@ func FuzzParseAndSelect(f *testing.F) {
}
// look for panic()
_, _, _ = sel.Select(node)
_, _, _ = Select(sel, node)
})
}

View File

@@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/assert"
"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
@@ -30,14 +30,14 @@ func TestSupportedForms(t *testing.T) {
for _, testcase := range []Testcase{
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
{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: "Object 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: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
{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: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
{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)
// 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.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)
// 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)
// TODO: should Select return a single node which is sometimes a list or null?
// require.Equal(t, datamodel.Null, node)
@@ -124,7 +124,7 @@ func TestSupportedForms(t *testing.T) {
require.NoError(t, err)
// 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)
assert.Nil(t, node)
assert.Empty(t, nodes)

View 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
View 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
}

View File

@@ -1,17 +1,19 @@
package delegation_test
import (
"crypto/rand"
"testing"
"time"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"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/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation"
"gotest.tools/v3/golden"
)
const (
@@ -63,9 +65,6 @@ const (
]
]
`
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
)
func TestConstructors(t *testing.T) {
@@ -84,20 +83,18 @@ func TestConstructors(t *testing.T) {
pol, err := policy.FromDagJson(subjectPol)
require.NoError(t, err)
exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z")
require.NoError(t, err)
exp := time.Time{}
meta := map[string]datamodel.Node{
"foo": basicnode.NewString("fooo"),
"bar": basicnode.NewString("barr"),
}
t.Run("New", func(t *testing.T) {
tkn, err := delegation.New(privKey, aud, cmd, pol,
delegation.WithNonce([]byte(nonce)),
delegation.WithSubject(sub),
delegation.WithExpiration(exp),
delegation.WithMeta("foo", "fooo"),
delegation.WithMeta("bar", "barr"),
)
dlg, err := delegation.New(privKey, aud, &sub, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
require.NoError(t, err)
data, err := tkn.ToDagJson(privKey)
data, err := dlg.ToDagJson()
require.NoError(t, err)
t.Log(string(data))
@@ -108,15 +105,10 @@ func TestConstructors(t *testing.T) {
t.Run("Root", func(t *testing.T) {
t.Parallel()
tkn, err := delegation.Root(privKey, aud, cmd, pol,
delegation.WithNonce([]byte(nonce)),
delegation.WithExpiration(exp),
delegation.WithMeta("foo", "fooo"),
delegation.WithMeta("bar", "barr"),
)
dlg, err := delegation.Root(privKey, aud, cmd, pol, []byte(nonce), delegation.WithExpiration(&exp), delegation.WithMeta(meta))
require.NoError(t, err)
data, err := tkn.ToDagJson(privKey)
data, err := dlg.ToDagJson()
require.NoError(t, err)
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)
require.NoError(t, err)
@@ -134,3 +128,22 @@ func privKey(t require.TestingT, privKeyCfg string) crypto.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
View 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
View 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
View 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
View 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"}}]

View File

@@ -116,11 +116,3 @@ func Parse(str string) (DID, error) {
buf = append(buf, suffix...)
return DID{str: string(buf), code: DIDCore}, nil
}
func MustParse(str string) DID {
did, err := Parse(str)
if err != nil {
panic(err)
}
return did
}

View File

@@ -2,8 +2,6 @@ package did
import (
"testing"
"github.com/stretchr/testify/require"
)
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) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
d0, err := Parse(str)

5
doc.go Normal file
View 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
View File

@@ -1,15 +1,19 @@
module github.com/ucan-wg/go-ucan
go 1.23
go 1.22.0
toolchain go1.22.4
require (
github.com/gobwas/glob v0.2.3
github.com/ipfs/go-cid v0.4.1
github.com/ipld/go-ipld-prime v0.21.0
github.com/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-multicodec v0.9.0
github.com/multiformats/go-multihash v0.2.3
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
gotest.tools/v3 v3.5.1
)
@@ -17,7 +21,8 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // 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/minio/sha256-simd v1.0.1 // 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/spaolacci/murmur3 v1.1.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
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

27
go.sum
View File

@@ -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/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/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/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-cmp v0.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/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
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/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-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
github.com/libp2p/go-libp2p v0.36.2 h1:BbqRkDaGC3/5xfaJakLV/BrpjlAuYqSB0lRvtzL3B/U=
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/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
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/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/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/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
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/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/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 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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View 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)
}

View 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()))
})
}

View 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
}

View File

@@ -0,0 +1 @@
X@=•zfˆîŒ— ©Ê¦Z.ÈÚàP óú¤Jr=n¬;¡)Dñ÷™¹6îB;ò

View 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
}
}
]

View 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
View 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
View 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
View 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())
}

View 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
View 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())
}

View 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
}

View 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
}

View 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
View File

@@ -0,0 +1,7 @@
//go:build tools
package tools
import (
_ "github.com/selesy/go-options"
)

View File

@@ -24,6 +24,7 @@
package varsig
import (
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
@@ -57,7 +58,7 @@ var (
//
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
func Decode(header []byte) (pb.KeyType, error) {
keyType, ok := decMap[string(header)]
keyType, ok := decMap[base64.RawStdEncoding.EncodeToString(header)]
if !ok {
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
}
@@ -81,10 +82,10 @@ func Encode(keyType pb.KeyType) ([]byte, error) {
return []byte(header), nil
}
func keyTypeToHeader() map[pb.KeyType]string {
func keyTypeToHeader() map[pb.KeyType][]byte {
const rsaSigLen = 0x100
return map[pb.KeyType]string{
return map[pb.KeyType][]byte{
pb.KeyType_RSA: header(
Prefix,
multicodec.RsaPub,
@@ -116,18 +117,18 @@ func headerToKeyType() map[string]pb.KeyType {
out := make(map[string]pb.KeyType, len(encMap))
for keyType, header := range encMap {
out[header] = keyType
out[base64.RawStdEncoding.EncodeToString(header)] = keyType
}
return out
}
func header(vals ...multicodec.Code) string {
func header(vals ...multicodec.Code) []byte {
var buf []byte
for _, val := range vals {
buf = binary.AppendUvarint(buf, uint64(val))
}
return string(buf)
return buf
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/libp2p/go-libp2p/core/crypto/pb"
"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) {

View File

@@ -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 |
![Overhead %](img/overhead_percent.png)
| 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 |
![img.png](img/overhead_bytes.png)
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)
![img.png](img/cpu.png)
![img_1.png](img/alloc_byte.png)
![img_2.png](img/alloc_count.png)
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.**

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

Binary file not shown.

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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")
})
}

View File

@@ -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)
}

View File

@@ -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!")
}
}

View File

@@ -1,293 +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, _, err := s.selector.Select(node)
if err != nil {
return false
}
if one != nil {
return datamodel.DeepEqual(s.value, one)
}
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 {
one, many, err := s.selector.Select(node)
if err != nil {
return false
}
if one != nil {
it := one.ListIterator()
if it != nil {
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return false
}
ok := matchStatement(s.statement, v)
if !ok {
return false
}
}
} else {
ok := matchStatement(s.statement, one)
if !ok {
return false
}
}
}
if len(many) > 0 {
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 {
it := one.ListIterator()
if it != nil {
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return false
}
ok := matchStatement(s.statement, v)
if ok {
return true
}
}
} else {
ok := matchStatement(s.statement, one)
if ok {
return true
}
}
}
if len(many) > 0 {
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 }

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)
}
})
})
}

View File

@@ -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"}}]

View File

@@ -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"}}]

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}
})
}

View File

@@ -1 +0,0 @@
X@|úŸÀ½â–ðõ+ÁŠ­!µ.®ÿhéÍúGO- ü¬”jÉssY¨quëiþ“ä°¬Íuý#ò¼’ç˜ c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld

View File

@@ -1 +0,0 @@
[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}]

View File

@@ -1,6 +0,0 @@
type DID string
type Example struct {
hello String
issuer DID (rename "iss")
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}