Compare commits
4 Commits
v1-fix-pol
...
v1-policy-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfd5f00618 | ||
|
|
d66b8e40ec | ||
|
|
18820f5e9d | ||
|
|
ddaa67ed7d |
34
.github/workflows/bench.yml
vendored
34
.github/workflows/bench.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: go continuous benchmark
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v1
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: Run Go continuous benchmark
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: Run benchmark
|
||||
run: go test -v ./... -bench=. -run=xxx -benchmem | tee output.txt
|
||||
|
||||
- name: Store benchmark result
|
||||
uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
name: Go Benchmark
|
||||
tool: 'go'
|
||||
output-file-path: output.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Push and deploy GitHub pages branch automatically
|
||||
auto-push: true
|
||||
# Show alert with commit comment on detecting possible performance regression
|
||||
alert-threshold: '200%'
|
||||
comment-on-alert: true
|
||||
@@ -116,11 +116,3 @@ func Parse(str string) (DID, error) {
|
||||
buf = append(buf, suffix...)
|
||||
return DID{str: string(buf), code: DIDCore}, nil
|
||||
}
|
||||
|
||||
func MustParse(str string) DID {
|
||||
did, err := Parse(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return did
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package did
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDIDKey(t *testing.T) {
|
||||
@@ -17,18 +15,6 @@ func TestParseDIDKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMustParseDIDKey(t *testing.T) {
|
||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
require.NotPanics(t, func() {
|
||||
d := MustParse(str)
|
||||
require.Equal(t, str, d.String())
|
||||
})
|
||||
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
require.Panics(t, func() {
|
||||
MustParse(str)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecodeDIDKey(t *testing.T) {
|
||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
d0, err := Parse(str)
|
||||
|
||||
5
go.mod
5
go.mod
@@ -1,6 +1,8 @@
|
||||
module github.com/ucan-wg/go-ucan
|
||||
|
||||
go 1.23
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.4
|
||||
|
||||
require (
|
||||
github.com/ipfs/go-cid v0.4.1
|
||||
@@ -29,7 +31,6 @@ require (
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
)
|
||||
|
||||
3
go.sum
3
go.sum
@@ -80,9 +80,8 @@ 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=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
@@ -16,12 +16,14 @@ var _ fmt.Stringer = (*Command)(nil)
|
||||
// by one or more slash-separated Segments of lowercase characters.
|
||||
//
|
||||
// [Command]: https://github.com/ucan-wg/spec#command
|
||||
type Command string
|
||||
type Command struct {
|
||||
segments []string
|
||||
}
|
||||
|
||||
// New creates a validated command from the provided list of segment strings.
|
||||
// An error is returned if an invalid Command would be formed
|
||||
func New(segments ...string) Command {
|
||||
return Top().Join(segments...)
|
||||
return Command{segments: segments}
|
||||
}
|
||||
|
||||
// Parse verifies that the provided string contains the required
|
||||
@@ -31,20 +33,20 @@ func New(segments ...string) Command {
|
||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||
func Parse(s string) (Command, error) {
|
||||
if !strings.HasPrefix(s, "/") {
|
||||
return "", ErrRequiresLeadingSlash
|
||||
return Command{}, ErrRequiresLeadingSlash
|
||||
}
|
||||
|
||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||
return "", ErrDisallowsTrailingSlash
|
||||
return Command{}, ErrDisallowsTrailingSlash
|
||||
}
|
||||
|
||||
if s != strings.ToLower(s) {
|
||||
return "", ErrRequiresLowercase
|
||||
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(s), nil
|
||||
return Command{strings.Split(s, "/")[1:]}, nil
|
||||
}
|
||||
|
||||
// MustParse is the same as Parse, but panic() if the parsing fail.
|
||||
@@ -56,14 +58,14 @@ func MustParse(s string) Command {
|
||||
return c
|
||||
}
|
||||
|
||||
// Top is the most powerful capability.
|
||||
// [Top] is the most powerful capability.
|
||||
//
|
||||
// This function returns a Command that is a wildcard and therefore represents the
|
||||
// most powerful ability. As such, it should be handled with care and used sparingly.
|
||||
//
|
||||
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
||||
func Top() Command {
|
||||
return Command(separator)
|
||||
return New()
|
||||
}
|
||||
|
||||
// IsValid returns true if the provided string is a valid UCAN command.
|
||||
@@ -75,34 +77,17 @@ func IsValid(s string) bool {
|
||||
// Join appends segments to the end of this command using the required
|
||||
// segment separator.
|
||||
func (c Command) Join(segments ...string) Command {
|
||||
size := 0
|
||||
for _, s := range segments {
|
||||
size += len(s)
|
||||
}
|
||||
if size == 0 {
|
||||
return c
|
||||
}
|
||||
buf := make([]byte, 0, len(c)+size+len(segments))
|
||||
buf = append(buf, []byte(c)...)
|
||||
for _, s := range segments {
|
||||
if s != "" {
|
||||
if len(buf) > 1 {
|
||||
buf = append(buf, separator...)
|
||||
}
|
||||
buf = append(buf, []byte(s)...)
|
||||
}
|
||||
}
|
||||
return Command(buf)
|
||||
return Command{append(c.segments, segments...)}
|
||||
}
|
||||
|
||||
// Segments returns the ordered segments that comprise the Command as a
|
||||
// slice of strings.
|
||||
func (c Command) Segments() []string {
|
||||
return strings.Split(string(c), separator)
|
||||
return c.segments
|
||||
}
|
||||
|
||||
// String returns the composed representation the command. This is also
|
||||
// the required wire representation (before IPLD encoding occurs.)
|
||||
func (c Command) String() string {
|
||||
return string(c)
|
||||
return "/" + strings.Join(c.segments, "/")
|
||||
}
|
||||
|
||||
@@ -13,66 +13,73 @@ func TestTop(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIsValidCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("succeeds when", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, testcase := range validTestcases(t) {
|
||||
testcase := testcase
|
||||
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.True(t, command.IsValid(testcase.inp))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails when", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, testcase := range invalidTestcases(t) {
|
||||
testcase := testcase
|
||||
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.False(t, command.IsValid(testcase.inp))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
require.Equal(t, command.Top(), command.New())
|
||||
require.Equal(t, "/foo", command.New("foo").String())
|
||||
require.Equal(t, "/foo/bar", command.New("foo", "bar").String())
|
||||
require.Equal(t, "/foo/bar/baz", command.New("foo", "bar/baz").String())
|
||||
}
|
||||
|
||||
func TestParseCommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("succeeds when", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, testcase := range validTestcases(t) {
|
||||
testcase := testcase
|
||||
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd, err := command.Parse("/elem0/elem1/elem2")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, cmd)
|
||||
require.NotNil(t, cmd)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails when", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, testcase := range invalidTestcases(t) {
|
||||
testcase := testcase
|
||||
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd, err := command.Parse(testcase.inp)
|
||||
require.ErrorIs(t, err, testcase.err)
|
||||
require.Zero(t, cmd)
|
||||
require.Equal(t, command.Command{}, cmd)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEquality(t *testing.T) {
|
||||
require.True(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/baz"))
|
||||
require.False(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/bazz"))
|
||||
require.False(t, command.MustParse("/foo/bar") == command.MustParse("/foo/bar/baz"))
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
require.Equal(t, "/foo", command.Top().Join("foo").String())
|
||||
require.Equal(t, "/foo/bar", command.Top().Join("foo/bar").String())
|
||||
require.Equal(t, "/foo/bar", command.Top().Join("foo", "bar").String())
|
||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo/bar").String())
|
||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String())
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
name string
|
||||
inp string
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# Token container
|
||||
|
||||
## Why do I need that?
|
||||
|
||||
Some common situation asks to package multiple tokens together:
|
||||
- calling a service requires sending an invocation, alongside the matching delegations
|
||||
- sending a series of revocations
|
||||
- \<insert your application specific scenario here>
|
||||
|
||||
The UCAN specification defines how a single token is serialized (envelope with signature, IPLD encoded as Dag-cbor), but it's entirely left open how to package multiple tokens together. To be clear, this is a correct thing to do for a specification, as different ways equally valid to solve that problem exists and can coexist. Any wire format holding a list of bytes would do (cbor, json, csv ...).
|
||||
|
||||
**go-ucan** however, provide an opinionated implementation, which may or may not work in your situation.
|
||||
|
||||
Some experiment has been done over which format is appropriate, and two have been selected:
|
||||
- **DAG-CBOR** of a list of bytes, as a low overhead option
|
||||
- **CAR** file, as a somewhat common ways to cary arbitrary blocks of data
|
||||
|
||||
Notably, **compression is not included**, even though it does work reasonably well. This is because your transport medium might already do it, or should.
|
||||
|
||||
## Wire format consideration
|
||||
|
||||
Several possible formats have been explored:
|
||||
- CAR files (binary or base64)
|
||||
- DAG-CBOR (binary or base64)
|
||||
|
||||
Additionally, gzip and deflate compression has been experimented with.
|
||||
|
||||
Below are the results in terms of storage used, as percentage and byte overhead over the raw tokens:
|
||||
|
||||
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|
||||
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
|
||||
| 1 | 15 | 54 | 7 | 42 | 0 | 35 | \-8 | 22 | \-12 | 16 |
|
||||
| 2 | 12 | 49 | \-12 | 15 | 0 | 34 | \-25 | 0 | \-28 | \-3 |
|
||||
| 3 | 11 | 48 | \-21 | 4 | 0 | 34 | \-32 | \-10 | \-34 | \-11 |
|
||||
| 4 | 10 | 47 | \-26 | \-1 | 0 | 34 | \-36 | \-15 | \-37 | \-17 |
|
||||
| 5 | 10 | 47 | \-28 | \-4 | 0 | 34 | \-38 | \-18 | \-40 | \-20 |
|
||||
| 6 | 10 | 47 | \-30 | \-7 | 0 | 34 | \-40 | \-20 | \-40 | \-20 |
|
||||
| 7 | 10 | 46 | \-31 | \-8 | 0 | 34 | \-41 | \-21 | \-42 | \-22 |
|
||||
| 8 | 9 | 46 | \-32 | \-10 | 0 | 34 | \-42 | \-22 | \-42 | \-23 |
|
||||
| 9 | 9 | 46 | \-33 | \-11 | 0 | 34 | \-43 | \-23 | \-43 | \-24 |
|
||||
| 10 | 9 | 46 | \-34 | \-12 | 0 | 34 | \-43 | \-25 | \-44 | \-25 |
|
||||
|
||||

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

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

|
||||

|
||||

|
||||
|
||||
Conclusion:
|
||||
- CAR files are heavy for this usage, notably because they carry the CIDs of the tokens
|
||||
- compression works quite well and warrants its usage even with a single token
|
||||
- DAG-CBOR outperform CAR files everywhere, and comes with a tiny ~3 bytes per token overhead.
|
||||
|
||||
**Formats beside DAG-CBOR and CAR, with or without base64, have been removed. They are in the git history though.**
|
||||
@@ -1,263 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
)
|
||||
|
||||
/*
|
||||
Note: below is essentially a re-implementation of the CAR file v1 read and write.
|
||||
This exists here for two reasons:
|
||||
- go-car's API forces to go through an IPLD getter or through a blockstore API
|
||||
- generally, go-car is a very complex and large dependency
|
||||
*/
|
||||
|
||||
// EmptyCid is a "zero" Cid: zero-length "identity" multihash with "raw" codec
|
||||
// It can be used to have at least one root in a CARv1 file (making it legal), yet
|
||||
// denote that it can be ignored.
|
||||
var EmptyCid = cid.MustParse([]byte{01, 55, 00, 00})
|
||||
|
||||
type carBlock struct {
|
||||
c cid.Cid
|
||||
data []byte
|
||||
}
|
||||
|
||||
// writeCar writes a CARv1 file containing the blocks from the iterator.
|
||||
// If no roots are provided, a single EmptyCid is used as root to make the file
|
||||
// spec compliant.
|
||||
func writeCar(w io.Writer, roots []cid.Cid, blocks iter.Seq2[carBlock, error]) error {
|
||||
if len(roots) == 0 {
|
||||
roots = []cid.Cid{EmptyCid}
|
||||
}
|
||||
h := carHeader{
|
||||
Roots: roots,
|
||||
Version: 1,
|
||||
}
|
||||
hb, err := h.Write()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ldWrite(w, hb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for block, err := range blocks {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ldWrite(w, block.c.Bytes(), block.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readCar reads a CARv1 file from the reader, and return a block iterator.
|
||||
// Roots are ignored.
|
||||
func readCar(r io.Reader) (roots []cid.Cid, blocks iter.Seq2[carBlock, error], err error) {
|
||||
br := bufio.NewReader(r)
|
||||
|
||||
hb, err := ldRead(br)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
h, err := readHeader(hb)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if h.Version != 1 {
|
||||
return nil, nil, fmt.Errorf("invalid car version: %d", h.Version)
|
||||
}
|
||||
|
||||
return h.Roots, func(yield func(block carBlock, err error) bool) {
|
||||
for {
|
||||
block, err := readBlock(br)
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if !yield(carBlock{}, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !yield(block, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// readBlock reads a section from the reader and decode a (cid+data) block.
|
||||
func readBlock(r *bufio.Reader) (carBlock, error) {
|
||||
raw, err := ldRead(r)
|
||||
if err != nil {
|
||||
return carBlock{}, err
|
||||
}
|
||||
|
||||
n, c, err := cid.CidFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return carBlock{}, err
|
||||
}
|
||||
data := raw[n:]
|
||||
|
||||
// integrity check
|
||||
hashed, err := c.Prefix().Sum(data)
|
||||
if err != nil {
|
||||
return carBlock{}, err
|
||||
}
|
||||
|
||||
if !hashed.Equals(c) {
|
||||
return carBlock{}, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", c, hashed)
|
||||
}
|
||||
|
||||
return carBlock{c: c, data: data}, nil
|
||||
}
|
||||
|
||||
// maxAllowedSectionSize dictates the maximum number of bytes that a CARv1 header
|
||||
// or section is allowed to occupy without causing a decode to error.
|
||||
// This cannot be supplied as an option, only adjusted as a global. You should
|
||||
// use v2#NewReader instead since it allows for options to be passed in.
|
||||
var maxAllowedSectionSize uint = 32 << 20 // 32MiB
|
||||
|
||||
// ldRead performs a length-delimited read of a section from the reader.
|
||||
// A section is composed of an uint length followed by the data.
|
||||
func ldRead(r *bufio.Reader) ([]byte, error) {
|
||||
if _, err := r.Peek(1); err != nil { // no more blocks, likely clean io.EOF
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := binary.ReadUvarint(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, io.ErrUnexpectedEOF // don't silently pretend this is a clean EOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if l == 0 {
|
||||
return nil, fmt.Errorf("invalid zero size section")
|
||||
}
|
||||
|
||||
if l > uint64(maxAllowedSectionSize) { // Don't OOM
|
||||
return nil, fmt.Errorf("malformed car; header is bigger than MaxAllowedSectionSize")
|
||||
}
|
||||
|
||||
buf := make([]byte, l)
|
||||
if _, err := io.ReadFull(r, buf); err != nil {
|
||||
if err == io.EOF {
|
||||
// we should be able to read the promised bytes, this is not normal
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// ldWrite performs a length-delimited write of a section on the writer.
|
||||
// A section is composed of an uint length followed by the data.
|
||||
func ldWrite(w io.Writer, d ...[]byte) error {
|
||||
var sum uint64
|
||||
for _, s := range d {
|
||||
sum += uint64(len(s))
|
||||
}
|
||||
|
||||
buf := make([]byte, 8)
|
||||
n := binary.PutUvarint(buf, sum)
|
||||
_, err := w.Write(buf[:n])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range d {
|
||||
_, err = w.Write(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type carHeader struct {
|
||||
Roots []cid.Cid
|
||||
Version uint64
|
||||
}
|
||||
|
||||
const rootsKey = "roots"
|
||||
const versionKey = "version"
|
||||
|
||||
func readHeader(data []byte) (*carHeader, error) {
|
||||
var header carHeader
|
||||
|
||||
nd, err := ipld.Decode(data, dagcbor.Decode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nd.Length() != 2 {
|
||||
return nil, fmt.Errorf("malformed car header")
|
||||
}
|
||||
rootsNd, err := nd.LookupByString(rootsKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed car header")
|
||||
}
|
||||
it := rootsNd.ListIterator()
|
||||
if it == nil {
|
||||
return nil, fmt.Errorf("malformed car header")
|
||||
}
|
||||
header.Roots = make([]cid.Cid, 0, rootsNd.Length())
|
||||
for !it.Done() {
|
||||
_, nd, err := it.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lk, err := nd.AsLink()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch lk := lk.(type) {
|
||||
case cidlink.Link:
|
||||
header.Roots = append(header.Roots, lk.Cid)
|
||||
default:
|
||||
return nil, fmt.Errorf("malformed car header")
|
||||
}
|
||||
}
|
||||
versionNd, err := nd.LookupByString(versionKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed car header")
|
||||
}
|
||||
version, err := versionNd.AsInt()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed car header")
|
||||
}
|
||||
header.Version = uint64(version)
|
||||
return &header, nil
|
||||
}
|
||||
|
||||
func (ch *carHeader) Write() ([]byte, error) {
|
||||
nd, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, rootsKey, qp.List(int64(len(ch.Roots)), func(la datamodel.ListAssembler) {
|
||||
for _, root := range ch.Roots {
|
||||
qp.ListEntry(la, qp.Link(cidlink.Link{Cid: root}))
|
||||
}
|
||||
}))
|
||||
qp.MapEntry(ma, versionKey, qp.Int(1))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipld.Encode(nd, dagcbor.Encode)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCarRoundTrip(t *testing.T) {
|
||||
// this car file is a complex and legal CARv1 file
|
||||
original, err := os.ReadFile("testdata/sample-v1.car")
|
||||
require.NoError(t, err)
|
||||
|
||||
roots, it, err := readCar(bytes.NewReader(original))
|
||||
require.NoError(t, err)
|
||||
|
||||
var blks []carBlock
|
||||
for blk, err := range it {
|
||||
require.NoError(t, err)
|
||||
blks = append(blks, blk)
|
||||
}
|
||||
|
||||
require.Len(t, blks, 1049)
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
err = writeCar(buf, roots, func(yield func(carBlock, error) bool) {
|
||||
for _, blk := range blks {
|
||||
if !yield(blk, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bytes equal after the round-trip
|
||||
require.Equal(t, original, buf.Bytes())
|
||||
}
|
||||
|
||||
func FuzzCarRoundTrip(f *testing.F) {
|
||||
example, err := os.ReadFile("testdata/sample-v1.car")
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(example)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
roots, blocksIter, err := readCar(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
// skip invalid binary
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
// reading all the blocks, which force reading and verifying the full file
|
||||
var blocks []carBlock
|
||||
for block, err := range blocksIter {
|
||||
if err != nil {
|
||||
// error reading, invalid data
|
||||
t.Skip()
|
||||
}
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = writeCar(&buf, roots, func(yield func(carBlock, error) bool) {
|
||||
for _, blk := range blocks {
|
||||
if !yield(blk, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// test if the round-trip produce a byte-equal CAR
|
||||
require.Equal(t, data, buf.Bytes())
|
||||
})
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB |
@@ -1,122 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||
)
|
||||
|
||||
var ErrNotFound = fmt.Errorf("not found")
|
||||
|
||||
// Reader is a token container reader. It exposes the tokens conveniently decoded.
|
||||
type Reader map[cid.Cid]token.Token
|
||||
|
||||
// GetToken returns an arbitrary decoded token, from its CID.
|
||||
// If not found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
|
||||
tkn, ok := ctn[cid]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
||||
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||
tkn, err := ctn.GetToken(cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
||||
return tkn, nil
|
||||
}
|
||||
return nil, fmt.Errorf("not a delegation token")
|
||||
}
|
||||
|
||||
// GetInvocation returns the first found invocation.Token.
|
||||
// If none are found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||
for _, t := range ctn {
|
||||
if inv, ok := t.(*invocation.Token); ok {
|
||||
return inv, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func FromCar(r io.Reader) (Reader, error) {
|
||||
_, it, err := readCar(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctn := make(Reader)
|
||||
|
||||
for block, err := range it {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ctn.addToken(block.data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCarBase64(r io.Reader) (Reader, error) {
|
||||
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
|
||||
}
|
||||
|
||||
func FromCbor(r io.Reader) (Reader, error) {
|
||||
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n.Kind() != datamodel.Kind_List {
|
||||
return nil, fmt.Errorf("not a list")
|
||||
}
|
||||
|
||||
ctn := make(Reader, n.Length())
|
||||
|
||||
it := n.ListIterator()
|
||||
for !it.Done() {
|
||||
_, val, err := it.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := val.AsBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ctn.addToken(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCborBase64(r io.Reader) (Reader, error) {
|
||||
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
|
||||
}
|
||||
|
||||
func (ctn Reader) addToken(data []byte) error {
|
||||
tkn, c, err := token.FromSealed(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctn[c] = tkn
|
||||
return nil
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
)
|
||||
|
||||
func TestContainerRoundTrip(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
writer func(ctn Writer, w io.Writer) error
|
||||
reader func(io.Reader) (Reader, error)
|
||||
}{
|
||||
{"car", Writer.ToCar, FromCar},
|
||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
||||
{"cbor", Writer.ToCbor, FromCbor},
|
||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tokens := make(map[cid.Cid]*delegation.Token)
|
||||
var dataSize int
|
||||
|
||||
writer := NewWriter()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
dlg, c, data := randToken()
|
||||
writer.AddSealed(c, data)
|
||||
tokens[c] = dlg
|
||||
dataSize += len(data)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
err := tc.writer(writer, buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("data size %d", dataSize)
|
||||
t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize)
|
||||
|
||||
reader, err := tc.reader(bytes.NewReader(buf.Bytes()))
|
||||
require.NoError(t, err)
|
||||
|
||||
for c, dlg := range tokens {
|
||||
tknRead, err := reader.GetToken(c)
|
||||
require.NoError(t, err)
|
||||
|
||||
// require.Equal fails as time.Time holds a wall time that is going to be
|
||||
// different, even if it represents the same event.
|
||||
// We need to do the following instead.
|
||||
|
||||
dlgRead := tknRead.(*delegation.Token)
|
||||
require.Equal(t, dlg.Issuer(), dlgRead.Issuer())
|
||||
require.Equal(t, dlg.Audience(), dlgRead.Audience())
|
||||
require.Equal(t, dlg.Subject(), dlgRead.Subject())
|
||||
require.Equal(t, dlg.Command(), dlgRead.Command())
|
||||
require.Equal(t, dlg.Policy(), dlgRead.Policy())
|
||||
require.Equal(t, dlg.Nonce(), dlgRead.Nonce())
|
||||
require.True(t, dlg.Meta().Equals(dlgRead.Meta()))
|
||||
if dlg.NotBefore() != nil {
|
||||
// within 1s as the original value gets truncated to seconds when serialized
|
||||
require.WithinDuration(t, *dlg.NotBefore(), *dlgRead.NotBefore(), time.Second)
|
||||
}
|
||||
if dlg.Expiration() != nil {
|
||||
// within 1s as the original value gets truncated to seconds when serialized
|
||||
require.WithinDuration(t, *dlg.Expiration(), *dlgRead.Expiration(), time.Second)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContainerSerialisation(b *testing.B) {
|
||||
var duration strings.Builder
|
||||
var allocByte strings.Builder
|
||||
var allocCount strings.Builder
|
||||
|
||||
for _, builder := range []strings.Builder{duration, allocByte, allocCount} {
|
||||
builder.WriteString("car\tcarBase64\tcarGzip\tcarGzipBase64\tcbor\tcborBase64\tcborGzip\tcborGzipBase64\tcborFlate\tcborFlateBase64\n")
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
writer func(ctn Writer, w io.Writer) error
|
||||
reader func(io.Reader) (Reader, error)
|
||||
}{
|
||||
{"car", Writer.ToCar, FromCar},
|
||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
||||
{"cbor", Writer.ToCbor, FromCbor},
|
||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
||||
} {
|
||||
writer := NewWriter()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
_, c, data := randToken()
|
||||
writer.AddSealed(c, data)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = tc.writer(writer, buf)
|
||||
|
||||
b.Run(tc.name+"_write", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
_ = tc.writer(writer, buf)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run(tc.name+"_read", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = tc.reader(bytes.NewReader(buf.Bytes()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func randBytes(n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
return b
|
||||
}
|
||||
|
||||
func randDID() (crypto.PrivKey, did.DID) {
|
||||
privKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
d, err := did.FromPrivKey(privKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return privKey, d
|
||||
}
|
||||
|
||||
func randomString(length int) string {
|
||||
b := make([]byte, length/2+1)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)[0:length]
|
||||
}
|
||||
|
||||
func randToken() (*delegation.Token, cid.Cid, []byte) {
|
||||
priv, iss := randDID()
|
||||
_, aud := randDID()
|
||||
cmd := command.New("foo", "bar")
|
||||
pol := policy.MustConstruct(
|
||||
policy.All(".[]",
|
||||
policy.GreaterThan(".value", literal.Int(2)),
|
||||
),
|
||||
)
|
||||
|
||||
opts := []delegation.Option{
|
||||
delegation.WithExpiration(time.Now().Add(time.Hour)),
|
||||
delegation.WithSubject(iss),
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
||||
}
|
||||
|
||||
t, err := delegation.New(priv, aud, cmd, pol, opts...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
b, c, err := t.ToSealed(priv)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t, c, b
|
||||
}
|
||||
BIN
pkg/container/testdata/sample-v1.car
vendored
BIN
pkg/container/testdata/sample-v1.car
vendored
Binary file not shown.
@@ -1,61 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
)
|
||||
|
||||
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
|
||||
|
||||
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
||||
type Writer map[cid.Cid][]byte
|
||||
|
||||
func NewWriter() Writer {
|
||||
return make(Writer)
|
||||
}
|
||||
|
||||
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
|
||||
func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
|
||||
ctn[cid] = data
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCar(w io.Writer) error {
|
||||
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
|
||||
for c, bytes := range ctn {
|
||||
if !yield(carBlock{c: c, data: bytes}, nil) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCarBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCar(w2)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCbor(w io.Writer) error {
|
||||
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
|
||||
for _, bytes := range ctn {
|
||||
qp.ListEntry(la, qp.Bytes(bytes))
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCborBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCbor(w2)
|
||||
}
|
||||
@@ -4,12 +4,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
)
|
||||
|
||||
var ErrUnsupported = errors.New("failure adding unsupported type to meta")
|
||||
@@ -125,41 +123,6 @@ func (m *Meta) Add(key string, val any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Equals tells if two Meta hold the same key/values.
|
||||
func (m *Meta) Equals(other *Meta) bool {
|
||||
if len(m.Keys) != len(other.Keys) {
|
||||
return false
|
||||
}
|
||||
if len(m.Values) != len(other.Values) {
|
||||
return false
|
||||
}
|
||||
for _, key := range m.Keys {
|
||||
if !ipld.DeepEqual(m.Values[key], other.Values[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Meta) String() string {
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("{")
|
||||
|
||||
var i int
|
||||
for key, node := range m.Values {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
i++
|
||||
buf.WriteString(key)
|
||||
buf.WriteString(":")
|
||||
buf.WriteString(printer.Sprint(node))
|
||||
}
|
||||
|
||||
buf.WriteString("}")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func fqtn(val any) string {
|
||||
var name string
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return negation{statement: statement}, nil
|
||||
return Not(statement), nil
|
||||
|
||||
case KindAnd, KindOr:
|
||||
arg2, _ := node.LookupByIndex(1)
|
||||
@@ -93,11 +93,11 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
||||
if pattern.Kind() != datamodel.Kind_String {
|
||||
return nil, ErrNotAString(combinePath(path, op, 2))
|
||||
}
|
||||
g, err := parseGlob(must.String(pattern))
|
||||
res, err := Like(sel, must.String(pattern))
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
||||
}
|
||||
return wildcard{selector: sel, pattern: g}, nil
|
||||
return res, nil
|
||||
|
||||
case KindAll, KindAny:
|
||||
sel, err := arg2AsSelector(op)
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
// Package literal holds a collection of functions to create IPLD types to use in policies, selector and args.
|
||||
package literal
|
||||
|
||||
import (
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
)
|
||||
|
||||
func Node(n ipld.Node) ipld.Node {
|
||||
return n
|
||||
}
|
||||
|
||||
func Link(cid ipld.Link) ipld.Node {
|
||||
nb := basicnode.Prototype.Link.NewBuilder()
|
||||
nb.AssignLink(cid)
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
func Bool(val bool) ipld.Node {
|
||||
nb := basicnode.Prototype.Bool.NewBuilder()
|
||||
nb.AssignBool(val)
|
||||
@@ -38,16 +45,6 @@ func Bytes(val []byte) ipld.Node {
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
func Link(link ipld.Link) ipld.Node {
|
||||
nb := basicnode.Prototype.Link.NewBuilder()
|
||||
nb.AssignLink(link)
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
func LinkCid(cid cid.Cid) ipld.Node {
|
||||
return Link(cidlink.Link{Cid: cid})
|
||||
}
|
||||
|
||||
func Null() ipld.Node {
|
||||
nb := basicnode.Prototype.Any.NewBuilder()
|
||||
nb.AssignNull()
|
||||
|
||||
@@ -7,9 +7,28 @@ import (
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
// Match determines if the IPLD node satisfies the policy.
|
||||
func (p Policy) Filter(sel selector.Selector) Policy {
|
||||
return p.FilterWithMatcher(sel, selector.SegmentEquals)
|
||||
}
|
||||
|
||||
// FilterWithMatcher extracts a subset of the policy related to the specified selector,
|
||||
// by matching each segment using the given selector.SegmentMatcher.
|
||||
func (p Policy) FilterWithMatcher(sel selector.Selector, matcher selector.SegmentMatcher) Policy {
|
||||
var filtered Policy
|
||||
for _, stmt := range p {
|
||||
if stmt.Selector().Matches(sel, matcher) {
|
||||
filtered = append(filtered, stmt)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// Match determines if the IPLD node matches the policy document.
|
||||
func (p Policy) Match(node datamodel.Node) bool {
|
||||
for _, stmt := range p {
|
||||
ok := matchStatement(stmt, node)
|
||||
@@ -17,40 +36,34 @@ func (p Policy) Match(node datamodel.Node) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path
|
||||
func (p Policy) Filter(path ...string) Policy {
|
||||
var filtered Policy
|
||||
|
||||
for _, stmt := range p {
|
||||
newChild, remain := filter(stmt, path)
|
||||
if newChild != nil && len(remain) == 0 {
|
||||
filtered = append(filtered, newChild)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
switch statement.Kind() {
|
||||
case KindEqual:
|
||||
if s, ok := statement.(equality); ok {
|
||||
one, _, err := s.selector.Select(node)
|
||||
one, many, err := selector.Select(s.selector, node)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if one != nil {
|
||||
return datamodel.DeepEqual(s.value, one)
|
||||
}
|
||||
if many != nil {
|
||||
for _, n := range many {
|
||||
if eq := datamodel.DeepEqual(s.value, n); eq {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
case KindGreaterThan:
|
||||
if s, ok := statement.(equality); ok {
|
||||
one, _, err := s.selector.Select(node)
|
||||
one, _, err := selector.Select(s.selector, node)
|
||||
if err != nil || one == nil {
|
||||
return false
|
||||
}
|
||||
@@ -58,7 +71,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
}
|
||||
case KindGreaterThanOrEqual:
|
||||
if s, ok := statement.(equality); ok {
|
||||
one, _, err := s.selector.Select(node)
|
||||
one, _, err := selector.Select(s.selector, node)
|
||||
if err != nil || one == nil {
|
||||
return false
|
||||
}
|
||||
@@ -66,7 +79,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
}
|
||||
case KindLessThan:
|
||||
if s, ok := statement.(equality); ok {
|
||||
one, _, err := s.selector.Select(node)
|
||||
one, _, err := selector.Select(s.selector, node)
|
||||
if err != nil || one == nil {
|
||||
return false
|
||||
}
|
||||
@@ -74,7 +87,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
}
|
||||
case KindLessThanOrEqual:
|
||||
if s, ok := statement.(equality); ok {
|
||||
one, _, err := s.selector.Select(node)
|
||||
one, _, err := selector.Select(s.selector, node)
|
||||
if err != nil || one == nil {
|
||||
return false
|
||||
}
|
||||
@@ -109,7 +122,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
}
|
||||
case KindLike:
|
||||
if s, ok := statement.(wildcard); ok {
|
||||
one, _, err := s.selector.Select(node)
|
||||
one, _, err := selector.Select(s.selector, node)
|
||||
if err != nil || one == nil {
|
||||
return false
|
||||
}
|
||||
@@ -121,72 +134,31 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
}
|
||||
case KindAll:
|
||||
if s, ok := statement.(quantifier); ok {
|
||||
one, many, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
_, many, err := selector.Select(s.selector, node)
|
||||
if err != nil || many == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if one != nil {
|
||||
it := one.ListIterator()
|
||||
if it != nil {
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ok := matchStatement(s.statement, v)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok := matchStatement(s.statement, one)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, n := range many {
|
||||
ok := matchStatement(s.statement, n)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(many) > 0 {
|
||||
for _, n := range many {
|
||||
ok := matchStatement(s.statement, n)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
case KindAny:
|
||||
if s, ok := statement.(quantifier); ok {
|
||||
one, many, err := s.selector.Select(node)
|
||||
one, many, err := selector.Select(s.selector, node)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if one != nil {
|
||||
it := one.ListIterator()
|
||||
if it != nil {
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ok := matchStatement(s.statement, v)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ok := matchStatement(s.statement, one)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
ok := matchStatement(s.statement, one)
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if len(many) > 0 {
|
||||
if many != nil {
|
||||
for _, n := range many {
|
||||
ok := matchStatement(s.statement, n)
|
||||
if ok {
|
||||
@@ -201,70 +173,6 @@ func matchStatement(statement Statement, node ipld.Node) bool {
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
|
||||
}
|
||||
|
||||
// filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path
|
||||
func filter(stmt Statement, path []string) (Statement, []string) {
|
||||
// For each kind, we do some of the following if it applies:
|
||||
// - test the path against the selector, consuming segments
|
||||
// - for terminal statements (equality, wildcard), require all the segments to have been consumed
|
||||
// - recursively filter child (negation, quantifier) or children (connective) statements with the remaining path
|
||||
switch stmt.(type) {
|
||||
case equality:
|
||||
match, remain := stmt.(equality).selector.MatchPath(path...)
|
||||
if match && len(remain) == 0 {
|
||||
return stmt, remain
|
||||
}
|
||||
return nil, nil
|
||||
case negation:
|
||||
newChild, remain := filter(stmt.(negation).statement, path)
|
||||
if newChild != nil && len(remain) == 0 {
|
||||
return negation{
|
||||
statement: newChild,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
case connective:
|
||||
var newChildren []Statement
|
||||
for _, child := range stmt.(connective).statements {
|
||||
newChild, remain := filter(child, path)
|
||||
if newChild != nil && len(remain) == 0 {
|
||||
newChildren = append(newChildren, newChild)
|
||||
}
|
||||
}
|
||||
if len(newChildren) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return connective{
|
||||
kind: stmt.(connective).kind,
|
||||
statements: newChildren,
|
||||
}, nil
|
||||
case wildcard:
|
||||
match, remain := stmt.(wildcard).selector.MatchPath(path...)
|
||||
if match && len(remain) == 0 {
|
||||
return stmt, remain
|
||||
}
|
||||
return nil, nil
|
||||
case quantifier:
|
||||
match, remain := stmt.(quantifier).selector.MatchPath(path...)
|
||||
if match && len(remain) == 0 {
|
||||
return stmt, remain
|
||||
}
|
||||
if !match {
|
||||
return nil, nil
|
||||
}
|
||||
newChild, remain := filter(stmt.(quantifier).statement, remain)
|
||||
if newChild != nil && len(remain) == 0 {
|
||||
return quantifier{
|
||||
kind: stmt.(quantifier).kind,
|
||||
selector: stmt.(quantifier).selector,
|
||||
statement: newChild,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
default:
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", stmt.Kind()))
|
||||
}
|
||||
}
|
||||
|
||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||
a := must.Int(actual)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
@@ -10,9 +11,11 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
@@ -23,15 +26,15 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignString("test")
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.String("test")))
|
||||
pol := Policy{Equal(selector.MustParse("."), literal.String("test"))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("test2")))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Int(138)))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -42,15 +45,15 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Int(138)))
|
||||
pol := Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -61,15 +64,15 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignFloat(1.138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Float(1.138)))
|
||||
pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -83,15 +86,15 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignLink(l0)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
||||
pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
||||
pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -105,19 +108,19 @@ func TestMatch(t *testing.T) {
|
||||
ma.Finish()
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
||||
pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
||||
pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))}
|
||||
ok = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
||||
pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
||||
pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -130,11 +133,11 @@ func TestMatch(t *testing.T) {
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
||||
pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
||||
pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -147,7 +150,7 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
||||
pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -158,11 +161,11 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
||||
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
||||
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
||||
ok = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -173,7 +176,7 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignFloat(1.38)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
||||
pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -184,11 +187,11 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignFloat(1.38)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
||||
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
||||
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))}
|
||||
ok = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -199,7 +202,7 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
||||
pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -210,11 +213,11 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
||||
pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
||||
pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
||||
ok = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -226,11 +229,11 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignBool(false)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
||||
pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
||||
pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -241,25 +244,25 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(
|
||||
pol := Policy{
|
||||
And(
|
||||
GreaterThan(".", literal.Int(1)),
|
||||
LessThan(".", literal.Int(1138)),
|
||||
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
||||
LessThan(selector.MustParse("."), literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(
|
||||
pol = Policy{
|
||||
And(
|
||||
GreaterThan(".", literal.Int(1)),
|
||||
Equal(".", literal.Int(1138)),
|
||||
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
||||
Equal(selector.MustParse("."), literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(And())
|
||||
pol = Policy{And()}
|
||||
ok = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -270,25 +273,25 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(
|
||||
pol := Policy{
|
||||
Or(
|
||||
GreaterThan(".", literal.Int(138)),
|
||||
LessThan(".", literal.Int(1138)),
|
||||
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
||||
LessThan(selector.MustParse("."), literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(
|
||||
pol = Policy{
|
||||
Or(
|
||||
GreaterThan(".", literal.Int(138)),
|
||||
Equal(".", literal.Int(1138)),
|
||||
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
||||
Equal(selector.MustParse("."), literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
|
||||
pol = MustConstruct(Or())
|
||||
pol = Policy{Or()}
|
||||
ok = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -309,7 +312,10 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Like(".", pattern))
|
||||
statement, err := Like(selector.MustParse("."), pattern)
|
||||
require.NoError(t, err)
|
||||
|
||||
pol := Policy{statement}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
})
|
||||
@@ -330,7 +336,10 @@ func TestMatch(t *testing.T) {
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Like(".", pattern))
|
||||
statement, err := Like(selector.MustParse("."), pattern)
|
||||
require.NoError(t, err)
|
||||
|
||||
pol := Policy{statement}
|
||||
ok := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -361,11 +370,21 @@ func TestMatch(t *testing.T) {
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
||||
pol := Policy{
|
||||
All(
|
||||
selector.MustParse(".[]"),
|
||||
GreaterThan(selector.MustParse(".value"), literal.Int(2)),
|
||||
),
|
||||
}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
||||
pol = Policy{
|
||||
All(
|
||||
selector.MustParse(".[]"),
|
||||
GreaterThan(selector.MustParse(".value"), literal.Int(20)),
|
||||
),
|
||||
}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -382,11 +401,21 @@ func TestMatch(t *testing.T) {
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
|
||||
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
||||
pol := Policy{
|
||||
Any(
|
||||
selector.MustParse(".[]"),
|
||||
GreaterThan(selector.MustParse(".value"), literal.Int(60)),
|
||||
),
|
||||
}
|
||||
ok := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
|
||||
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
||||
pol = Policy{
|
||||
Any(
|
||||
selector.MustParse(".[]"),
|
||||
GreaterThan(selector.MustParse(".value"), literal.Int(100)),
|
||||
),
|
||||
}
|
||||
ok = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
})
|
||||
@@ -515,101 +544,99 @@ func FuzzMatch(f *testing.F) {
|
||||
}
|
||||
|
||||
func TestPolicyFilter(t *testing.T) {
|
||||
pol := MustConstruct(
|
||||
Any(".http", And(
|
||||
Equal(".method", literal.String("GET")),
|
||||
Equal(".path", literal.String("/foo")),
|
||||
)),
|
||||
Equal(".http", literal.String("foobar")),
|
||||
All(".jsonrpc.foo", Or(
|
||||
Not(Equal(".bar", literal.String("foo"))),
|
||||
Equal(".", literal.String("foo")),
|
||||
Like(".boo", "abcd"),
|
||||
Like(".boo", "*bcd"),
|
||||
)),
|
||||
)
|
||||
sel1 := selector.Selector{selector.NewFieldSegment("http")}
|
||||
sel2 := selector.Selector{selector.NewFieldSegment("jsonrpc")}
|
||||
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
expected Policy
|
||||
}{
|
||||
{
|
||||
path: "http",
|
||||
expected: MustConstruct(
|
||||
Any(".http", And(
|
||||
Equal(".method", literal.String("GET")),
|
||||
Equal(".path", literal.String("/foo")),
|
||||
)),
|
||||
Equal(".http", literal.String("foobar")),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "http,method",
|
||||
expected: MustConstruct(
|
||||
Any(".http", And(
|
||||
Equal(".method", literal.String("GET")),
|
||||
)),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "http,path",
|
||||
expected: MustConstruct(
|
||||
Any(".http", And(
|
||||
Equal(".path", literal.String("/foo")),
|
||||
)),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "http,foo",
|
||||
expected: Policy{},
|
||||
},
|
||||
{
|
||||
path: "jsonrpc",
|
||||
expected: MustConstruct(
|
||||
All(".jsonrpc.foo", Or(
|
||||
Not(Equal(".bar", literal.String("foo"))),
|
||||
Equal(".", literal.String("foo")),
|
||||
Like(".boo", "abcd"),
|
||||
Like(".boo", "*bcd"),
|
||||
)),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "jsonrpc,baz",
|
||||
expected: Policy{},
|
||||
},
|
||||
{
|
||||
path: "jsonrpc,foo",
|
||||
expected: MustConstruct(
|
||||
All(".jsonrpc.foo", Or(
|
||||
Not(Equal(".bar", literal.String("foo"))),
|
||||
Equal(".", literal.String("foo")),
|
||||
Like(".boo", "abcd"),
|
||||
Like(".boo", "*bcd"),
|
||||
)),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "jsonrpc,foo,bar",
|
||||
expected: MustConstruct(
|
||||
All(".jsonrpc.foo", Or(
|
||||
Not(Equal(".bar", literal.String("foo"))),
|
||||
)),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "jsonrpc,foo,boo",
|
||||
expected: MustConstruct(
|
||||
All(".jsonrpc.foo", Or(
|
||||
Like(".boo", "abcd"),
|
||||
Like(".boo", "*bcd"),
|
||||
)),
|
||||
),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.path, func(t *testing.T) {
|
||||
res := pol.Filter(strings.Split(tc.path, ",")...)
|
||||
require.Equal(t, tc.expected.String(), res.String())
|
||||
})
|
||||
}
|
||||
stmt1 := Equal(sel1, basicnode.NewString("value1"))
|
||||
stmt2 := Equal(sel2, basicnode.NewString("value2"))
|
||||
|
||||
p := Policy{stmt1, stmt2}
|
||||
|
||||
filtered := p.Filter(sel1)
|
||||
assert.Len(t, filtered, 1)
|
||||
assert.Equal(t, stmt1, filtered[0])
|
||||
|
||||
filtered = p.Filter(sel2)
|
||||
assert.Len(t, filtered, 1)
|
||||
assert.Equal(t, stmt2, filtered[0])
|
||||
|
||||
sel3 := selector.Selector{selector.NewFieldSegment("nonexistent")}
|
||||
filtered = p.Filter(sel3)
|
||||
assert.Len(t, filtered, 0)
|
||||
|
||||
stmt1 = Equal(
|
||||
selector.Selector{selector.NewFieldSegment(".http.host")},
|
||||
basicnode.NewString("mainnet.infura.io"),
|
||||
)
|
||||
stmt2, err := Like(
|
||||
selector.Selector{selector.NewFieldSegment(".jsonrpc.method")},
|
||||
"eth_*",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
p = Policy{stmt1, stmt2}
|
||||
filtered = p.FilterWithMatcher(selector.Selector{selector.NewFieldSegment(".http")}, selector.SegmentStartsWith)
|
||||
assert.Len(t, filtered, 1)
|
||||
assert.Equal(t, stmt1, filtered[0])
|
||||
}
|
||||
|
||||
func FuzzPolicyFilter(f *testing.F) {
|
||||
f.Add([]byte(`{"selector": [{"field": "http"}], "value": "value1"}`))
|
||||
f.Add([]byte(`{"selector": [{"field": "jsonrpc"}], "value": "value2"}`))
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
var input struct {
|
||||
Selector []struct {
|
||||
Field string `json:"field"` // because selector.segment is not public
|
||||
} `json:"selector"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &input); err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
var sel selector.Selector
|
||||
for _, seg := range input.Selector {
|
||||
sel = append(sel, selector.NewFieldSegment(seg.Field))
|
||||
}
|
||||
stmt := Equal(sel, basicnode.NewString(input.Value))
|
||||
|
||||
// create a policy and filter it based on the fuzzy input selector
|
||||
p := Policy{stmt}
|
||||
filtered := p.Filter(sel)
|
||||
|
||||
// verify that the filtered policy contains the statement
|
||||
if len(filtered) != 1 || !reflect.DeepEqual(filtered[0], stmt) {
|
||||
t.Errorf("filtered policy does not contain the expected statement")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkPolicyFilter(b *testing.B) {
|
||||
sel1 := selector.Selector{selector.NewFieldSegment("http")}
|
||||
sel2 := selector.Selector{selector.NewFieldSegment("jsonrpc")}
|
||||
|
||||
stmt1 := Equal(sel1, basicnode.NewString("value1"))
|
||||
stmt2 := Equal(sel2, basicnode.NewString("value2"))
|
||||
|
||||
p := Policy{stmt1, stmt2}
|
||||
|
||||
b.Run("Filter by sel1", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.Filter(sel1)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Filter by sel2", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.Filter(sel2)
|
||||
}
|
||||
})
|
||||
|
||||
sel3 := selector.Selector{selector.NewFieldSegment("nonexistent")}
|
||||
b.Run("Filter by sel3", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
p.Filter(sel3)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@ package policy
|
||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
|
||||
selpkg "github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,43 +24,14 @@ const (
|
||||
|
||||
type Policy []Statement
|
||||
|
||||
type Constructor func() (Statement, error)
|
||||
|
||||
func Construct(cstors ...Constructor) (Policy, error) {
|
||||
stmts, err := assemble(cstors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stmts, nil
|
||||
}
|
||||
|
||||
func MustConstruct(cstors ...Constructor) Policy {
|
||||
pol, err := Construct(cstors...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pol
|
||||
}
|
||||
|
||||
func (p Policy) String() string {
|
||||
if len(p) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
childs := make([]string, len(p))
|
||||
for i, statement := range p {
|
||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
||||
}
|
||||
return fmt.Sprintf("[\n %s\n]", strings.Join(childs, ",\n "))
|
||||
}
|
||||
|
||||
type Statement interface {
|
||||
Kind() string
|
||||
String() string
|
||||
Selector() selector.Selector
|
||||
}
|
||||
|
||||
type equality struct {
|
||||
kind string
|
||||
selector selpkg.Selector
|
||||
selector selector.Selector
|
||||
value ipld.Node
|
||||
}
|
||||
|
||||
@@ -72,47 +39,28 @@ func (e equality) Kind() string {
|
||||
return e.kind
|
||||
}
|
||||
|
||||
func (e equality) String() string {
|
||||
child, err := ipld.Encode(e.value, dagjson.Encode)
|
||||
if err != nil {
|
||||
return "ERROR: INVALID VALUE"
|
||||
}
|
||||
return fmt.Sprintf(`["%s", "%s", %s]`, e.kind, e.selector, strings.ReplaceAll(string(child), "\n", "\n "))
|
||||
func (e equality) Selector() selector.Selector {
|
||||
return e.selector
|
||||
}
|
||||
|
||||
func Equal(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return equality{kind: KindEqual, selector: sel, value: value}, err
|
||||
}
|
||||
func Equal(selector selector.Selector, value ipld.Node) Statement {
|
||||
return equality{kind: KindEqual, selector: selector, value: value}
|
||||
}
|
||||
|
||||
func GreaterThan(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return equality{kind: KindGreaterThan, selector: sel, value: value}, err
|
||||
}
|
||||
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
||||
return equality{kind: KindGreaterThan, selector: selector, value: value}
|
||||
}
|
||||
|
||||
func GreaterThanOrEqual(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return equality{kind: KindGreaterThanOrEqual, selector: sel, value: value}, err
|
||||
}
|
||||
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||
return equality{kind: KindGreaterThanOrEqual, selector: selector, value: value}
|
||||
}
|
||||
|
||||
func LessThan(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return equality{kind: KindLessThan, selector: sel, value: value}, err
|
||||
}
|
||||
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
||||
return equality{kind: KindLessThan, selector: selector, value: value}
|
||||
}
|
||||
|
||||
func LessThanOrEqual(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return equality{kind: KindLessThanOrEqual, selector: sel, value: value}, err
|
||||
}
|
||||
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
||||
return equality{kind: KindLessThanOrEqual, selector: selector, value: value}
|
||||
}
|
||||
|
||||
type negation struct {
|
||||
@@ -123,16 +71,12 @@ func (n negation) Kind() string {
|
||||
return KindNot
|
||||
}
|
||||
|
||||
func (n negation) String() string {
|
||||
child := n.statement.String()
|
||||
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
|
||||
func (n negation) Selector() selector.Selector {
|
||||
return n.statement.Selector()
|
||||
}
|
||||
|
||||
func Not(cstor Constructor) Constructor {
|
||||
return func() (Statement, error) {
|
||||
stmt, err := cstor()
|
||||
return negation{statement: stmt}, err
|
||||
}
|
||||
func Not(stmt Statement) Statement {
|
||||
return negation{statement: stmt}
|
||||
}
|
||||
|
||||
type connective struct {
|
||||
@@ -144,36 +88,25 @@ func (c connective) Kind() string {
|
||||
return c.kind
|
||||
}
|
||||
|
||||
func (c connective) String() string {
|
||||
childs := make([]string, len(c.statements))
|
||||
for i, statement := range c.statements {
|
||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
||||
func (c connective) Selector() selector.Selector {
|
||||
// assuming the first statement's selector is representative
|
||||
if len(c.statements) > 0 {
|
||||
return c.statements[0].Selector()
|
||||
}
|
||||
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
|
||||
|
||||
return selector.Selector{}
|
||||
}
|
||||
|
||||
func And(cstors ...Constructor) Constructor {
|
||||
return func() (Statement, error) {
|
||||
stmts, err := assemble(cstors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connective{kind: KindAnd, statements: stmts}, nil
|
||||
}
|
||||
func And(stmts ...Statement) Statement {
|
||||
return connective{kind: KindAnd, statements: stmts}
|
||||
}
|
||||
|
||||
func Or(cstors ...Constructor) Constructor {
|
||||
return func() (Statement, error) {
|
||||
stmts, err := assemble(cstors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return connective{kind: KindOr, statements: stmts}, nil
|
||||
}
|
||||
func Or(stmts ...Statement) Statement {
|
||||
return connective{kind: KindOr, statements: stmts}
|
||||
}
|
||||
|
||||
type wildcard struct {
|
||||
selector selpkg.Selector
|
||||
selector selector.Selector
|
||||
pattern glob
|
||||
}
|
||||
|
||||
@@ -181,24 +114,22 @@ func (n wildcard) Kind() string {
|
||||
return KindLike
|
||||
}
|
||||
|
||||
func (n wildcard) String() string {
|
||||
return fmt.Sprintf(`["%s", "%s", "%s"]`, n.Kind(), n.selector, n.pattern)
|
||||
func (n wildcard) Selector() selector.Selector {
|
||||
return n.selector
|
||||
}
|
||||
|
||||
func Like(selector string, pattern string) Constructor {
|
||||
return func() (Statement, error) {
|
||||
g, err := parseGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return wildcard{selector: sel, pattern: g}, err
|
||||
func Like(selector selector.Selector, pattern string) (Statement, error) {
|
||||
g, err := parseGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wildcard{selector: selector, pattern: g}, nil
|
||||
}
|
||||
|
||||
type quantifier struct {
|
||||
kind string
|
||||
selector selpkg.Selector
|
||||
selector selector.Selector
|
||||
statement Statement
|
||||
}
|
||||
|
||||
@@ -206,41 +137,14 @@ func (n quantifier) Kind() string {
|
||||
return n.kind
|
||||
}
|
||||
|
||||
func (n quantifier) String() string {
|
||||
child := n.statement.String()
|
||||
return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
|
||||
func (n quantifier) Selector() selector.Selector {
|
||||
return n.selector
|
||||
}
|
||||
|
||||
func All(selector string, cstor Constructor) Constructor {
|
||||
return func() (Statement, error) {
|
||||
stmt, err := cstor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return quantifier{kind: KindAll, selector: sel, statement: stmt}, err
|
||||
}
|
||||
func All(selector selector.Selector, statement Statement) Statement {
|
||||
return quantifier{kind: KindAll, selector: selector, statement: statement}
|
||||
}
|
||||
|
||||
func Any(selector string, cstor Constructor) Constructor {
|
||||
return func() (Statement, error) {
|
||||
stmt, err := cstor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return quantifier{kind: KindAny, selector: sel, statement: stmt}, err
|
||||
}
|
||||
}
|
||||
|
||||
func assemble(cstors []Constructor) ([]Statement, error) {
|
||||
stmts := make([]Statement, 0, len(cstors))
|
||||
for _, cstor := range cstors {
|
||||
stmt, err := cstor()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmts = append(stmts, stmt)
|
||||
}
|
||||
return stmts, nil
|
||||
func Any(selector selector.Selector, statement Statement) Statement {
|
||||
return quantifier{kind: KindAny, selector: selector, statement: statement}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package policy_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
func ExamplePolicy() {
|
||||
pol := policy.MustConstruct(
|
||||
policy.Equal(".status", literal.String("draft")),
|
||||
policy.All(".reviewer",
|
||||
policy.Like(".email", "*@example.com"),
|
||||
),
|
||||
policy.Any(".tags", policy.Or(
|
||||
policy.Equal(".", literal.String("news")),
|
||||
policy.Equal(".", literal.String("press")),
|
||||
)),
|
||||
)
|
||||
|
||||
fmt.Println(pol)
|
||||
|
||||
// Output:
|
||||
// [
|
||||
// ["==", ".status", "draft"],
|
||||
// ["all", ".reviewer",
|
||||
// ["like", ".email", "*@example.com"]],
|
||||
// ["any", ".tags",
|
||||
// ["or", [
|
||||
// ["==", ".", "news"],
|
||||
// ["==", ".", "press"]]]
|
||||
// ]
|
||||
// ]
|
||||
}
|
||||
|
||||
func TestConstruct(t *testing.T) {
|
||||
pol, err := policy.Construct(
|
||||
policy.Equal(".status", literal.String("draft")),
|
||||
policy.All(".reviewer",
|
||||
policy.Like(".email", "*@example.com"),
|
||||
),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, pol)
|
||||
|
||||
// check if errors cascade correctly
|
||||
pol, err = policy.Construct(
|
||||
policy.Equal(".status", literal.String("draft")),
|
||||
policy.All(".reviewer", policy.Or(
|
||||
policy.Like(".email", "*@example.com"),
|
||||
policy.Like(".", "\\"), // invalid pattern
|
||||
)),
|
||||
)
|
||||
require.Error(t, err)
|
||||
require.Nil(t, pol)
|
||||
}
|
||||
@@ -2,18 +2,10 @@ package selector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
identity = Selector{segment{str: ".", identity: true}}
|
||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
||||
)
|
||||
|
||||
func Parse(str string) (Selector, error) {
|
||||
if len(str) == 0 {
|
||||
return nil, newParseError("empty selector", str, 0, "")
|
||||
@@ -21,9 +13,6 @@ func Parse(str string) (Selector, error) {
|
||||
if string(str[0]) != "." {
|
||||
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
||||
}
|
||||
if str == "." {
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
col := 0
|
||||
var sel Selector
|
||||
@@ -38,9 +27,9 @@ func Parse(str string) (Selector, error) {
|
||||
if len(sel) > 0 && sel[len(sel)-1].Identity() {
|
||||
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
|
||||
}
|
||||
sel = append(sel, segment{str: ".", identity: true})
|
||||
sel = append(sel, Identity)
|
||||
case "[]":
|
||||
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
|
||||
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
|
||||
default:
|
||||
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
|
||||
lookup := seg[1 : len(seg)-1]
|
||||
|
||||
@@ -2,6 +2,7 @@ package selector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
@@ -14,18 +15,6 @@ import (
|
||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
||||
type Selector []segment
|
||||
|
||||
// Select perform the selection described by the selector on the input IPLD DAG.
|
||||
// If no error, Select returns either one ipld.Node or a []ipld.Node.
|
||||
func (s Selector) Select(subject ipld.Node) (ipld.Node, []ipld.Node, error) {
|
||||
return resolve(s, subject, nil)
|
||||
}
|
||||
|
||||
// MatchPath tells if the selector operates on the given (string only) path segments.
|
||||
// It returns the segments that didn't get consumed by the matching.
|
||||
func (s Selector) MatchPath(pathSegment ...string) (bool, []string) {
|
||||
return matchPath(s, pathSegment)
|
||||
}
|
||||
|
||||
func (s Selector) String() string {
|
||||
var res strings.Builder
|
||||
for _, seg := range s {
|
||||
@@ -34,6 +23,29 @@ func (s Selector) String() string {
|
||||
return res.String()
|
||||
}
|
||||
|
||||
// Matches checks if the selector matches another selector.
|
||||
func (s Selector) Matches(other Selector, matcher SegmentMatcher) bool {
|
||||
if len(s) != len(other) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, seg := range s {
|
||||
if !matcher(seg, other[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
@@ -44,6 +56,48 @@ type segment struct {
|
||||
index int
|
||||
}
|
||||
|
||||
// NewFieldSegment creates a new segment for a field.
|
||||
func NewFieldSegment(field string) segment {
|
||||
return segment{
|
||||
str: fmt.Sprintf(".%s", field),
|
||||
field: field,
|
||||
}
|
||||
}
|
||||
|
||||
// NewIndexSegment creates a new segment for an index.
|
||||
func NewIndexSegment(index int) segment {
|
||||
return segment{
|
||||
str: fmt.Sprintf("[%d]", index),
|
||||
index: index,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSliceSegment creates a new segment for a slice.
|
||||
func NewSliceSegment(slice []int) segment {
|
||||
return segment{
|
||||
str: fmt.Sprintf("[%d:%d]", slice[0], slice[1]),
|
||||
slice: slice,
|
||||
}
|
||||
}
|
||||
|
||||
// NewIteratorSegment creates a new segment for an iterator.
|
||||
func NewIteratorSegment() segment {
|
||||
return segment{
|
||||
str: "*",
|
||||
iterator: true,
|
||||
}
|
||||
}
|
||||
|
||||
type SegmentMatcher func(s1, s2 segment) bool
|
||||
|
||||
var SegmentEquals SegmentMatcher = func(s1, s2 segment) bool {
|
||||
return s1.str == s2.str
|
||||
}
|
||||
|
||||
var SegmentStartsWith SegmentMatcher = func(s1, s2 segment) bool {
|
||||
return strings.HasPrefix(s1.str, s2.str)
|
||||
}
|
||||
|
||||
// String returns the segment's string representation.
|
||||
func (s segment) String() string {
|
||||
return s.str
|
||||
@@ -79,6 +133,12 @@ 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 {
|
||||
@@ -315,7 +375,7 @@ func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.No
|
||||
}
|
||||
}
|
||||
|
||||
default: // Index()
|
||||
default:
|
||||
at = append(at, fmt.Sprintf("%d", seg.Index()))
|
||||
if cur == nil {
|
||||
if seg.Optional() {
|
||||
@@ -377,39 +437,6 @@ func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.No
|
||||
return cur, nil, nil
|
||||
}
|
||||
|
||||
func matchPath(sel Selector, path []string) (bool, []string) {
|
||||
for _, seg := range sel {
|
||||
if len(path) == 0 {
|
||||
return true, path
|
||||
}
|
||||
switch {
|
||||
case seg.Identity():
|
||||
continue
|
||||
|
||||
case seg.Iterator():
|
||||
// we have reached a [] iterator, it should have matched earlier
|
||||
return false, nil
|
||||
|
||||
case seg.Field() != "":
|
||||
// if exact match on the segment, we continue
|
||||
if path[0] == seg.Field() {
|
||||
path = path[1:]
|
||||
continue
|
||||
}
|
||||
return false, nil
|
||||
|
||||
case seg.Slice() != nil:
|
||||
// we have reached a [<int>:<int>] slicing, it should have matched earlier
|
||||
return false, nil
|
||||
|
||||
default: // Index()
|
||||
// we have reached a [<int>] indexing, it should have matched earlier
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, path
|
||||
}
|
||||
|
||||
// resolveSliceIndices resolves the start and end indices for slicing a list or byte array.
|
||||
//
|
||||
// It takes the slice indices from the selector segment and the length of the list or byte array,
|
||||
|
||||
@@ -313,7 +313,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -328,7 +328,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".name.first")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -338,7 +338,7 @@ func TestSelect(t *testing.T) {
|
||||
name := must.String(one)
|
||||
require.Equal(t, alice.Name.First, name)
|
||||
|
||||
one, many, err = sel.Select(bnode)
|
||||
one, many, err = Select(sel, bnode)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -353,7 +353,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".name.middle?")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -363,7 +363,7 @@ func TestSelect(t *testing.T) {
|
||||
name := must.String(one)
|
||||
require.Equal(t, *alice.Name.Middle, name)
|
||||
|
||||
one, many, err = sel.Select(bnode)
|
||||
one, many, err = Select(sel, bnode)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -373,7 +373,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".name.foo")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.Error(t, err)
|
||||
require.Empty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -387,7 +387,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".name.foo?")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, one)
|
||||
require.Empty(t, many)
|
||||
@@ -397,7 +397,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".interests[]")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, one)
|
||||
require.NotEmpty(t, many)
|
||||
@@ -417,7 +417,7 @@ func TestSelect(t *testing.T) {
|
||||
sel, err := Parse(".interests[0][]")
|
||||
require.NoError(t, err)
|
||||
|
||||
one, many, err := sel.Select(anode)
|
||||
one, many, err := Select(sel, anode)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, one)
|
||||
require.NotEmpty(t, many)
|
||||
@@ -431,32 +431,6 @@ func TestSelect(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
sel string
|
||||
path []string
|
||||
want bool
|
||||
remaining []string
|
||||
}{
|
||||
{sel: ".foo.bar", path: []string{"foo", "bar"}, want: true, remaining: []string{}},
|
||||
{sel: ".foo.bar", path: []string{"foo"}, want: true, remaining: []string{}},
|
||||
{sel: ".foo.bar", path: []string{"foo", "bar", "baz"}, want: true, remaining: []string{"baz"}},
|
||||
{sel: ".foo.bar", path: []string{"foo", "faa"}, want: false},
|
||||
{sel: ".foo.[]", path: []string{"foo", "faa"}, want: false},
|
||||
{sel: ".foo.[]", path: []string{"foo"}, want: true, remaining: []string{}},
|
||||
{sel: ".foo.bar?", path: []string{"foo"}, want: true, remaining: []string{}},
|
||||
{sel: ".foo.bar?", path: []string{"foo", "bar"}, want: true, remaining: []string{}},
|
||||
{sel: ".foo.bar?", path: []string{"foo", "baz"}, want: false},
|
||||
} {
|
||||
t.Run(tc.sel, func(t *testing.T) {
|
||||
sel := MustParse(tc.sel)
|
||||
res, remain := sel.MatchPath(tc.path...)
|
||||
require.Equal(t, tc.want, res)
|
||||
require.EqualValues(t, tc.remaining, remain)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParse(f *testing.F) {
|
||||
selectorCorpus := []string{
|
||||
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
||||
@@ -520,6 +494,6 @@ func FuzzParseAndSelect(f *testing.F) {
|
||||
}
|
||||
|
||||
// look for panic()
|
||||
_, _, _ = sel.Select(node)
|
||||
_, _, _ = Select(sel, node)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestSupportedForms(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// attempt to select
|
||||
node, nodes, err := sel.Select(makeNode(t, tc.Input))
|
||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestSupportedForms(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// attempt to select
|
||||
node, nodes, err := sel.Select(makeNode(t, tc.Input))
|
||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||
require.NoError(t, err)
|
||||
// TODO: should Select return a single node which is sometimes a list or null?
|
||||
// require.Equal(t, datamodel.Null, node)
|
||||
@@ -124,7 +124,7 @@ func TestSupportedForms(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// attempt to select
|
||||
node, nodes, err := sel.Select(makeNode(t, tc.Input))
|
||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, node)
|
||||
assert.Empty(t, nodes)
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
package delegation_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
// The following example shows how to create a delegation.Token with
|
||||
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
|
||||
func ExampleNew() {
|
||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
issDid, err := did.FromPubKey(issPub)
|
||||
printThenPanicOnErr(err)
|
||||
fmt.Println("issDid:", issDid)
|
||||
|
||||
audDid := did.MustParse(AudienceDID)
|
||||
subDid := did.MustParse(subjectDID)
|
||||
|
||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
||||
cmd := command.MustParse("/foo/bar")
|
||||
|
||||
// The policy defines what is allowed to do.
|
||||
pol := policy.MustConstruct(
|
||||
policy.Equal(".status", literal.String("draft")),
|
||||
policy.All(".reviewer",
|
||||
policy.Like(".email", "*@example.com"),
|
||||
),
|
||||
policy.Any(".tags", policy.Or(
|
||||
policy.Equal(".", literal.String("news")),
|
||||
policy.Equal(".", literal.String("press")),
|
||||
)),
|
||||
)
|
||||
|
||||
tkn, err := delegation.New(issPriv, audDid, cmd, pol,
|
||||
delegation.WithSubject(subDid),
|
||||
delegation.WithExpirationIn(time.Hour),
|
||||
delegation.WithNotBeforeIn(time.Minute),
|
||||
delegation.WithMeta("foo", "bar"),
|
||||
delegation.WithMeta("baz", 123),
|
||||
)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
// "Seal", meaning encode and wrap into a signed envelope.
|
||||
data, id, err := tkn.ToSealed(issPriv)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
printCIDAndSealed(id, data)
|
||||
|
||||
// Example output:
|
||||
//
|
||||
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe
|
||||
//
|
||||
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW
|
||||
//
|
||||
// DAG-CBOR (base64) out: glhA5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDWzqY2lzc3g4ZGlkOmtleTp6Nk1raFZGem5QZVI1NzJyVEs1MVVqb1ROcG5GOGN4dVdmUG05b0JNUHI3eThBQmVjbmJmGmcNXxZjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3RBMXVCZENwcTR1SkJxRTlqak1pTHl4WkJnOWE2eGdQUEtKak1xc3M2WmMyZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMu0HMgJ5Y+M84I/66
|
||||
//
|
||||
// Converted to DAG-JSON out:
|
||||
// [
|
||||
// {
|
||||
// "/": {
|
||||
// "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "h": {
|
||||
// "/": {
|
||||
// "bytes": "NO0BcQ"
|
||||
// }
|
||||
// },
|
||||
// "ucan/dlg@1.0.0-rc.1": {
|
||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||
// "cmd": "/foo/bar",
|
||||
// "exp": 1728933098,
|
||||
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
||||
// "meta": {
|
||||
// "baz": 123,
|
||||
// "foo": "bar"
|
||||
// },
|
||||
// "nbf": 1728929558,
|
||||
// "nonce": {
|
||||
// "/": {
|
||||
// "bytes": "u0HMgJ5Y+M84I/66"
|
||||
// }
|
||||
// },
|
||||
// "pol": [
|
||||
// [
|
||||
// "==",
|
||||
// ".status",
|
||||
// "draft"
|
||||
// ],
|
||||
// [
|
||||
// "all",
|
||||
// ".reviewer",
|
||||
// [
|
||||
// "like",
|
||||
// ".email",
|
||||
// "*@example.com"
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// "any",
|
||||
// ".tags",
|
||||
// [
|
||||
// "or",
|
||||
// [
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "news"
|
||||
// ],
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "press"
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ],
|
||||
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
|
||||
// The following example shows how to create a UCAN root delegation.Token
|
||||
// - a delegation.Token with the subject (sub) set to the value of issuer
|
||||
// (iss).
|
||||
func ExampleRoot() {
|
||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
issDid, err := did.FromPubKey(issPub)
|
||||
printThenPanicOnErr(err)
|
||||
fmt.Println("issDid:", issDid)
|
||||
|
||||
audDid := did.MustParse(AudienceDID)
|
||||
|
||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
||||
cmd := command.MustParse("/foo/bar")
|
||||
|
||||
// The policy defines what is allowed to do.
|
||||
pol := policy.MustConstruct(
|
||||
policy.Equal(".status", literal.String("draft")),
|
||||
policy.All(".reviewer",
|
||||
policy.Like(".email", "*@example.com"),
|
||||
),
|
||||
policy.Any(".tags", policy.Or(
|
||||
policy.Equal(".", literal.String("news")),
|
||||
policy.Equal(".", literal.String("press")),
|
||||
)),
|
||||
)
|
||||
|
||||
tkn, err := delegation.Root(issPriv, audDid, cmd, pol,
|
||||
delegation.WithExpirationIn(time.Hour),
|
||||
delegation.WithNotBeforeIn(time.Minute),
|
||||
delegation.WithMeta("foo", "bar"),
|
||||
delegation.WithMeta("baz", 123),
|
||||
)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
// "Seal", meaning encode and wrap into a signed envelope.
|
||||
data, id, err := tkn.ToSealed(issPriv)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
printCIDAndSealed(id, data)
|
||||
|
||||
// Example output:
|
||||
//
|
||||
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
|
||||
//
|
||||
// CID (base58BTC): zdpuAwLojgfvFCbjz2FsKrvN1khDQ9mFGT6b6pxjMfz73Roed
|
||||
//
|
||||
// DAG-CBOR (base64) out: glhA6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDW0wY2lzc3g4ZGlkOmtleTp6Nk1rbldKcXoxN1k0QWZzWFNKVUZLb211QlI0R1RrVmlNN2tKWXV0elRNa0N5RkZjbmJmGmcNX1xjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa25XSnF6MTdZNEFmc1hTSlVGS29tdUJSNEdUa1ZpTTdrSll1dHpUTWtDeUZGZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMJOsjYi1Pq3OIB0La
|
||||
//
|
||||
// Converted to DAG-JSON out:
|
||||
// [
|
||||
// {
|
||||
// "/": {
|
||||
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "h": {
|
||||
// "/": {
|
||||
// "bytes": "NO0BcQ"
|
||||
// }
|
||||
// },
|
||||
// "ucan/dlg@1.0.0-rc.1": {
|
||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||
// "cmd": "/foo/bar",
|
||||
// "exp": 1728933168,
|
||||
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
|
||||
// "meta": {
|
||||
// "baz": 123,
|
||||
// "foo": "bar"
|
||||
// },
|
||||
// "nbf": 1728929628,
|
||||
// "nonce": {
|
||||
// "/": {
|
||||
// "bytes": "JOsjYi1Pq3OIB0La"
|
||||
// }
|
||||
// },
|
||||
// "pol": [
|
||||
// [
|
||||
// "==",
|
||||
// ".status",
|
||||
// "draft"
|
||||
// ],
|
||||
// [
|
||||
// "all",
|
||||
// ".reviewer",
|
||||
// [
|
||||
// "like",
|
||||
// ".email",
|
||||
// "*@example.com"
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// "any",
|
||||
// ".tags",
|
||||
// [
|
||||
// "or",
|
||||
// [
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "news"
|
||||
// ],
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "press"
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ],
|
||||
// "sub": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF"
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
|
||||
// The following example demonstrates how to get a delegation.Token from
|
||||
// a DAG-CBOR []byte.
|
||||
func ExampleToken_FromSealed() {
|
||||
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
||||
|
||||
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
tkn, c, err := delegation.FromSealed(cborBytes)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(c))
|
||||
fmt.Println("Issuer (iss):", tkn.Issuer().String())
|
||||
fmt.Println("Audience (aud):", tkn.Audience().String())
|
||||
fmt.Println("Subject (sub):", tkn.Subject().String())
|
||||
fmt.Println("Command (cmd):", tkn.Command().String())
|
||||
fmt.Println("Policy (pol):", tkn.Policy().String())
|
||||
fmt.Println("Nonce (nonce):", hex.EncodeToString(tkn.Nonce()))
|
||||
fmt.Println("Meta (meta):", tkn.Meta().String())
|
||||
fmt.Println("NotBefore (nbf):", tkn.NotBefore())
|
||||
fmt.Println("Expiration (exp):", tkn.Expiration())
|
||||
|
||||
// Output:
|
||||
// CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
|
||||
// Issuer (iss): did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2
|
||||
// Audience (aud): did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv
|
||||
// Subject (sub): did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2
|
||||
// Command (cmd): /foo/bar
|
||||
// Policy (pol): [
|
||||
// ["==", ".status", "draft"],
|
||||
// ["all", ".reviewer",
|
||||
// ["like", ".email", "*@example.com"]],
|
||||
// ["any", ".tags",
|
||||
// ["or", [
|
||||
// ["==", ".", "news"],
|
||||
// ["==", ".", "press"]]]
|
||||
// ]
|
||||
// ]
|
||||
// Nonce (nonce): 000102030405060708090a0b
|
||||
// Meta (meta): {}
|
||||
// NotBefore (nbf): <nil>
|
||||
// Expiration (exp): <nil>
|
||||
}
|
||||
|
||||
func printCIDAndSealed(id cid.Cid, data []byte) {
|
||||
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(id))
|
||||
fmt.Println("DAG-CBOR (base64) out:", base64.StdEncoding.EncodeToString(data))
|
||||
fmt.Println("Converted to DAG-JSON out:")
|
||||
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
rawJSON, err := ipld.Encode(node, dagjson.Encode)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
prettyJSON := &bytes.Buffer{}
|
||||
err = json.Indent(prettyJSON, rawJSON, "", "\t")
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
fmt.Println(prettyJSON.String())
|
||||
}
|
||||
|
||||
func printThenPanicOnErr(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
type Info = envelope.Info
|
||||
|
||||
// Inspect inspects the given token IPLD representation and extract some envelope facts.
|
||||
func Inspect(node datamodel.Node) (Info, error) {
|
||||
return envelope.Inspect(node)
|
||||
}
|
||||
|
||||
// FindTag inspect the given token IPLD representation and extract the token tag.
|
||||
func FindTag(node datamodel.Node) (string, error) {
|
||||
return envelope.FindTag(node)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime/codec"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
)
|
||||
|
||||
type Token interface {
|
||||
Marshaller
|
||||
|
||||
// Issuer returns the did.DID representing the Token's issuer.
|
||||
Issuer() did.DID
|
||||
// Meta returns the Token's metadata.
|
||||
Meta() *meta.Meta
|
||||
}
|
||||
|
||||
type Marshaller interface {
|
||||
// ToSealed wraps the token in an envelope, generates the signature, encodes
|
||||
// the result to DAG-CBOR and calculates the CID of the resulting binary data.
|
||||
ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error)
|
||||
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
||||
ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error)
|
||||
// Encode marshals a Token to the format specified by the provided codec.Encoder.
|
||||
Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error)
|
||||
// EncodeWriter is the same as Encode, but accepts an io.Writer.
|
||||
EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error
|
||||
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
||||
ToDagCbor(privKey crypto.PrivKey) ([]byte, error)
|
||||
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
|
||||
ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error
|
||||
// ToDagJson marshals the Token to the DAG-JSON format.
|
||||
ToDagJson(privKey crypto.PrivKey) ([]byte, error)
|
||||
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
||||
ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// Package invocation implements the UCAN [invocation] specification with
|
||||
// an immutable Token type as well as methods to convert the Token to and
|
||||
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
||||
// should most commonly be used for transport and storage.
|
||||
//
|
||||
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
||||
// [invocation]: https://github.com/ucan-wg/invocation
|
||||
package invocation
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
)
|
||||
|
||||
// Token is an immutable type that holds the fields of a UCAN invocation.
|
||||
type Token struct {
|
||||
// Issuer DID (invoker)
|
||||
issuer did.DID
|
||||
// Audience DID (receiver/executor)
|
||||
audience did.DID
|
||||
// Subject DID (subject being invoked)
|
||||
subject did.DID
|
||||
// The Command to invoke
|
||||
command command.Command
|
||||
// TODO: args
|
||||
// TODO: prf
|
||||
// A unique, random nonce
|
||||
nonce []byte
|
||||
// Arbitrary Metadata
|
||||
meta *meta.Meta
|
||||
// The timestamp at which the Invocation becomes invalid
|
||||
expiration *time.Time
|
||||
// The timestamp at which the Invocation was created
|
||||
invokedAt *time.Time
|
||||
// TODO: cause
|
||||
}
|
||||
|
||||
// Issuer returns the did.DID representing the Token's issuer.
|
||||
func (t *Token) Issuer() did.DID {
|
||||
return t.issuer
|
||||
}
|
||||
|
||||
// Audience returns the did.DID representing the Token's audience.
|
||||
func (t *Token) Audience() did.DID {
|
||||
return t.audience
|
||||
}
|
||||
|
||||
// Subject returns the did.DID representing the Token's subject.
|
||||
//
|
||||
// This field may be did.Undef for delegations that are [Powerlined] but
|
||||
// must be equal to the value returned by the Issuer method for root
|
||||
// tokens.
|
||||
func (t *Token) Subject() did.DID {
|
||||
return t.subject
|
||||
}
|
||||
|
||||
// Command returns the capability's command.Command.
|
||||
func (t *Token) Command() command.Command {
|
||||
return t.command
|
||||
}
|
||||
|
||||
// Nonce returns the random Nonce encapsulated in this Token.
|
||||
func (t *Token) Nonce() []byte {
|
||||
return t.nonce
|
||||
}
|
||||
|
||||
// Meta returns the Token's metadata.
|
||||
func (t *Token) Meta() *meta.Meta {
|
||||
return t.meta
|
||||
}
|
||||
|
||||
// Expiration returns the time at which the Token expires.
|
||||
func (t *Token) Expiration() *time.Time {
|
||||
return t.expiration
|
||||
}
|
||||
|
||||
func (t *Token) validate() error {
|
||||
var errs error
|
||||
|
||||
requiredDID := func(id did.DID, fieldname string) {
|
||||
if !id.Defined() {
|
||||
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
||||
}
|
||||
}
|
||||
|
||||
requiredDID(t.issuer, "Issuer")
|
||||
|
||||
// TODO
|
||||
|
||||
if len(t.nonce) < 12 {
|
||||
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// tokenFromModel build a decoded view of the raw IPLD data.
|
||||
// This function also serves as validation.
|
||||
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
var (
|
||||
tkn Token
|
||||
)
|
||||
|
||||
// TODO
|
||||
|
||||
return &tkn, nil
|
||||
}
|
||||
|
||||
// generateNonce creates a 12-byte random nonce.
|
||||
// TODO: some crypto scheme require more, is that our case?
|
||||
func generateNonce() ([]byte, error) {
|
||||
res := make([]byte, 12)
|
||||
_, err := rand.Read(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
type DID string
|
||||
|
||||
# The Invocation Payload attaches sender, receiver, and provenance to the Task.
|
||||
type Payload struct {
|
||||
# Issuer DID (sender)
|
||||
iss DID
|
||||
# Audience DID (receiver)
|
||||
aud DID
|
||||
# Principal that the chain is about (the Subject)
|
||||
sub optional DID
|
||||
|
||||
# The Command to eventually invoke
|
||||
cmd String
|
||||
|
||||
# A unique, random nonce
|
||||
nonce Bytes
|
||||
|
||||
# Arbitrary Metadata
|
||||
meta {String : Any}
|
||||
|
||||
# The timestamp at which the Invocation becomes invalid
|
||||
exp nullable Int
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
package invocation
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
// ToSealed wraps the invocation token in an envelope, generates the
|
||||
// signature, encodes the result to DAG-CBOR and calculates the CID of
|
||||
// the resulting binary data.
|
||||
func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
|
||||
data, err := t.ToDagCbor(privKey)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
return data, id, nil
|
||||
}
|
||||
|
||||
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
||||
func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error) {
|
||||
cidWriter := envelope.NewCIDWriter(w)
|
||||
|
||||
if err := t.ToDagCborWriter(cidWriter, privKey); err != nil {
|
||||
return cid.Undef, err
|
||||
}
|
||||
|
||||
return cidWriter.CID()
|
||||
}
|
||||
|
||||
// FromSealed decodes the provided binary data from the DAG-CBOR format,
|
||||
// verifies that the envelope's signature is correct based on the public
|
||||
// key taken from the issuer (iss) field and calculates the CID of the
|
||||
// incoming data.
|
||||
func FromSealed(data []byte) (*Token, cid.Cid, error) {
|
||||
tkn, err := FromDagCbor(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
return tkn, id, nil
|
||||
}
|
||||
|
||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
||||
func FromSealedReader(r io.Reader) (*Token, cid.Cid, error) {
|
||||
cidReader := envelope.NewCIDReader(r)
|
||||
|
||||
tkn, err := FromDagCborReader(cidReader)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
id, err := cidReader.CID()
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
return tkn, id, nil
|
||||
}
|
||||
|
||||
// Encode marshals a Token to the format specified by the provided
|
||||
// codec.Encoder.
|
||||
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
|
||||
node, err := t.toIPLD(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ipld.Encode(node, encFn)
|
||||
}
|
||||
|
||||
// EncodeWriter is the same as Encode, but accepts an io.Writer.
|
||||
func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error {
|
||||
node, err := t.toIPLD(privKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ipld.EncodeStreaming(w, node, encFn)
|
||||
}
|
||||
|
||||
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
||||
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
|
||||
return t.Encode(privKey, dagcbor.Encode)
|
||||
}
|
||||
|
||||
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
|
||||
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||
return t.EncodeWriter(w, privKey, dagcbor.Encode)
|
||||
}
|
||||
|
||||
// ToDagJson marshals the Token to the DAG-JSON format.
|
||||
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
|
||||
return t.Encode(privKey, dagjson.Encode)
|
||||
}
|
||||
|
||||
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
||||
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||
return t.EncodeWriter(w, privKey, dagjson.Encode)
|
||||
}
|
||||
|
||||
// Decode unmarshals the input data using the format specified by the
|
||||
// provided codec.Decoder into a Token.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
func Decode(b []byte, decFn codec.Decoder) (*Token, error) {
|
||||
node, err := ipld.Decode(b, decFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FromIPLD(node)
|
||||
}
|
||||
|
||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
|
||||
node, err := ipld.DecodeStreaming(r, decFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FromIPLD(node)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals the input data into a Token.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
func FromDagCbor(data []byte) (*Token, error) {
|
||||
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn, err := tokenFromModel(*pay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, err
|
||||
}
|
||||
|
||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||
func FromDagCborReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagJson unmarshals the input data into a Token.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
func FromDagJson(data []byte) (*Token, error) {
|
||||
return Decode(data, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||
func FromDagJsonReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromIPLD decode the given IPLD representation into a Token.
|
||||
func FromIPLD(node datamodel.Node) (*Token, error) {
|
||||
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tkn, err := tokenFromModel(*pay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, err
|
||||
}
|
||||
|
||||
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||
var sub *string
|
||||
|
||||
if t.subject != did.Undef {
|
||||
s := t.subject.String()
|
||||
sub = &s
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
var exp *int64
|
||||
if t.expiration != nil {
|
||||
u := t.expiration.Unix()
|
||||
exp = &u
|
||||
}
|
||||
|
||||
model := &tokenPayloadModel{
|
||||
Iss: t.issuer.String(),
|
||||
Aud: t.audience.String(),
|
||||
Sub: sub,
|
||||
Cmd: t.command.String(),
|
||||
Nonce: t.nonce,
|
||||
Meta: *t.meta,
|
||||
Exp: exp,
|
||||
}
|
||||
|
||||
return envelope.ToIPLD(privKey, model)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package invocation
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
// [Tag] is the string used as a key within the SigPayload that identifies
|
||||
// that the TokenPayload is an invocation.
|
||||
//
|
||||
// [Tag]: https://github.com/ucan-wg/invocation#type-tag
|
||||
const Tag = "ucan/inv@1.0.0-rc.1"
|
||||
|
||||
//go:embed invocation.ipldsch
|
||||
var schemaBytes []byte
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
)
|
||||
|
||||
func mustLoadSchema() *schema.TypeSystem {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
func payloadType() schema.Type {
|
||||
return mustLoadSchema().TypeByName("Payload")
|
||||
}
|
||||
|
||||
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
||||
|
||||
// TODO
|
||||
type tokenPayloadModel struct {
|
||||
// Issuer DID (sender)
|
||||
Iss string
|
||||
// Audience DID (receiver)
|
||||
Aud string
|
||||
// Principal that the chain is about (the Subject)
|
||||
// optional: can be nil
|
||||
Sub *string
|
||||
|
||||
// The Command to eventually invoke
|
||||
Cmd string
|
||||
|
||||
// A unique, random nonce
|
||||
Nonce []byte
|
||||
|
||||
// Arbitrary Metadata
|
||||
Meta meta.Meta
|
||||
|
||||
// The timestamp at which the Invocation becomes invalid
|
||||
// optional: can be nil
|
||||
Exp *int64
|
||||
}
|
||||
|
||||
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
||||
return bindnode.Prototype((*tokenPayloadModel)(nil), payloadType())
|
||||
}
|
||||
|
||||
func (*tokenPayloadModel) Tag() string {
|
||||
return Tag
|
||||
}
|
||||
125
token/read.go
125
token/read.go
@@ -1,125 +0,0 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||
)
|
||||
|
||||
// FromSealed decodes an arbitrary token type from the binary data,
|
||||
// verifies that the envelope's signature is correct based on the public
|
||||
// key taken from the issuer (iss) field and calculates the CID of the
|
||||
// incoming data.
|
||||
// Supported and returned types are:
|
||||
// - delegation.Token
|
||||
// - invocation.Token
|
||||
func FromSealed(data []byte) (Token, cid.Cid, error) {
|
||||
tkn, err := FromDagCbor(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
return tkn, id, nil
|
||||
}
|
||||
|
||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
||||
func FromSealedReader(r io.Reader) (Token, cid.Cid, error) {
|
||||
cidReader := envelope.NewCIDReader(r)
|
||||
|
||||
tkn, err := FromDagCborReader(cidReader)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
id, err := cidReader.CID()
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
}
|
||||
|
||||
return tkn, id, nil
|
||||
}
|
||||
|
||||
// Decode unmarshals the input data using the format specified by the
|
||||
// provided codec.Decoder into an arbitrary UCAN token.
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
// Supported and returned types are:
|
||||
// - delegation.Token
|
||||
// - invocation.Token
|
||||
func Decode(b []byte, decFn codec.Decoder) (Token, error) {
|
||||
node, err := ipld.Decode(b, decFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromIPLD(node)
|
||||
}
|
||||
|
||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||
func DecodeReader(r io.Reader, decFn codec.Decoder) (Token, error) {
|
||||
node, err := ipld.DecodeStreaming(r, decFn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromIPLD(node)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals an arbitrary DagCbor encoded UCAN token.
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
// Supported and returned types are:
|
||||
// - delegation.Token
|
||||
// - invocation.Token
|
||||
func FromDagCbor(b []byte) (Token, error) {
|
||||
return Decode(b, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
|
||||
func FromDagCborReader(r io.Reader) (Token, error) {
|
||||
return DecodeReader(r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals an arbitrary DagJson encoded UCAN token.
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
// Supported and returned types are:
|
||||
// - delegation.Token
|
||||
// - invocation.Token
|
||||
func FromDagJson(b []byte) (Token, error) {
|
||||
return Decode(b, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
|
||||
func FromDagJsonReader(r io.Reader) (Token, error) {
|
||||
return DecodeReader(r, dagjson.Decode)
|
||||
}
|
||||
|
||||
func fromIPLD(node datamodel.Node) (Token, error) {
|
||||
tag, err := envelope.FindTag(node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case delegation.Tag:
|
||||
return delegation.FromIPLD(node)
|
||||
case invocation.Tag:
|
||||
return invocation.FromIPLD(node)
|
||||
default:
|
||||
return nil, fmt.Errorf(`unknown tag "%s"`, tag)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
@@ -43,13 +44,11 @@ type Token struct {
|
||||
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 {
|
||||
@@ -64,6 +63,7 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
||||
policy: pol,
|
||||
meta: meta.NewMeta(),
|
||||
nonce: nil,
|
||||
cid: cid.Undef,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
@@ -86,12 +86,6 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
||||
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 {
|
||||
@@ -152,6 +146,13 @@ 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
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/tokens/delegation"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
// ToSealed wraps the delegation token in an envelope, generates the
|
||||
@@ -32,7 +32,7 @@ func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
|
||||
return data, id, nil
|
||||
}
|
||||
|
||||
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
|
||||
// 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)
|
||||
|
||||
@@ -47,38 +47,42 @@ func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, er
|
||||
// verifies that the envelope's signature is correct based on the public
|
||||
// key taken from the issuer (iss) field and calculates the CID of the
|
||||
// incoming data.
|
||||
func FromSealed(data []byte) (*Token, cid.Cid, error) {
|
||||
func FromSealed(data []byte) (*Token, error) {
|
||||
tkn, err := FromDagCbor(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := envelope.CIDFromBytes(data)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, id, nil
|
||||
tkn.cid = id
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// FromSealedReader is the same as Unseal but accepts an io.Reader.
|
||||
func FromSealedReader(r io.Reader) (*Token, cid.Cid, error) {
|
||||
func FromSealedReader(r io.Reader) (*Token, error) {
|
||||
cidReader := envelope.NewCIDReader(r)
|
||||
|
||||
tkn, err := FromDagCborReader(cidReader)
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := cidReader.CID()
|
||||
if err != nil {
|
||||
return nil, cid.Undef, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tkn, id, nil
|
||||
tkn.cid = id
|
||||
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// Encode marshals a Token to the format specified by the provided
|
||||
// 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)
|
||||
@@ -89,7 +93,7 @@ func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, err
|
||||
return ipld.Encode(node, encFn)
|
||||
}
|
||||
|
||||
// EncodeWriter is the same as Encode, but accepts an io.Writer.
|
||||
// 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 {
|
||||
@@ -99,37 +103,37 @@ func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.En
|
||||
return ipld.EncodeStreaming(w, node, encFn)
|
||||
}
|
||||
|
||||
// ToDagCbor marshals the Token to the DAG-CBOR format.
|
||||
// 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.
|
||||
// ToDagCborWriter is the same as ToDagCbor but it accepts an io.Writer.
|
||||
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||
return t.EncodeWriter(w, privKey, dagcbor.Encode)
|
||||
}
|
||||
|
||||
// ToDagJson marshals the Token to the DAG-JSON format.
|
||||
// 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.
|
||||
// ToDagJsonWriter is the same as ToDagJson but it accepts an io.Writer.
|
||||
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
|
||||
return t.EncodeWriter(w, privKey, dagjson.Encode)
|
||||
}
|
||||
|
||||
// Decode unmarshals the input data using the format specified by the
|
||||
// provided codec.Decoder into a Token.
|
||||
// provided codec.Decoder into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
// 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)
|
||||
return fromIPLD(node)
|
||||
}
|
||||
|
||||
// DecodeReader is the same as Decode, but accept an io.Reader.
|
||||
@@ -138,13 +142,13 @@ func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return FromIPLD(node)
|
||||
return fromIPLD(node)
|
||||
}
|
||||
|
||||
// FromDagCbor unmarshals the input data into a Token.
|
||||
// FromDagCbor unmarshals the input data into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
// View is invalid.
|
||||
func FromDagCbor(data []byte) (*Token, error) {
|
||||
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
|
||||
if err != nil {
|
||||
@@ -164,10 +168,10 @@ func FromDagCborReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagcbor.Decode)
|
||||
}
|
||||
|
||||
// FromDagJson unmarshals the input data into a Token.
|
||||
// FromDagJson unmarshals the input data into a View.
|
||||
//
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Token is invalid.
|
||||
// View is invalid.
|
||||
func FromDagJson(data []byte) (*Token, error) {
|
||||
return Decode(data, dagjson.Decode)
|
||||
}
|
||||
@@ -177,8 +181,7 @@ func FromDagJsonReader(r io.Reader) (*Token, error) {
|
||||
return DecodeReader(r, dagjson.Decode)
|
||||
}
|
||||
|
||||
// FromIPLD decode the given IPLD representation into a Token.
|
||||
func FromIPLD(node datamodel.Node) (*Token, error) {
|
||||
func fromIPLD(node datamodel.Node) (*Token, error) {
|
||||
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -24,15 +24,6 @@ func WithExpiration(exp time.Time) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithExpirationIn set's the Token's optional "expiration" field to Now() plus the given duration.
|
||||
func WithExpirationIn(exp time.Duration) Option {
|
||||
return func(t *Token) error {
|
||||
expTime := time.Now().Add(exp)
|
||||
t.expiration = &expTime
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMeta adds a key/value pair in the "meta" field.
|
||||
//
|
||||
// WithMeta can be used multiple times in the same call.
|
||||
@@ -57,21 +48,11 @@ func WithNotBefore(nbf time.Time) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithNotBeforeIn set's the Token's optional "notBefore" field to the value
|
||||
// of the provided time.Time.
|
||||
func WithNotBeforeIn(nbf time.Duration) Option {
|
||||
return func(t *Token) error {
|
||||
nbfTime := time.Now().Add(nbf)
|
||||
t.notBefore = &nbfTime
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the Tokens's optional "subject" field to the value of
|
||||
// provided did.DID.
|
||||
//
|
||||
// This Option should only be used with the New constructor - since
|
||||
// Subject is a required parameter when creating a Token via the Root
|
||||
// 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 {
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
// [Tag] is the string used as a key within the SigPayload that identifies
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/delegation"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
//go:embed delegation.ipldsch
|
||||
@@ -39,9 +39,9 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
fmt.Println("cborBytes length", len(cborBytes))
|
||||
fmt.Println("cbor", string(cborBytes))
|
||||
|
||||
p2, c2, err := delegation.FromSealed(cborBytes)
|
||||
p2, err := delegation.FromSealed(cborBytes)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, id, c2)
|
||||
assert.Equal(t, id, p2.CID())
|
||||
fmt.Println("read Cbor", p2)
|
||||
|
||||
readJson, err := p2.ToDagJson(privKey)
|
||||
@@ -70,9 +70,10 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||
|
||||
// buf = bytes.NewBuffer(cborBytes.Bytes())
|
||||
p2, c2, err := delegation.FromSealedReader(cborBytes)
|
||||
p2, err := delegation.FromSealedReader(cborBytes)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(c2))
|
||||
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))
|
||||
@@ -95,7 +96,7 @@ func BenchmarkRoundTrip(b *testing.B) {
|
||||
b.Run("via buffers", func(b *testing.B) {
|
||||
p1, _ := delegation.FromDagJson(delegationJson)
|
||||
cborBytes, _, _ := p1.ToSealed(privKey)
|
||||
p2, _, _ := delegation.FromSealed(cborBytes)
|
||||
p2, _ := delegation.FromSealed(cborBytes)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -116,7 +117,7 @@ func BenchmarkRoundTrip(b *testing.B) {
|
||||
b.Run("Unseal", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = delegation.FromSealed(cborBytes)
|
||||
_, _ = delegation.FromSealed(cborBytes)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -133,7 +134,7 @@ func BenchmarkRoundTrip(b *testing.B) {
|
||||
cborBuf := &bytes.Buffer{}
|
||||
_, _ = p1.ToSealedWriter(cborBuf, privKey)
|
||||
cborBytes := cborBuf.Bytes()
|
||||
p2, _, _ := delegation.FromSealedReader(bytes.NewReader(cborBytes))
|
||||
p2, _ := delegation.FromSealedReader(bytes.NewReader(cborBytes))
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
@@ -160,7 +161,7 @@ func BenchmarkRoundTrip(b *testing.B) {
|
||||
reader := bytes.NewReader(cborBytes)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = reader.Seek(0, 0)
|
||||
_, _, _ = delegation.FromSealedReader(reader)
|
||||
_, _ = delegation.FromSealedReader(reader)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
)
|
||||
|
||||
func TestCidFromBytes(t *testing.T) {
|
||||
@@ -16,9 +16,8 @@ import (
|
||||
"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"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,9 +27,7 @@ package envelope
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec"
|
||||
@@ -43,13 +41,10 @@ import (
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/token/internal/varsig"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
|
||||
)
|
||||
|
||||
const (
|
||||
VarsigHeaderKey = "h"
|
||||
UCANTagPrefix = "ucan/"
|
||||
)
|
||||
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).
|
||||
@@ -94,7 +89,19 @@ func DecodeReader[T Tokener](r io.Reader, decFn codec.Decoder) (T, error) {
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Tokener is invalid.
|
||||
func FromDagCbor[T Tokener](b []byte) (T, error) {
|
||||
return Decode[T](b, dagcbor.Decode)
|
||||
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.
|
||||
@@ -120,81 +127,113 @@ func FromDagJsonReader[T Tokener](r io.Reader) (T, error) {
|
||||
// An error is returned if the conversion fails, or if the resulting
|
||||
// Tokener is invalid.
|
||||
func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
||||
zero := *new(T)
|
||||
undef := *new(T)
|
||||
|
||||
info, err := Inspect(node)
|
||||
tkn, err := fromIPLD[T](node)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
if info.Tag != zero.Tag() {
|
||||
return zero, errors.New("data doesn't match the expected type")
|
||||
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 := info.tokenPayloadNode.LookupByString("iss")
|
||||
issuerNode, err := tokenPayloadNode.LookupByString("iss")
|
||||
if err != nil {
|
||||
return zero, err
|
||||
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 := zero.Prototype().Representation().NewBuilder()
|
||||
nb := undef.Prototype().Representation().NewBuilder()
|
||||
|
||||
err = nb.AssignNode(info.tokenPayloadNode)
|
||||
err = nb.AssignNode(tokenPayloadNode)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
tokenPayloadNode := nb.Build()
|
||||
tokenPayloadNode = nb.Build()
|
||||
|
||||
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
|
||||
if tokenPayload == nil {
|
||||
return zero, errors.New("failed to Unwrap the TokenPayload")
|
||||
return undef, errors.New("failed to Unwrap the TokenPayload")
|
||||
}
|
||||
|
||||
tkn, ok := tokenPayload.(T)
|
||||
if !ok {
|
||||
return zero, errors.New("failed to assert the TokenPayload type as *token.Token")
|
||||
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 zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
issuerDID, err := did.Parse(issuer)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
issuerPubKey, err := issuerDID.PubKey()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
|
||||
if err != nil {
|
||||
return zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
if string(info.VarsigHeader) != string(issuerVarsigHeader) {
|
||||
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
||||
}
|
||||
|
||||
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
|
||||
varsigHeader, err := varsigHeaderNode.AsBytes()
|
||||
if err != nil {
|
||||
return zero, err
|
||||
return undef, err
|
||||
}
|
||||
|
||||
ok, err = issuerPubKey.Verify(data, info.Signature)
|
||||
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 zero, errors.New("failed to verify the token's signature")
|
||||
return undef, errors.New("failed to verify the token's signature")
|
||||
}
|
||||
|
||||
return tkn, nil
|
||||
@@ -254,7 +293,7 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
|
||||
}
|
||||
|
||||
sigPayloadNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, VarsigHeaderKey, qp.Bytes(varsigHeader))
|
||||
qp.MapEntry(ma, varsigHeaderKey, qp.Bytes(varsigHeader))
|
||||
qp.MapEntry(ma, token.Tag(), qp.Node(tokenPayloadNode))
|
||||
})
|
||||
|
||||
@@ -273,121 +312,3 @@ func ToIPLD(privKey crypto.PrivKey, token Tokener) (datamodel.Node, error) {
|
||||
qp.ListEntry(la, qp.Node(sigPayloadNode))
|
||||
})
|
||||
}
|
||||
|
||||
// FindTag inspects the given token IPLD representation and extract the token tag.
|
||||
func FindTag(node datamodel.Node) (string, error) {
|
||||
sigPayloadNode, err := node.LookupByIndex(1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if sigPayloadNode.Kind() != datamodel.Kind_Map {
|
||||
return "", fmt.Errorf("unexpected type instead of map")
|
||||
}
|
||||
|
||||
it := sigPayloadNode.MapIterator()
|
||||
i := 0
|
||||
|
||||
for !it.Done() {
|
||||
if i >= 2 {
|
||||
return "", fmt.Errorf("expected two and only two fields in SigPayload")
|
||||
}
|
||||
i++
|
||||
|
||||
k, _, err := it.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key, err := k.AsString()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(key, UCANTagPrefix) {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no token tag found")
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Tag string
|
||||
Signature []byte
|
||||
VarsigHeader []byte
|
||||
sigPayloadNode datamodel.Node // private, we don't want to expose that
|
||||
tokenPayloadNode datamodel.Node // private, we don't want to expose that
|
||||
}
|
||||
|
||||
// Inspect inspects the given token IPLD representation and extract some envelope facts.
|
||||
func Inspect(node datamodel.Node) (Info, error) {
|
||||
var res Info
|
||||
|
||||
signatureNode, err := node.LookupByIndex(0)
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
res.Signature, err = signatureNode.AsBytes()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
res.sigPayloadNode, err = node.LookupByIndex(1)
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
if res.sigPayloadNode.Kind() != datamodel.Kind_Map {
|
||||
return Info{}, fmt.Errorf("unexpected type instead of map")
|
||||
}
|
||||
|
||||
it := res.sigPayloadNode.MapIterator()
|
||||
foundVarsigHeader := false
|
||||
foundTokenPayload := false
|
||||
i := 0
|
||||
|
||||
for !it.Done() {
|
||||
if i >= 2 {
|
||||
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload")
|
||||
}
|
||||
i++
|
||||
|
||||
k, v, err := it.Next()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
key, err := k.AsString()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case key == VarsigHeaderKey:
|
||||
foundVarsigHeader = true
|
||||
res.VarsigHeader, err = v.AsBytes()
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
case strings.HasPrefix(key, UCANTagPrefix):
|
||||
foundTokenPayload = true
|
||||
res.Tag = key
|
||||
res.tokenPayloadNode = v
|
||||
default:
|
||||
return Info{}, fmt.Errorf("unexpected key type %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
if i != 2 {
|
||||
return Info{}, fmt.Errorf("expected two and only two fields in SigPayload: %d", i)
|
||||
}
|
||||
if !foundVarsigHeader {
|
||||
return Info{}, errors.New("failed to find VarsigHeader field")
|
||||
}
|
||||
if !foundTokenPayload {
|
||||
return Info{}, errors.New("failed to find TokenPayload field")
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -3,17 +3,12 @@ package envelope_test
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||
)
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
@@ -154,56 +149,3 @@ func TestHash(t *testing.T) {
|
||||
require.Equal(t, hash1[:], hash2)
|
||||
require.Equal(t, hash1[:], hash3)
|
||||
}
|
||||
|
||||
func TestInspect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
data := golden.Get(t, "example.dagcbor")
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
require.NoError(t, err)
|
||||
|
||||
expSig, err := base64.RawStdEncoding.DecodeString("fPqfwL3iFpbw9SvBiq0DIbUurv9o6c36R08tC/yslGrJcwV51ghzWahxdetpEf6T5LCszXX9I/K8khvnmAxjAg")
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := envelope.Inspect(node)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expSig, info.Signature)
|
||||
assert.Equal(t, "ucan/example@v1.0.0-rc.1", info.Tag)
|
||||
assert.Equal(t, []byte{0x34, 0xed, 0x1, 0x71}, info.VarsigHeader)
|
||||
}
|
||||
|
||||
func FuzzInspect(f *testing.F) {
|
||||
data, err := os.ReadFile("testdata/example.dagcbor")
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(data)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
_, err = envelope.Inspect(node)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzFindTag(f *testing.F) {
|
||||
data, err := os.ReadFile("testdata/example.dagcbor")
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(data)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
_, err = envelope.FindTag(node)
|
||||
if err != nil {
|
||||
t.Skip()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/internal/varsig"
|
||||
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
|
||||
)
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
Reference in New Issue
Block a user