Compare commits
73 Commits
invocation
...
feat/deleg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79955057a3 | ||
|
|
59cebf8e74 | ||
|
|
952a6bb922 | ||
|
|
2089aa2a6a | ||
|
|
4a655506f9 | ||
|
|
93dd3ef719 | ||
|
|
6075c19957 | ||
|
|
6161f2e440 | ||
|
|
5202056cc7 | ||
|
|
f779477118 | ||
|
|
4974fed931 | ||
|
|
0d9955b7b0 | ||
|
|
6dd6f8a229 | ||
|
|
5509cce513 | ||
|
|
f4ad97679c | ||
|
|
41c8bc7218 | ||
|
|
371bf3b9f5 | ||
|
|
b14671009c | ||
|
|
043c9b160d | ||
|
|
130168809b | ||
|
|
20886f1b5f | ||
|
|
684c21c7a4 | ||
|
|
4749243e3c | ||
|
|
c7f6034376 | ||
|
|
55070dcb43 | ||
|
|
fe594e9906 | ||
|
|
0781b84937 | ||
|
|
f44b6ec2c3 | ||
|
|
abe8a8150a | ||
|
|
f44b5cb921 | ||
|
|
baf3edcf88 | ||
|
|
7107d6bc85 | ||
|
|
c66dd5b2a4 | ||
|
|
70dc12d68e | ||
|
|
dd1f54694f | ||
|
|
ac73cae3ec | ||
|
|
a19d3505fe | ||
|
|
526a34b45d | ||
|
|
989f409fd0 | ||
|
|
40488dfc3d | ||
|
|
84122e57bc | ||
|
|
4e15349c5e | ||
|
|
53cb82a2b4 | ||
|
|
64936fd061 | ||
|
|
30be95b20c | ||
|
|
16ba4b392d | ||
|
|
94a0d4d56e | ||
|
|
53ef97231d | ||
|
|
c960481a10 | ||
|
|
d4d4514971 | ||
|
|
282db65900 | ||
|
|
2459f1a5c3 | ||
|
|
37f5286315 | ||
|
|
06e0674c46 | ||
|
|
ad03154b6e | ||
|
|
700f130858 | ||
|
|
d57d2a230b | ||
|
|
7060d4bb33 | ||
|
|
a183b627be | ||
|
|
dbfff3f70c | ||
|
|
cb45d9019b | ||
|
|
2e17ff8550 | ||
|
|
e86e45be73 | ||
|
|
97c9990045 | ||
|
|
6a17270545 | ||
|
|
941e6366a3 | ||
|
|
8bf7fa173c | ||
|
|
f731f56dd2 | ||
|
|
d042b5cdfd | ||
|
|
d51bbc303c | ||
|
|
dfb09cadb9 | ||
|
|
707ec46338 | ||
|
|
4dd91f95f7 |
@@ -1,142 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
separator = "/"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNew indicates that the wrapped error was encountered while creating
|
|
||||||
// a new Command.
|
|
||||||
var ErrNew = errors.New("failed to create Command from elems")
|
|
||||||
|
|
||||||
// ErrParse indicates that the wrapped error was encountered while
|
|
||||||
// attempting to parse a string as a Command.
|
|
||||||
var ErrParse = errors.New("failed to parse Command")
|
|
||||||
|
|
||||||
// ErrorJoin indicates that the wrapped error was encountered while
|
|
||||||
// attempting to join a new segment to a Command.
|
|
||||||
var ErrJoin = errors.New("failed to join segments to Command")
|
|
||||||
|
|
||||||
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
|
||||||
// doesn't start with a [leading slash character].
|
|
||||||
//
|
|
||||||
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrRequiresLeadingSlash = parseError("a command requires a leading slash character")
|
|
||||||
|
|
||||||
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
|
||||||
// with a trailing slash character].
|
|
||||||
//
|
|
||||||
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrDisallowsTrailingSlash = parseError("a command must not include a trailing slash")
|
|
||||||
|
|
||||||
// ErrUCANNamespaceReserved is returned to indicate that a Command's
|
|
||||||
// first segment would contain the [reserved "ucan" namespace].
|
|
||||||
//
|
|
||||||
// [reserved "ucan" namespace]: https://github.com/ucan-wg/spec#ucan-namespace
|
|
||||||
var ErrUCANNamespaceReserved = errors.New("the UCAN namespace is reserved")
|
|
||||||
|
|
||||||
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
|
||||||
// [uppercase unicode characters].
|
|
||||||
//
|
|
||||||
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrRequiresLowercase = parseError("UCAN path segments must must not contain upper-case characters")
|
|
||||||
|
|
||||||
func parseError(msg string) error {
|
|
||||||
return fmt.Errorf("%w: %s", ErrParse, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ fmt.Stringer = (*Command)(nil)
|
|
||||||
|
|
||||||
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
|
||||||
// interpretable by the Subject of a UCAN.
|
|
||||||
//
|
|
||||||
// A [Command] is composed of a leading slash which is optionally followed
|
|
||||||
// by one or more slash-separated Segments of lowercase characters.
|
|
||||||
//
|
|
||||||
// [Command]: https://github.com/ucan-wg/spec#command
|
|
||||||
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, error) {
|
|
||||||
return newCommand(ErrNew, segments...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCommand(err error, segments ...string) (*Command, error) {
|
|
||||||
if len(segments) > 0 && segments[0] == "ucan" {
|
|
||||||
return nil, fmt.Errorf("%w: %w", err, ErrUCANNamespaceReserved)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := Command{segments}
|
|
||||||
|
|
||||||
return &cmd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse verifies that the provided string contains the required
|
|
||||||
// [segment structure] and, if valid, returns the resulting
|
|
||||||
// Command.
|
|
||||||
//
|
|
||||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
func Parse(s string) (*Command, error) {
|
|
||||||
if !strings.HasPrefix(s, "/") {
|
|
||||||
return nil, ErrRequiresLeadingSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
|
||||||
return nil, ErrDisallowsTrailingSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
if s != strings.ToLower(s) {
|
|
||||||
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 newCommand(ErrParse, strings.Split(s, "/")[1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Top] is the most powerful capability.
|
|
||||||
//
|
|
||||||
// This function returns a Command that is a wildcard and therefore represents the
|
|
||||||
// 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 {
|
|
||||||
cmd, _ := New()
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid returns true if the provided string is a valid UCAN command.
|
|
||||||
func IsValid(s string) bool {
|
|
||||||
_, err := Parse(s)
|
|
||||||
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join appends segments to the end of this command using the required
|
|
||||||
// segment separator.
|
|
||||||
func (c *Command) Join(segments ...string) (*Command, error) {
|
|
||||||
return newCommand(ErrJoin, append(c.segments, segments...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Segments returns the ordered segments that comprise the Command as a
|
|
||||||
// slice of strings.
|
|
||||||
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 "/" + strings.Join([]string(c.segments), "/")
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
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}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *PayloadModel) EncodeDagCbor() ([]byte, error) {
|
|
||||||
return ipld.Marshal(dagcbor.Encode, p, PayloadType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PayloadModel) EncodeDagJson() ([]byte, error) {
|
|
||||||
return ipld.Marshal(dagjson.Encode, p, PayloadType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeDagCbor(data []byte) (*PayloadModel, error) {
|
|
||||||
var p PayloadModel
|
|
||||||
_, err := ipld.Unmarshal(data, dagcbor.Decode, &p, PayloadType())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeDagJson(data []byte) (*PayloadModel, error) {
|
|
||||||
var p PayloadModel
|
|
||||||
_, err := ipld.Unmarshal(data, dagjson.Decode, &p, PayloadType())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &p, nil
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
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 --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
|
||||||
// function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson()
|
|
||||||
|
|
||||||
p1, err := DecodeDagJson([]byte(delegationJson))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cborBytes, err := p1.EncodeDagCbor()
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("cborBytes length", len(cborBytes))
|
|
||||||
fmt.Println("cbor", string(cborBytes))
|
|
||||||
|
|
||||||
p2, err := DecodeDagCbor(cborBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("read Cbor", p2)
|
|
||||||
|
|
||||||
readJson, err := p2.EncodeDagJson()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/did"
|
|
||||||
)
|
|
||||||
|
|
||||||
type View struct {
|
|
||||||
// Issuer DID (sender)
|
|
||||||
Issuer did.DID
|
|
||||||
// Audience DID (receiver)
|
|
||||||
Audience did.DID
|
|
||||||
// Principal that the chain is about (the Subject)
|
|
||||||
Subject did.DID
|
|
||||||
// The Command to eventually invoke
|
|
||||||
Command *command.Command
|
|
||||||
// The delegation policy
|
|
||||||
Policy policy.Policy
|
|
||||||
// A unique, random nonce
|
|
||||||
Nonce []byte
|
|
||||||
// Arbitrary Metadata
|
|
||||||
Meta map[string]datamodel.Node
|
|
||||||
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
|
||||||
NotBefore time.Time
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
|
||||||
Expiration time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewFromModel build a decoded view of the raw IPLD data.
|
|
||||||
// This function also serves as validation.
|
|
||||||
func ViewFromModel(m PayloadModel) (*View, error) {
|
|
||||||
var view View
|
|
||||||
var err error
|
|
||||||
|
|
||||||
view.Issuer, err = did.Parse(m.Iss)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse iss: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.Audience, err = did.Parse(m.Aud)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse audience: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Sub != nil {
|
|
||||||
view.Subject, err = did.Parse(*m.Sub)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse subject: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
view.Subject = did.Undef
|
|
||||||
}
|
|
||||||
|
|
||||||
view.Command, err = command.Parse(m.Cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.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")
|
|
||||||
}
|
|
||||||
view.Nonce = m.Nonce
|
|
||||||
|
|
||||||
// TODO: copy?
|
|
||||||
view.Meta = m.Meta.Values
|
|
||||||
|
|
||||||
if m.Nbf != nil {
|
|
||||||
view.NotBefore = time.Unix(*m.Nbf, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Exp != nil {
|
|
||||||
view.Expiration = time.Unix(*m.Exp, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &view, nil
|
|
||||||
}
|
|
||||||
48
did/crypto.go
Normal file
48
did/crypto.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
|
"github.com/multiformats/go-multicodec"
|
||||||
|
"github.com/multiformats/go-varint"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
|
||||||
|
return FromPubKey(privKey.GetPublic())
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
|
||||||
|
code, ok := map[pb.KeyType]multicodec.Code{
|
||||||
|
pb.KeyType_Ed25519: multicodec.Ed25519Pub,
|
||||||
|
pb.KeyType_RSA: multicodec.RsaPub,
|
||||||
|
pb.KeyType_Secp256k1: multicodec.Secp256k1Pub,
|
||||||
|
pb.KeyType_ECDSA: multicodec.Es256,
|
||||||
|
}[pubKey.Type()]
|
||||||
|
if !ok {
|
||||||
|
return Undef, errors.New("Blah")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := varint.ToUvarint(uint64(code))
|
||||||
|
|
||||||
|
pubBytes, err := pubKey.Raw()
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DID{
|
||||||
|
str: string(append(buf, pubBytes...)),
|
||||||
|
code: uint64(code),
|
||||||
|
key: true,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPubKey(s string) (crypto.PubKey, error) {
|
||||||
|
id, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id.PubKey()
|
||||||
|
}
|
||||||
51
did/crypto_test.go
Normal file
51
did/crypto_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package did_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
exampleDIDStr = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
|
||||||
|
examplePubKeyStr = "Lm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY="
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFromPubKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(examplePubKey(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, exampleDID(t), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToPubKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
pubKey, err := did.ToPubKey(exampleDIDStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, examplePubKey(t), pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleDID(t *testing.T) did.DID {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
id, err := did.Parse(exampleDIDStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePubKey(t *testing.T) crypto.PubKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pubKeyCfg, err := crypto.ConfigDecodeKey(examplePubKeyStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pubKey, err := crypto.UnmarshalEd25519PublicKey(pubKeyCfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return pubKey
|
||||||
|
}
|
||||||
50
did/did.go
50
did/did.go
@@ -4,7 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||||
mbase "github.com/multiformats/go-multibase"
|
mbase "github.com/multiformats/go-multibase"
|
||||||
|
"github.com/multiformats/go-multicodec"
|
||||||
varint "github.com/multiformats/go-varint"
|
varint "github.com/multiformats/go-varint"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,12 +15,16 @@ const KeyPrefix = "did:key:"
|
|||||||
|
|
||||||
const DIDCore = 0x0d1d
|
const DIDCore = 0x0d1d
|
||||||
const Ed25519 = 0xed
|
const Ed25519 = 0xed
|
||||||
|
const RSA = uint64(multicodec.RsaPub)
|
||||||
|
|
||||||
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
|
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
|
||||||
|
|
||||||
|
//
|
||||||
|
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
|
||||||
type DID struct {
|
type DID struct {
|
||||||
key bool
|
key bool
|
||||||
str string
|
code uint64
|
||||||
|
str string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undef can be used to represent a nil or undefined DID, using DID{}
|
// Undef can be used to represent a nil or undefined DID, using DID{}
|
||||||
@@ -36,10 +42,36 @@ func (d DID) Bytes() []byte {
|
|||||||
return []byte(d.str)
|
return []byte(d.str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d DID) Code() uint64 {
|
||||||
|
return d.code
|
||||||
|
}
|
||||||
|
|
||||||
func (d DID) DID() DID {
|
func (d DID) DID() DID {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d DID) Key() bool {
|
||||||
|
return d.key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DID) PubKey() (crypto.PubKey, error) {
|
||||||
|
if !d.key {
|
||||||
|
return nil, fmt.Errorf("unsupported did type: %s", d.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
|
||||||
|
multicodec.Ed25519Pub: crypto.UnmarshalEd25519PublicKey,
|
||||||
|
multicodec.RsaPub: crypto.UnmarshalRsaPublicKey,
|
||||||
|
multicodec.Secp256k1Pub: crypto.UnmarshalSecp256k1PublicKey,
|
||||||
|
multicodec.Es256: crypto.UnmarshalECDSAPublicKey,
|
||||||
|
}[multicodec.Code(d.code)]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
return unmarshaler(d.Bytes()[varint.UvarintSize(d.code):])
|
||||||
|
}
|
||||||
|
|
||||||
// String formats the decentralized identity document (DID) as a string.
|
// String formats the decentralized identity document (DID) as a string.
|
||||||
func (d DID) String() string {
|
func (d DID) String() string {
|
||||||
if d.key {
|
if d.key {
|
||||||
@@ -54,8 +86,8 @@ func Decode(bytes []byte) (DID, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Undef, err
|
return Undef, err
|
||||||
}
|
}
|
||||||
if code == Ed25519 {
|
if code == Ed25519 || code == RSA {
|
||||||
return DID{str: string(bytes), key: true}, nil
|
return DID{str: string(bytes), code: code, key: true}, nil
|
||||||
} else if code == DIDCore {
|
} else if code == DIDCore {
|
||||||
return DID{str: string(bytes)}, nil
|
return DID{str: string(bytes)}, nil
|
||||||
}
|
}
|
||||||
@@ -82,5 +114,13 @@ func Parse(str string) (DID, error) {
|
|||||||
varint.PutUvarint(buf, DIDCore)
|
varint.PutUvarint(buf, DIDCore)
|
||||||
suffix, _ := strings.CutPrefix(str, Prefix)
|
suffix, _ := strings.CutPrefix(str, Prefix)
|
||||||
buf = append(buf, suffix...)
|
buf = append(buf, suffix...)
|
||||||
return DID{str: string(buf)}, nil
|
return DID{str: string(buf), code: DIDCore}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustParse(str string) DID {
|
||||||
|
did, err := Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return did
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package did
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseDIDKey(t *testing.T) {
|
func TestParseDIDKey(t *testing.T) {
|
||||||
@@ -15,6 +17,18 @@ func TestParseDIDKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMustParseDIDKey(t *testing.T) {
|
||||||
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
d := MustParse(str)
|
||||||
|
require.Equal(t, str, d.String())
|
||||||
|
})
|
||||||
|
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
|
require.Panics(t, func() {
|
||||||
|
MustParse(str)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDecodeDIDKey(t *testing.T) {
|
func TestDecodeDIDKey(t *testing.T) {
|
||||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
d0, err := Parse(str)
|
d0, err := Parse(str)
|
||||||
|
|||||||
33
go.mod
33
go.mod
@@ -1,31 +1,36 @@
|
|||||||
module github.com/ucan-wg/go-ucan/v1
|
module github.com/ucan-wg/go-ucan
|
||||||
|
|
||||||
go 1.21
|
go 1.22
|
||||||
|
|
||||||
toolchain go1.22.1
|
toolchain go1.22.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gobwas/glob v0.2.3
|
|
||||||
github.com/ipfs/go-cid v0.4.1
|
github.com/ipfs/go-cid v0.4.1
|
||||||
github.com/ipld/go-ipld-prime v0.21.0
|
github.com/ipld/go-ipld-prime v0.21.0
|
||||||
github.com/multiformats/go-multibase v0.0.3
|
github.com/libp2p/go-libp2p v0.36.3
|
||||||
github.com/multiformats/go-varint v0.0.6
|
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/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
gotest.tools/v3 v3.5.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
github.com/google/go-cmp v0.5.9 // 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
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/multiformats/go-base32 v0.0.3 // indirect
|
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||||
github.com/multiformats/go-base36 v0.1.0 // indirect
|
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polydawn/refmt v0.89.0 // indirect
|
github.com/polydawn/refmt v0.89.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
golang.org/x/crypto v0.1.0 // indirect
|
golang.org/x/crypto v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.1.6 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
57
go.sum
57
go.sum
@@ -2,11 +2,13 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||||
|
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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
@@ -17,30 +19,34 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH
|
|||||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||||
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
|
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/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=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
|
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||||
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
|
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||||
github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4=
|
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||||
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
|
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||||
github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk=
|
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
|
||||||
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
|
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
|
||||||
|
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||||
|
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||||
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||||
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
||||||
@@ -61,14 +67,19 @@ github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60Nt
|
|||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
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/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -76,5 +87,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
|||||||
93
pkg/command/command.go
Normal file
93
pkg/command/command.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const separator = "/"
|
||||||
|
|
||||||
|
var _ fmt.Stringer = (*Command)(nil)
|
||||||
|
|
||||||
|
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
||||||
|
// interpretable by the Subject of a UCAN.
|
||||||
|
//
|
||||||
|
// A [Command] is composed of a leading slash which is optionally followed
|
||||||
|
// by one or more slash-separated Segments of lowercase characters.
|
||||||
|
//
|
||||||
|
// [Command]: https://github.com/ucan-wg/spec#command
|
||||||
|
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 Command{segments: segments}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse verifies that the provided string contains the required
|
||||||
|
// [segment structure] and, if valid, returns the resulting
|
||||||
|
// Command.
|
||||||
|
//
|
||||||
|
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
func Parse(s string) (Command, error) {
|
||||||
|
if !strings.HasPrefix(s, "/") {
|
||||||
|
return Command{}, ErrRequiresLeadingSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||||
|
return Command{}, ErrDisallowsTrailingSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != strings.ToLower(s) {
|
||||||
|
return Command{}, 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{strings.Split(s, "/")[1:]}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParse is the same as Parse, but panic() if the parsing fail.
|
||||||
|
func MustParse(s string) Command {
|
||||||
|
c, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// [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.
|
||||||
|
//
|
||||||
|
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
||||||
|
func Top() Command {
|
||||||
|
return New()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if the provided string is a valid UCAN command.
|
||||||
|
func IsValid(s string) bool {
|
||||||
|
_, err := Parse(s)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join appends segments to the end of this command using the required
|
||||||
|
// segment separator.
|
||||||
|
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 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 "/" + strings.Join(c.segments, "/")
|
||||||
|
}
|
||||||
21
pkg/command/command_errors.go
Normal file
21
pkg/command/command_errors.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
||||||
|
// doesn't start with a [leading slash character].
|
||||||
|
//
|
||||||
|
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrRequiresLeadingSlash = fmt.Errorf("a command requires a leading slash character")
|
||||||
|
|
||||||
|
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
||||||
|
// with a trailing slash character].
|
||||||
|
//
|
||||||
|
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrDisallowsTrailingSlash = fmt.Errorf("a command must not include a trailing slash")
|
||||||
|
|
||||||
|
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
||||||
|
// [uppercase unicode characters].
|
||||||
|
//
|
||||||
|
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrRequiresLowercase = fmt.Errorf("UCAN path segments must must not contain upper-case characters")
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTop(t *testing.T) {
|
func TestTop(t *testing.T) {
|
||||||
@@ -73,9 +73,8 @@ func TestParseCommand(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cmd, err := command.Parse(testcase.inp)
|
cmd, err := command.Parse(testcase.inp)
|
||||||
require.ErrorIs(t, err, command.ErrParse)
|
|
||||||
require.ErrorIs(t, err, testcase.err)
|
require.ErrorIs(t, err, testcase.err)
|
||||||
require.Nil(t, cmd)
|
require.Equal(t, command.Command{}, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -134,20 +133,6 @@ func invalidTestcases(t *testing.T) []errorTestcase {
|
|||||||
},
|
},
|
||||||
err: command.ErrDisallowsTrailingSlash,
|
err: command.ErrDisallowsTrailingSlash,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
testcase: testcase{
|
|
||||||
name: "only reserved ucan namespace",
|
|
||||||
inp: "/ucan",
|
|
||||||
},
|
|
||||||
err: command.ErrUCANNamespaceReserved,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testcase: testcase{
|
|
||||||
name: "reserved ucan namespace prefix",
|
|
||||||
inp: "/ucan/elem0/elem1/elem2",
|
|
||||||
},
|
|
||||||
err: command.ErrUCANNamespaceReserved,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
testcase: testcase{
|
testcase: testcase{
|
||||||
name: "uppercase character are present",
|
name: "uppercase character are present",
|
||||||
136
pkg/meta/meta.go
Normal file
136
pkg/meta/meta.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
23
pkg/meta/meta_test.go
Normal file
23
pkg/meta/meta_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
79
pkg/policy/glob.go
Normal file
79
pkg/policy/glob.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
73
pkg/policy/glob_test.go
Normal file
73
pkg/policy/glob_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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!")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||||
@@ -40,14 +40,14 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
}
|
}
|
||||||
op := must.String(opNode)
|
op := must.String(opNode)
|
||||||
|
|
||||||
arg2AsSelector := func() (selector.Selector, error) {
|
arg2AsSelector := func(op string) (selector.Selector, error) {
|
||||||
nd, _ := node.LookupByIndex(1)
|
nd, _ := node.LookupByIndex(1)
|
||||||
if nd.Kind() != datamodel.Kind_String {
|
if nd.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(path + "1/")
|
return nil, ErrNotAString(combinePath(path, op, 1))
|
||||||
}
|
}
|
||||||
sel, err := selector.Parse(must.String(nd))
|
sel, err := selector.Parse(must.String(nd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidSelector(path+"1/", err)
|
return nil, ErrInvalidSelector(combinePath(path, op, 1), err)
|
||||||
}
|
}
|
||||||
return sel, nil
|
return sel, nil
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
switch op {
|
switch op {
|
||||||
case KindNot:
|
case KindNot:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
statement, err := statementFromIPLD(path+"1/", arg2)
|
statement, err := statementFromIPLD(combinePath(path, op, 1), arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
|
|
||||||
case KindAnd, KindOr:
|
case KindAnd, KindOr:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
statement, err := statementsFromIPLD(path+"1/", arg2)
|
statement, err := statementsFromIPLD(combinePath(path, op, 1), arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
case 3:
|
case 3:
|
||||||
switch op {
|
switch op {
|
||||||
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
||||||
sel, err := arg2AsSelector()
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -85,28 +85,31 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
return equality{kind: op, selector: sel, value: arg3}, nil
|
return equality{kind: op, selector: sel, value: arg3}, nil
|
||||||
|
|
||||||
case KindLike:
|
case KindLike:
|
||||||
sel, err := arg2AsSelector()
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pattern, _ := node.LookupByIndex(2)
|
pattern, _ := node.LookupByIndex(2)
|
||||||
if pattern.Kind() != datamodel.Kind_String {
|
if pattern.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(path + "2/")
|
return nil, ErrNotAString(combinePath(path, op, 2))
|
||||||
}
|
}
|
||||||
res, err := Like(sel, must.String(pattern))
|
res, err := Like(sel, must.String(pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidPattern(path+"2/", err)
|
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|
||||||
case KindAll, KindAny:
|
case KindAll, KindAny:
|
||||||
sel, err := arg2AsSelector()
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
statementsNodes, _ := node.LookupByIndex(2)
|
statementsNode, _ := node.LookupByIndex(2)
|
||||||
statements, err := statementsFromIPLD(path+"1/", statementsNodes)
|
statement, err := statementFromIPLD(combinePath(path, op, 1), statementsNode)
|
||||||
return quantifier{kind: op, selector: sel, statements: statements}, nil
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quantifier{kind: op, selector: sel, statement: statement}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, ErrUnrecognizedOperator(path, op)
|
return nil, ErrUnrecognizedOperator(path, op)
|
||||||
@@ -123,7 +126,7 @@ func statementsFromIPLD(path string, node datamodel.Node) ([]Statement, error) {
|
|||||||
return nil, ErrNotATuple(path)
|
return nil, ErrNotATuple(path)
|
||||||
}
|
}
|
||||||
if node.Length() == 0 {
|
if node.Length() == 0 {
|
||||||
return nil, ErrEmptyList(path)
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]Statement, node.Length())
|
res := make([]Statement, node.Length())
|
||||||
@@ -229,7 +232,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = listBuilder.AssembleValue().AssignString(statement.pattern)
|
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -243,7 +246,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
args, err := statementsToIPLD(statement.statements)
|
args, err := statementToIPLD(statement.statement)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -260,3 +263,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
|
|
||||||
return list.Build(), nil
|
return list.Build(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func combinePath(prev string, operator string, index int) string {
|
||||||
|
return fmt.Sprintf("%s%d-%s/", prev, index, operator)
|
||||||
|
}
|
||||||
@@ -35,10 +35,6 @@ func ErrNotATuple(path string) error {
|
|||||||
return errWithPath{path: path, msg: "not a tuple"}
|
return errWithPath{path: path, msg: "not a tuple"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrEmptyList(path string) error {
|
|
||||||
return errWithPath{path: path, msg: "empty list"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func safeStr(str string) string {
|
func safeStr(str string) string {
|
||||||
if len(str) > 10 {
|
if len(str) > 10 {
|
||||||
return str[:10]
|
return str[:10]
|
||||||
@@ -11,16 +11,14 @@ import (
|
|||||||
func TestIpldRoundTrip(t *testing.T) {
|
func TestIpldRoundTrip(t *testing.T) {
|
||||||
const illustrativeExample = `
|
const illustrativeExample = `
|
||||||
[
|
[
|
||||||
["==", ".status", "draft"],
|
["==", ".status", "draft"],
|
||||||
["all", ".reviewer", [
|
["all", ".reviewer", ["like", ".email", "*@example.com"]],
|
||||||
["like", ".email", "*@example.com"]]
|
["any", ".tags",
|
||||||
],
|
["or", [
|
||||||
["any", ".tags", [
|
["==", ".", "news"],
|
||||||
["or", [
|
["==", ".", "press"]
|
||||||
["==", ".", "news"],
|
]]
|
||||||
["==", ".", "press"]]
|
]
|
||||||
]]
|
|
||||||
]
|
|
||||||
]`
|
]`
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Match determines if the IPLD node matches the policy document.
|
// Match determines if the IPLD node matches the policy document.
|
||||||
@@ -26,11 +26,22 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
|||||||
switch statement.Kind() {
|
switch statement.Kind() {
|
||||||
case KindEqual:
|
case KindEqual:
|
||||||
if s, ok := statement.(equality); ok {
|
if s, ok := statement.(equality); ok {
|
||||||
one, _, err := selector.Select(s.selector, node)
|
one, many, err := selector.Select(s.selector, node)
|
||||||
if err != nil || one == nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return datamodel.DeepEqual(s.value, one)
|
if one != nil {
|
||||||
|
return datamodel.DeepEqual(s.value, one)
|
||||||
|
}
|
||||||
|
if many != nil {
|
||||||
|
for _, n := range many {
|
||||||
|
if eq := datamodel.DeepEqual(s.value, n); eq {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
case KindGreaterThan:
|
case KindGreaterThan:
|
||||||
if s, ok := statement.(equality); ok {
|
if s, ok := statement.(equality); ok {
|
||||||
@@ -101,7 +112,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return s.glob.Match(v)
|
return s.pattern.Match(v)
|
||||||
}
|
}
|
||||||
case KindAll:
|
case KindAll:
|
||||||
if s, ok := statement.(quantifier); ok {
|
if s, ok := statement.(quantifier); ok {
|
||||||
@@ -110,7 +121,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, n := range many {
|
for _, n := range many {
|
||||||
ok := Match(s.statements, n)
|
ok := matchStatement(s.statement, n)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -119,16 +130,25 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
|||||||
}
|
}
|
||||||
case KindAny:
|
case KindAny:
|
||||||
if s, ok := statement.(quantifier); ok {
|
if s, ok := statement.(quantifier); ok {
|
||||||
_, many, err := selector.Select(s.selector, node)
|
one, many, err := selector.Select(s.selector, node)
|
||||||
if err != nil || many == nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, n := range many {
|
if one != nil {
|
||||||
ok := Match(s.statements, n)
|
ok := matchStatement(s.statement, one)
|
||||||
if ok {
|
if ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if many != nil {
|
||||||
|
for _, n := range many {
|
||||||
|
ok := matchStatement(s.statement, n)
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,13 @@ import (
|
|||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/literal"
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMatch(t *testing.T) {
|
func TestMatch(t *testing.T) {
|
||||||
@@ -400,8 +401,7 @@ func TestMatch(t *testing.T) {
|
|||||||
pol := Policy{
|
pol := Policy{
|
||||||
Any(
|
Any(
|
||||||
selector.MustParse(".[]"),
|
selector.MustParse(".[]"),
|
||||||
GreaterThan(selector.MustParse(".value"), literal.Int(10)),
|
GreaterThan(selector.MustParse(".value"), literal.Int(60)),
|
||||||
LessThan(selector.MustParse(".value"), literal.Int(50)),
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
ok := Match(pol, nd)
|
ok := Match(pol, nd)
|
||||||
@@ -418,3 +418,124 @@ func TestMatch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPolicyExamples(t *testing.T) {
|
||||||
|
makeNode := func(data string) ipld.Node {
|
||||||
|
nd, err := ipld.Decode([]byte(data), dagjson.Decode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return nd
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate := func(statement string, data ipld.Node) bool {
|
||||||
|
// we need to wrap statement with [] to make them a policy
|
||||||
|
policy := fmt.Sprintf("[%s]", statement)
|
||||||
|
|
||||||
|
pol, err := FromDagJson(policy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return Match(pol, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("And", func(t *testing.T) {
|
||||||
|
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`["and", []]`, data))
|
||||||
|
require.True(t, evaluate(`
|
||||||
|
["and", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
[">=", ".age", 21]
|
||||||
|
]]`, data))
|
||||||
|
require.False(t, evaluate(`
|
||||||
|
["and", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
[">=", ".age", 21],
|
||||||
|
["==", ".nationalities", ["American"]]
|
||||||
|
]]`, data))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Or", func(t *testing.T) {
|
||||||
|
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`["or", []]`, data))
|
||||||
|
require.True(t, evaluate(`
|
||||||
|
["or", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
[">", ".age", 45]
|
||||||
|
]]
|
||||||
|
`, data))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Not", func(t *testing.T) {
|
||||||
|
data := makeNode(`{ "name": "Katie", "nationalities": ["Canadian", "South African"] }`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`
|
||||||
|
["not",
|
||||||
|
["and", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
["==", ".nationalities", ["American"]]
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
`, data))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("All", func(t *testing.T) {
|
||||||
|
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
||||||
|
|
||||||
|
require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Any", func(t *testing.T) {
|
||||||
|
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
Match(policy, dataNode)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,10 +3,9 @@ package policy
|
|||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gobwas/glob"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -40,23 +39,23 @@ func (e equality) Kind() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Equal(selector selector.Selector, value ipld.Node) Statement {
|
func Equal(selector selector.Selector, value ipld.Node) Statement {
|
||||||
return equality{KindEqual, selector, value}
|
return equality{kind: KindEqual, selector: selector, value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
||||||
return equality{KindGreaterThan, selector, value}
|
return equality{kind: KindGreaterThan, selector: selector, value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||||
return equality{KindGreaterThanOrEqual, selector, value}
|
return equality{kind: KindGreaterThanOrEqual, selector: selector, value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
||||||
return equality{KindLessThan, selector, value}
|
return equality{kind: KindLessThan, selector: selector, value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||||
return equality{KindLessThanOrEqual, selector, value}
|
return equality{kind: KindLessThanOrEqual, selector: selector, value: value}
|
||||||
}
|
}
|
||||||
|
|
||||||
type negation struct {
|
type negation struct {
|
||||||
@@ -68,7 +67,7 @@ func (n negation) Kind() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Not(stmt Statement) Statement {
|
func Not(stmt Statement) Statement {
|
||||||
return negation{stmt}
|
return negation{statement: stmt}
|
||||||
}
|
}
|
||||||
|
|
||||||
type connective struct {
|
type connective struct {
|
||||||
@@ -81,17 +80,16 @@ func (c connective) Kind() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func And(stmts ...Statement) Statement {
|
func And(stmts ...Statement) Statement {
|
||||||
return connective{KindAnd, stmts}
|
return connective{kind: KindAnd, statements: stmts}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Or(stmts ...Statement) Statement {
|
func Or(stmts ...Statement) Statement {
|
||||||
return connective{KindOr, stmts}
|
return connective{kind: KindOr, statements: stmts}
|
||||||
}
|
}
|
||||||
|
|
||||||
type wildcard struct {
|
type wildcard struct {
|
||||||
selector selector.Selector
|
selector selector.Selector
|
||||||
pattern string
|
pattern glob
|
||||||
glob glob.Glob // not serialized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n wildcard) Kind() string {
|
func (n wildcard) Kind() string {
|
||||||
@@ -99,27 +97,28 @@ func (n wildcard) Kind() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Like(selector selector.Selector, pattern string) (Statement, error) {
|
func Like(selector selector.Selector, pattern string) (Statement, error) {
|
||||||
g, err := glob.Compile(pattern)
|
g, err := parseGlob(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return wildcard{selector, pattern, g}, nil
|
|
||||||
|
return wildcard{selector: selector, pattern: g}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type quantifier struct {
|
type quantifier struct {
|
||||||
kind string
|
kind string
|
||||||
selector selector.Selector
|
selector selector.Selector
|
||||||
statements []Statement
|
statement Statement
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n quantifier) Kind() string {
|
func (n quantifier) Kind() string {
|
||||||
return n.kind
|
return n.kind
|
||||||
}
|
}
|
||||||
|
|
||||||
func All(selector selector.Selector, policy ...Statement) Statement {
|
func All(selector selector.Selector, statement Statement) Statement {
|
||||||
return quantifier{KindAll, selector, policy}
|
return quantifier{kind: KindAll, selector: selector, statement: statement}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Any(selector selector.Selector, policy ...Statement) Statement {
|
func Any(selector selector.Selector, statement Statement) Statement {
|
||||||
return quantifier{KindAny, selector, policy}
|
return quantifier{kind: KindAny, selector: selector, statement: statement}
|
||||||
}
|
}
|
||||||
462
pkg/policy/selector/selector.go
Normal file
462
pkg/policy/selector/selector.go
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
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/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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSupported Forms runs tests against the Selector according to the
|
// TestSupported Forms runs tests against the Selector according to the
|
||||||
@@ -30,14 +30,14 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
||||||
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `()`},
|
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `[]`},
|
||||||
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
|
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
|
||||||
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
||||||
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
||||||
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
||||||
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
||||||
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
||||||
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}`, Output: `0`},
|
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}}`, Output: `0`},
|
||||||
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
255
tokens/delegation/delegation.go
Normal file
255
tokens/delegation/delegation.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
// 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/ipfs/go-cid"
|
||||||
|
"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
|
||||||
|
// The CID of the Token when enclosed in an Envelope and encoded to DAG-CBOR
|
||||||
|
cid cid.Cid
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
cid: cid.Undef,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// CID returns the content identifier of the Token model when enclosed
|
||||||
|
// in an Envelope and encoded to DAG-CBOR.
|
||||||
|
// Returns cid.Undef if the token has not been serialized or deserialized yet.
|
||||||
|
func (t *Token) CID() cid.Cid {
|
||||||
|
return t.cid
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
136
tokens/delegation/delegation_test.go
Normal file
136
tokens/delegation/delegation_test.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package delegation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
|
||||||
|
"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/tokens/delegation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||||
|
|
||||||
|
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
||||||
|
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
|
||||||
|
|
||||||
|
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
|
||||||
|
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
||||||
|
|
||||||
|
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
|
||||||
|
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||||
|
subJectCmd = "/foo/bar"
|
||||||
|
subjectPol = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
".status",
|
||||||
|
"draft"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"all",
|
||||||
|
".reviewer",
|
||||||
|
[
|
||||||
|
"like",
|
||||||
|
".email",
|
||||||
|
"*@example.com"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"any",
|
||||||
|
".tags",
|
||||||
|
[
|
||||||
|
"or",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
".",
|
||||||
|
"news"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
".",
|
||||||
|
"press"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
||||||
|
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConstructors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
privKey := privKey(t, issuerPrivKeyCfg)
|
||||||
|
|
||||||
|
aud, err := did.Parse(AudienceDID)
|
||||||
|
|
||||||
|
sub, err := did.Parse(subjectDID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cmd, err := command.Parse(subJectCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pol, err := policy.FromDagJson(subjectPol)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagJson(privKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Log(string(data))
|
||||||
|
|
||||||
|
golden.Assert(t, string(data), "new.dagjson")
|
||||||
|
})
|
||||||
|
|
||||||
|
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"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagJson(privKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Log(string(data))
|
||||||
|
|
||||||
|
golden.Assert(t, string(data), "root.dagjson")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
||||||
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
313
tokens/delegation/examples_test.go
Normal file
313
tokens/delegation/examples_test.go
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
package delegation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/tokens/delegation"
|
||||||
|
"github.com/ucan-wg/go-ucan/tokens/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() {
|
||||||
|
issuerPrivKey := examplePrivKey(issuerPrivKeyCfg)
|
||||||
|
audienceDID := exampleDID(AudienceDID)
|
||||||
|
command := exampleCommand(subJectCmd)
|
||||||
|
policy := examplePolicy(subjectPol)
|
||||||
|
subjectDID := exampleDID(subjectDID)
|
||||||
|
|
||||||
|
// Don't do this in your code - a nonce should be a cryptographically
|
||||||
|
// strong random slice of bytes to ensure the integrity of your private
|
||||||
|
// key. For this example, a fixed nonce is required to obtain the fixed
|
||||||
|
// printed output (below). If unsure of what value to supply for the
|
||||||
|
// nonce, don't pass the WithNonce option and one will be generated
|
||||||
|
// when the token is created.
|
||||||
|
nonce := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
|
||||||
|
|
||||||
|
tkn, err := delegation.New(
|
||||||
|
issuerPrivKey,
|
||||||
|
audienceDID,
|
||||||
|
command,
|
||||||
|
policy,
|
||||||
|
delegation.WithSubject(subjectDID),
|
||||||
|
delegation.WithNonce(nonce),
|
||||||
|
)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
data, id, err := tkn.ToSealed(issuerPrivKey)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
printCIDAndSealed(id, data)
|
||||||
|
|
||||||
|
//Output:
|
||||||
|
//CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
|
||||||
|
//DAG-CBOR (base64) out: glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL
|
||||||
|
//Converted to DAG-JSON out:
|
||||||
|
//[
|
||||||
|
// {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "mnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBQ"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "h": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "NO0BcQ"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "ucan/dlg@1.0.0-rc.1": {
|
||||||
|
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||||
|
// "cmd": "/foo/bar",
|
||||||
|
// "exp": null,
|
||||||
|
// "iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
|
||||||
|
// "meta": {},
|
||||||
|
// "nonce": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "AAECAwQFBgcICQoL"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "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() {
|
||||||
|
issuerPrivKey := examplePrivKey(issuerPrivKeyCfg)
|
||||||
|
audienceDID := exampleDID(AudienceDID)
|
||||||
|
command := exampleCommand(subJectCmd)
|
||||||
|
policy := examplePolicy(subjectPol)
|
||||||
|
|
||||||
|
// Don't do this in your code - a nonce should be a cryptographically
|
||||||
|
// strong random slice of bytes to ensure the integrity of your private
|
||||||
|
// key. For this example, a fixed nonce is required to obtain the fixed
|
||||||
|
// printed output (below). If unsure of what value to supply for the
|
||||||
|
// nonce, don't pass the WithNonce option and one will be generated
|
||||||
|
// when the token is created.
|
||||||
|
nonce := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
|
||||||
|
|
||||||
|
tkn, err := delegation.Root(
|
||||||
|
issuerPrivKey,
|
||||||
|
audienceDID,
|
||||||
|
command,
|
||||||
|
policy,
|
||||||
|
delegation.WithNonce(nonce),
|
||||||
|
)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
data, id, err := tkn.ToSealed(issuerPrivKey)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
printCIDAndSealed(id, data)
|
||||||
|
|
||||||
|
//Output:
|
||||||
|
//CID (base58BTC): zdpuAnbsR3e6DK8hBk5WA7KwbHYN6CKY4a3Bv1GNehvFYShQ8
|
||||||
|
//DAG-CBOR (base64) out: glhA67ASBczF/wlIP0ESENn+4ZNQKukjcTNz+fo7K2tYa6OUm0rWICDJJkDWm7lJeQt+KvSA+Y4ctHTQbAr3Lr7mDqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rcHpuMm4zWkdUMlZhcU1HU1FDM3R6bXpWNFRTOVM3MWlGc0RYRTFXbm9OSDJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL
|
||||||
|
//Converted to DAG-JSON out:
|
||||||
|
//[
|
||||||
|
// {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "67ASBczF/wlIP0ESENn+4ZNQKukjcTNz+fo7K2tYa6OUm0rWICDJJkDWm7lJeQt+KvSA+Y4ctHTQbAr3Lr7mDg"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "h": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "NO0BcQ"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "ucan/dlg@1.0.0-rc.1": {
|
||||||
|
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||||
|
// "cmd": "/foo/bar",
|
||||||
|
// "exp": null,
|
||||||
|
// "iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
|
||||||
|
// "meta": {},
|
||||||
|
// "nonce": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "AAECAwQFBgcICQoL"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "pol": [
|
||||||
|
// [
|
||||||
|
// "==",
|
||||||
|
// ".status",
|
||||||
|
// "draft"
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "all",
|
||||||
|
// ".reviewer",
|
||||||
|
// [
|
||||||
|
// "like",
|
||||||
|
// ".email",
|
||||||
|
// "*@example.com"
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "any",
|
||||||
|
// ".tags",
|
||||||
|
// [
|
||||||
|
// "or",
|
||||||
|
// [
|
||||||
|
// [
|
||||||
|
// "==",
|
||||||
|
// ".",
|
||||||
|
// "news"
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "==",
|
||||||
|
// ".",
|
||||||
|
// "press"
|
||||||
|
// ]
|
||||||
|
// ]
|
||||||
|
// ]
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// "sub": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following example demonstrates how to get a delegation.Token from
|
||||||
|
// a DAG-CBOR []byte.
|
||||||
|
func ExampleToken_FromSealed() {
|
||||||
|
cborBytes := exampleCBORData()
|
||||||
|
fmt.Println("DAG-CBOR (base64) in:", base64.StdEncoding.EncodeToString(cborBytes))
|
||||||
|
|
||||||
|
tkn, err := delegation.FromSealed(cborBytes)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(tkn.CID()))
|
||||||
|
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): TODO")
|
||||||
|
fmt.Println("Nonce (nonce):", hex.EncodeToString(tkn.Nonce()))
|
||||||
|
fmt.Println("Meta (meta): TODO")
|
||||||
|
fmt.Println("NotBefore (nbf):", tkn.NotBefore())
|
||||||
|
fmt.Println("Expiration (exp):", tkn.Expiration())
|
||||||
|
|
||||||
|
//Output:
|
||||||
|
//DAG-CBOR (base64) in: glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL
|
||||||
|
//CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
|
||||||
|
//Issuer (iss): did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2
|
||||||
|
//Audience (aud): did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv
|
||||||
|
//Subject (sub): did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2
|
||||||
|
//Command (cmd): /foo/bar
|
||||||
|
//Policy (pol): TODO
|
||||||
|
//Nonce (nonce): 000102030405060708090a0b
|
||||||
|
//Meta (meta): TODO
|
||||||
|
//NotBefore (nbf): <nil>
|
||||||
|
//Expiration (exp): <nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleCBORData() []byte {
|
||||||
|
data, err := base64.StdEncoding.DecodeString("glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL")
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleDID(didStr string) did.DID {
|
||||||
|
id, err := did.Parse(didStr)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleCommand(cmdStr string) command.Command {
|
||||||
|
cmd, err := command.Parse(cmdStr)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePolicy(policyJSON string) policy.Policy {
|
||||||
|
pol, err := policy.FromDagJson(policyJSON)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
return pol
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePrivKey(privKeyCfg string) crypto.PrivKey {
|
||||||
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
236
tokens/delegation/ipld.go
Normal file
236
tokens/delegation/ipld.go
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
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/tokens/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 Seal 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, error) {
|
||||||
|
tkn, err := FromDagCbor(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := envelope.CIDFromBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn.cid = id
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
||||||
|
func FromSealedReader(r io.Reader) (*Token, error) {
|
||||||
|
cidReader := envelope.NewCIDReader(r)
|
||||||
|
|
||||||
|
tkn, err := FromDagCborReader(cidReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := cidReader.CID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn.cid = id
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode marshals a View 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 View 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 View 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 View.
|
||||||
|
//
|
||||||
|
// An error is returned if the conversion fails, or if the resulting
|
||||||
|
// View 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 View.
|
||||||
|
//
|
||||||
|
// An error is returned if the conversion fails, or if the resulting
|
||||||
|
// View 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 View.
|
||||||
|
//
|
||||||
|
// An error is returned if the conversion fails, or if the resulting
|
||||||
|
// View 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
72
tokens/delegation/options.go
Normal file
72
tokens/delegation/options.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,21 @@ import (
|
|||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
|
"github.com/ucan-wg/go-ucan/tokens/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
|
//go:embed delegation.ipldsch
|
||||||
var schemaBytes []byte
|
var schemaBytes []byte
|
||||||
|
|
||||||
@@ -29,11 +41,13 @@ func mustLoadSchema() *schema.TypeSystem {
|
|||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
|
|
||||||
func PayloadType() schema.Type {
|
func payloadType() schema.Type {
|
||||||
return mustLoadSchema().TypeByName("Payload")
|
return mustLoadSchema().TypeByName("Payload")
|
||||||
}
|
}
|
||||||
|
|
||||||
type PayloadModel struct {
|
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
||||||
|
|
||||||
|
type tokenPayloadModel struct {
|
||||||
// Issuer DID (sender)
|
// Issuer DID (sender)
|
||||||
Iss string
|
Iss string
|
||||||
// Audience DID (receiver)
|
// Audience DID (receiver)
|
||||||
@@ -52,8 +66,7 @@ type PayloadModel struct {
|
|||||||
Nonce []byte
|
Nonce []byte
|
||||||
|
|
||||||
// Arbitrary Metadata
|
// Arbitrary Metadata
|
||||||
// optional: can be nil
|
Meta meta.Meta
|
||||||
Meta MetaModel
|
|
||||||
|
|
||||||
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
// optional: can be nil
|
// optional: can be nil
|
||||||
@@ -63,7 +76,10 @@ type PayloadModel struct {
|
|||||||
Exp *int64
|
Exp *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetaModel struct {
|
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
||||||
Keys []string
|
return bindnode.Prototype((*tokenPayloadModel)(nil), payloadType())
|
||||||
Values map[string]datamodel.Node
|
}
|
||||||
|
|
||||||
|
func (*tokenPayloadModel) Tag() string {
|
||||||
|
return Tag
|
||||||
}
|
}
|
||||||
177
tokens/delegation/schema_test.go
Normal file
177
tokens/delegation/schema_test.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
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/tokens/delegation"
|
||||||
|
"github.com/ucan-wg/go-ucan/tokens/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, err := delegation.FromSealed(cborBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, id, p2.CID())
|
||||||
|
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, err := delegation.FromSealedReader(cborBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Log(len(p2.CID().Bytes()), p2.CID().Bytes())
|
||||||
|
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(p2.CID()))
|
||||||
|
|
||||||
|
readJson := &bytes.Buffer{}
|
||||||
|
require.NoError(t, p2.ToDagJsonWriter(readJson, privKey))
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(delegationJson), readJson.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSchemaLoad(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRoundTrip(b *testing.B) {
|
||||||
|
delegationJson := golden.Get(b, "new.dagjson")
|
||||||
|
privKey := privKey(b, issuerPrivKeyCfg)
|
||||||
|
|
||||||
|
b.Run("via buffers", func(b *testing.B) {
|
||||||
|
p1, _ := delegation.FromDagJson(delegationJson)
|
||||||
|
cborBytes, _, _ := p1.ToSealed(privKey)
|
||||||
|
p2, _ := delegation.FromSealed(cborBytes)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
b.Run("FromDagJson", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = delegation.FromDagJson(delegationJson)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Seal", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _, _ = p1.ToSealed(privKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Unseal", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = delegation.FromSealed(cborBytes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("ToDagJson", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = p2.ToDagJson(privKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("via streaming", func(b *testing.B) {
|
||||||
|
p1, _ := delegation.FromDagJsonReader(bytes.NewReader(delegationJson))
|
||||||
|
cborBuf := &bytes.Buffer{}
|
||||||
|
_, _ = p1.ToSealedWriter(cborBuf, privKey)
|
||||||
|
cborBytes := cborBuf.Bytes()
|
||||||
|
p2, _ := delegation.FromSealedReader(bytes.NewReader(cborBytes))
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
b.Run("FromDagJsonReader", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
reader := bytes.NewReader(delegationJson)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = reader.Seek(0, 0)
|
||||||
|
_, _ = delegation.FromDagJsonReader(reader)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("SealWriter", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
buf.Reset()
|
||||||
|
_, _ = p1.ToSealedWriter(buf, privKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("UnsealReader", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
reader := bytes.NewReader(cborBytes)
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, _ = reader.Seek(0, 0)
|
||||||
|
_, _ = delegation.FromSealedReader(reader)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("ToDagJsonReader", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
buf.Reset()
|
||||||
|
_ = p2.ToDagJsonWriter(buf, privKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
1
tokens/delegation/testdata/new.dagjson
vendored
Normal file
1
tokens/delegation/testdata/new.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"/":{"bytes":"FM6otj0r/noJWiGAC5WV86xAazxrF173IihuHJgEt35CtSzjeaelrR3UwaSr8xbE9sLpo5xJhUbo0QLI273hDA"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"}}]
|
||||||
1
tokens/delegation/testdata/root.dagjson
vendored
Normal file
1
tokens/delegation/testdata/root.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"/":{"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"}}]
|
||||||
124
tokens/internal/envelope/cid.go
Normal file
124
tokens/internal/envelope/cid.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
86
tokens/internal/envelope/cid_test.go
Normal file
86
tokens/internal/envelope/cid_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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/tokens/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)
|
||||||
|
})
|
||||||
|
}
|
||||||
137
tokens/internal/envelope/example_test.go
Normal file
137
tokens/internal/envelope/example_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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"
|
||||||
|
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
314
tokens/internal/envelope/ipld.go
Normal file
314
tokens/internal/envelope/ipld.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
// 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"
|
||||||
|
"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/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/tokens/internal/varsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
const varsigHeaderKey = "h"
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
undef := *new(T)
|
||||||
|
|
||||||
|
node, err := ipld.Decode(b, dagcbor.Decode)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, err := fromIPLD[T](node)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
undef := *new(T)
|
||||||
|
|
||||||
|
tkn, err := fromIPLD[T](node)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
||||||
|
undef := *new(T)
|
||||||
|
|
||||||
|
signatureNode, err := node.LookupByIndex(0)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := signatureNode.AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sigPayloadNode, err := node.LookupByIndex(1)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag())
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 := tokenPayloadNode.LookupByString("iss")
|
||||||
|
if err != nil {
|
||||||
|
return undef, 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 := undef.Prototype().Representation().NewBuilder()
|
||||||
|
|
||||||
|
err = nb.AssignNode(tokenPayloadNode)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenPayloadNode = nb.Build()
|
||||||
|
|
||||||
|
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
|
||||||
|
if tokenPayload == nil {
|
||||||
|
return undef, errors.New("failed to Unwrap the TokenPayload")
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, ok := tokenPayload.(T)
|
||||||
|
if !ok {
|
||||||
|
return undef, 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 undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issuerDID, err := did.Parse(issuer)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issuerPubKey, err := issuerDID.PubKey()
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
varsigHeader, err := varsigHeaderNode.AsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(varsigHeader) != string(issuerVarsigHeader) {
|
||||||
|
return undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode)
|
||||||
|
if err != nil {
|
||||||
|
return undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = issuerPubKey.Verify(data, signature)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return undef, 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
151
tokens/internal/envelope/ipld_test.go
Normal file
151
tokens/internal/envelope/ipld_test.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package envelope_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
1
tokens/internal/envelope/testdata/example.dagcbor
vendored
Normal file
1
tokens/internal/envelope/testdata/example.dagcbor
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
‚X@|úŸÀ½â–ðõ+ÁŠ!µ.®ÿhéÍúGO-ü¬”jÉsyÖsY¨quëiþ“ä°¬Íuý#ò¼’ç˜c¢ahD4íqxucan/example@v1.0.0-rc.1¢cissx8did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nhehelloeworld
|
||||||
1
tokens/internal/envelope/testdata/example.dagjson
vendored
Normal file
1
tokens/internal/envelope/testdata/example.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"/":{"bytes":"fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/example@v1.0.0-rc.1":{"hello":"world","iss":"did:key:z6MkpuK2Amsu1RqcLGgmHHQHhvmeXCCBVsM4XFSg2cCyg4Nh"}}]
|
||||||
6
tokens/internal/envelope/testdata/example.ipldsch
vendored
Normal file
6
tokens/internal/envelope/testdata/example.ipldsch
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
type DID string
|
||||||
|
|
||||||
|
type Example struct {
|
||||||
|
hello String
|
||||||
|
issuer DID (rename "iss")
|
||||||
|
}
|
||||||
133
tokens/internal/varsig/varsig.go
Normal file
133
tokens/internal/varsig/varsig.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// Package varsig implements the portion of the [varsig specification]
|
||||||
|
// that's needed for the UCAN [Envelope].
|
||||||
|
//
|
||||||
|
// While the [Envelope] specification has a field that's labelled
|
||||||
|
// "VarsigHeader", this field is actually the prefix, header and segments
|
||||||
|
// of the body excluding the signature itself (which is a different field
|
||||||
|
// in the [Envelope]).
|
||||||
|
//
|
||||||
|
// Given that [go-ucan] supports a limited number of public key types,
|
||||||
|
// and that the signature isn't part of the resulting field, the values
|
||||||
|
// that are used are constants. Note that for key types that are fully
|
||||||
|
// specified in the [did:key], the [VarsigHeader] field isn't technically
|
||||||
|
// needed and could theoretically conflict with the DID.
|
||||||
|
//
|
||||||
|
// Treating these values as constants has no impact when issuing or
|
||||||
|
// delegating tokens. When decoding tokens, simply matching the strings
|
||||||
|
// will allow us to detect errors but won't provide as much detail (e.g.
|
||||||
|
// we can't indicate that the signature was incorrectly generated from
|
||||||
|
// a DAG-JSON encoding.)
|
||||||
|
//
|
||||||
|
// [varsig specification]: https://github.com/ChainAgnostic/varsig
|
||||||
|
// [Envelope]:https://github.com/ucan-wg/spec#envelope
|
||||||
|
// [go-ucan]: https://github.com/ucan-wg/go-ucan
|
||||||
|
package varsig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
|
"github.com/multiformats/go-multicodec"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Prefix = 0x34
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrUnknownHeader is returned when it's not possible to decode the
|
||||||
|
// provided string into a libp2p public key type.
|
||||||
|
var ErrUnknownHeader = errors.New("could not decode unknown header")
|
||||||
|
|
||||||
|
// ErrUnknownKeyType is returned when value provided is not a valid
|
||||||
|
// libp2p public key type.
|
||||||
|
var ErrUnknownKeyType = errors.New("could not encode unsupported key type")
|
||||||
|
|
||||||
|
var (
|
||||||
|
decMap = headerToKeyType()
|
||||||
|
encMap = keyTypeToHeader()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decode returns either the pb.KeyType associated with the provided Header
|
||||||
|
// or an error.
|
||||||
|
//
|
||||||
|
// Currently, only the four key types supported by the [go-libp2p/core/crypto]
|
||||||
|
// library are supported.
|
||||||
|
//
|
||||||
|
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
||||||
|
func Decode(header []byte) (pb.KeyType, error) {
|
||||||
|
keyType, ok := decMap[string(header)]
|
||||||
|
if !ok {
|
||||||
|
return -1, fmt.Errorf("%w: %s", ErrUnknownHeader, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode returns either the header associated with the provided pb.KeyType
|
||||||
|
// or an error indicating the header was unknown.
|
||||||
|
//
|
||||||
|
// Currently, only the four key types supported by the [go-libp2p/core/crypto]
|
||||||
|
// library are supported.
|
||||||
|
//
|
||||||
|
// [go-libp2p/core/crypto]: github.com/libp2p/go-libp2p/core/crypto
|
||||||
|
func Encode(keyType pb.KeyType) ([]byte, error) {
|
||||||
|
header, ok := encMap[keyType]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrUnknownKeyType, keyType.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(header), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyTypeToHeader() map[pb.KeyType]string {
|
||||||
|
const rsaSigLen = 0x100
|
||||||
|
|
||||||
|
return map[pb.KeyType]string{
|
||||||
|
pb.KeyType_RSA: header(
|
||||||
|
Prefix,
|
||||||
|
multicodec.RsaPub,
|
||||||
|
multicodec.Sha2_256,
|
||||||
|
rsaSigLen,
|
||||||
|
multicodec.DagCbor,
|
||||||
|
),
|
||||||
|
pb.KeyType_Ed25519: header(
|
||||||
|
Prefix,
|
||||||
|
multicodec.Ed25519Pub,
|
||||||
|
multicodec.DagCbor,
|
||||||
|
),
|
||||||
|
pb.KeyType_Secp256k1: header(
|
||||||
|
Prefix,
|
||||||
|
multicodec.Secp256k1Pub,
|
||||||
|
multicodec.Sha2_256,
|
||||||
|
multicodec.DagCbor,
|
||||||
|
),
|
||||||
|
pb.KeyType_ECDSA: header(
|
||||||
|
Prefix,
|
||||||
|
multicodec.Es256,
|
||||||
|
multicodec.Sha2_256,
|
||||||
|
multicodec.DagCbor,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headerToKeyType() map[string]pb.KeyType {
|
||||||
|
out := make(map[string]pb.KeyType, len(encMap))
|
||||||
|
|
||||||
|
for keyType, header := range encMap {
|
||||||
|
out[header] = keyType
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func header(vals ...multicodec.Code) string {
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
for _, val := range vals {
|
||||||
|
buf = binary.AppendUvarint(buf, uint64(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
51
tokens/internal/varsig/varsig_test.go
Normal file
51
tokens/internal/varsig/varsig_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package varsig_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
notAHeader := base64.RawStdEncoding.EncodeToString([]byte("not a header"))
|
||||||
|
keyType, err := varsig.Decode([]byte(notAHeader))
|
||||||
|
assert.Equal(t, pb.KeyType(-1), keyType)
|
||||||
|
assert.ErrorIs(t, err, varsig.ErrUnknownHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleDecode() {
|
||||||
|
hdr, err := base64.RawStdEncoding.DecodeString("NIUkEoACcQ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyType, _ := varsig.Decode(hdr)
|
||||||
|
fmt.Println(keyType.String())
|
||||||
|
// Output:
|
||||||
|
// RSA
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
header, err := varsig.Encode(pb.KeyType(99))
|
||||||
|
assert.Nil(t, header)
|
||||||
|
assert.ErrorIs(t, err, varsig.ErrUnknownKeyType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleEncode() {
|
||||||
|
header, _ := varsig.Encode(pb.KeyType_RSA)
|
||||||
|
fmt.Println(base64.RawStdEncoding.EncodeToString(header))
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// NIUkEoACcQ
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user