38 Commits

Author SHA1 Message Date
Fabio Bozzo
d44bf32fad adapt KindAll too 2024-10-21 16:39:19 +02:00
Fabio Bozzo
948945bdf1 fix(policy): refactor kind-any and equality 2024-10-21 16:33:18 +02:00
Michael Muré
ac1b03f144 Merge pull request #44 from ucan-wg/policy-filtering
policy: add a way to filter policies with a path
2024-10-16 14:29:57 +02:00
Michael Muré
7fa3ba1492 policy: add a way to filter policies with a path
Based on exploration work https://github.com/ucan-wg/go-ucan/pull/27
2024-10-16 13:48:01 +02:00
Michael Muré
081d382028 selector: Select is now a method 2024-10-15 17:26:49 +02:00
Michael Muré
2ad3aeb6da policy: match is now a method of Policy 2024-10-15 16:53:06 +02:00
Michael Muré
030db7ec0d invocation: fix comment 2024-10-15 15:41:14 +02:00
Michael Muré
aa4ad2fc10 Merge pull request #42 from ucan-wg/fluent-policy
policy: fluent construction
2024-10-15 13:06:56 +02:00
Michael Muré
51e8d5ce04 policy: fluent construction 2024-10-15 13:06:46 +02:00
Michael Muré
59da2d1a2c Merge pull request #43 from ucan-wg/dlg-options
delegation: tune Nbf & Exp options
2024-10-15 10:38:50 +02:00
Michael Muré
88ed55b252 delegation: tune Nbf & Exp options 2024-10-14 20:13:49 +02:00
Michael Muré
9051e5250b Merge pull request #40 from ucan-wg/dlg-example
delegation: make the examples more examply, less testy
2024-10-14 12:52:52 +02:00
Michael Muré
5f2877f0ff delegation: make the examples more examply, less testy 2024-10-14 12:46:28 +02:00
Michael Muré
100a510097 pkg/container: harden the CAR file round-trip with fuzzing 2024-10-09 18:38:35 +02:00
Michael Muré
2a51d61b46 Merge pull request #39 from ucan-wg/command-as-string
command: make the type a string, for easier equality test
2024-10-09 18:30:45 +02:00
Michael Muré
3e3c5a83cc command: make the type a string, for easier equality test 2024-10-09 17:53:05 +02:00
Michael Muré
d60fb71156 Merge pull request #22 from ucan-wg/container
add a token container with serialization as CARv1 file
2024-10-08 11:40:00 +02:00
Michael Muré
40639b6715 container: add readme, remove extra formats, remove go-ipld-cbor dependency 2024-10-07 18:46:19 +02:00
Michael Muré
60922ced96 container: split into reader+writer 2024-10-02 13:43:17 +02:00
Michael Muré
f7b4b48791 container: more experiments 2024-10-02 13:43:17 +02:00
Michael Muré
346efbd31d container: add cbor serialisation 2024-10-02 13:43:17 +02:00
Michael Muré
df9beadf9c add a token container with serialization as CARv1 file 2024-10-02 13:43:16 +02:00
Michael Muré
8615f6c72b Merge pull request #35 from ucan-wg/cid-proposal
token: don't store the CID in the token, symmetric API for sealed
2024-10-02 13:42:06 +02:00
Michael Muré
d9739a3bab token: don't store the CID in the token, symmetric API for sealed 2024-10-02 13:40:29 +02:00
Michael Muré
6b8fbcee0a Merge pull request #34 from ucan-wg/invocation-stub
token: add invocation partial stub
2024-10-02 11:02:32 +02:00
Michael Muré
a7037dbc47 token: add invocation partial stub 2024-10-02 10:53:30 +02:00
Michael Muré
50ea43e3fa Merge pull request #33 from ucan-wg/continuous-bench
gha: add a continuous benchmark action
2024-10-01 17:40:50 +02:00
Michael Muré
0ec16a085c gha: add a continuous benchmark action 2024-10-01 17:28:19 +02:00
Michael Muré
2b45f7630e Merge pull request #32 from ucan-wg/cleanups
Cleanups and Token interface
2024-10-01 17:20:52 +02:00
Michael Muré
637973b10b add a token interface 2024-10-01 17:08:57 +02:00
Michael Muré
bb4725d87c rename tokens to token 2024-10-01 17:02:49 +02:00
Michael Muré
8782554a7b cleanup some obsolete testdata 2024-10-01 16:24:17 +02:00
Michael Muré
a8302ad441 Merge pull request #31 from ucan-wg/read-arbitrary
Read arbitrary
2024-10-01 15:32:47 +02:00
Michael Muré
b4dd8c0757 envelope: fuzz Inspect and FindTag 2024-10-01 15:04:46 +02:00
Michael Muré
4201ab2dca tokens: expose Inspect and FindTag 2024-10-01 14:51:44 +02:00
Michael Muré
1b7059c029 token/read: add the usual collections of readers 2024-10-01 14:17:02 +02:00
Michael Muré
f3a5209cec tokens: read arbitrary token 2024-10-01 13:51:56 +02:00
Steve Moyer
a2822f02c7 feat(envelope): expose an Inspect function 2024-09-30 09:58:08 -04:00
56 changed files with 2818 additions and 718 deletions

34
.github/workflows/bench.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
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

5
go.mod
View File

@@ -1,8 +1,6 @@
module github.com/ucan-wg/go-ucan
go 1.22
toolchain go1.22.4
go 1.23
require (
github.com/ipfs/go-cid v0.4.1
@@ -31,6 +29,7 @@ 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
View File

@@ -80,8 +80,9 @@ 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=

View File

@@ -16,14 +16,12 @@ 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 struct {
segments []string
}
type Command string
// New creates a validated command from the provided list of segment strings.
// An error is returned if an invalid Command would be formed
func New(segments ...string) Command {
return Command{segments: segments}
return Top().Join(segments...)
}
// Parse verifies that the provided string contains the required
@@ -33,20 +31,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 Command{}, ErrRequiresLeadingSlash
return "", ErrRequiresLeadingSlash
}
if len(s) > 1 && strings.HasSuffix(s, "/") {
return Command{}, ErrDisallowsTrailingSlash
return "", ErrDisallowsTrailingSlash
}
if s != strings.ToLower(s) {
return Command{}, ErrRequiresLowercase
return "", ErrRequiresLowercase
}
// The leading slash will result in the first element from strings.Split
// being an empty string which is removed as strings.Join will ignore it.
return Command{strings.Split(s, "/")[1:]}, nil
return Command(s), nil
}
// MustParse is the same as Parse, but panic() if the parsing fail.
@@ -58,14 +56,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 New()
return Command(separator)
}
// IsValid returns true if the provided string is a valid UCAN command.
@@ -77,17 +75,34 @@ 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 {
return Command{append(c.segments, segments...)}
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)
}
// Segments returns the ordered segments that comprise the Command as a
// slice of strings.
func (c Command) Segments() []string {
return c.segments
return strings.Split(string(c), separator)
}
// String returns the composed representation the command. This is also
// the required wire representation (before IPLD encoding occurs.)
func (c Command) String() string {
return "/" + strings.Join(c.segments, "/")
return string(c)
}

View File

@@ -13,73 +13,66 @@ 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.NotNil(t, cmd)
require.NotEmpty(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.Equal(t, command.Command{}, cmd)
require.Zero(t, cmd)
})
}
})
}
func TestEquality(t *testing.T) {
require.True(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/baz"))
require.False(t, command.MustParse("/foo/bar/baz") == command.MustParse("/foo/bar/bazz"))
require.False(t, command.MustParse("/foo/bar") == command.MustParse("/foo/bar/baz"))
}
func TestJoin(t *testing.T) {
require.Equal(t, "/foo", command.Top().Join("foo").String())
require.Equal(t, "/foo/bar", command.Top().Join("foo/bar").String())
require.Equal(t, "/foo/bar", command.Top().Join("foo", "bar").String())
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo/bar").String())
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String())
}
type testcase struct {
name string
inp string

86
pkg/container/Readme.md Normal file
View File

@@ -0,0 +1,86 @@
# Token container
## Why do I need that?
Some common situation asks to package multiple tokens together:
- calling a service requires sending an invocation, alongside the matching delegations
- sending a series of revocations
- \<insert your application specific scenario here>
The UCAN specification defines how a single token is serialized (envelope with signature, IPLD encoded as Dag-cbor), but it's entirely left open how to package multiple tokens together. To be clear, this is a correct thing to do for a specification, as different ways equally valid to solve that problem exists and can coexist. Any wire format holding a list of bytes would do (cbor, json, csv ...).
**go-ucan** however, provide an opinionated implementation, which may or may not work in your situation.
Some experiment has been done over which format is appropriate, and two have been selected:
- **DAG-CBOR** of a list of bytes, as a low overhead option
- **CAR** file, as a somewhat common ways to cary arbitrary blocks of data
Notably, **compression is not included**, even though it does work reasonably well. This is because your transport medium might already do it, or should.
## Wire format consideration
Several possible formats have been explored:
- CAR files (binary or base64)
- DAG-CBOR (binary or base64)
Additionally, gzip and deflate compression has been experimented with.
Below are the results in terms of storage used, as percentage and byte overhead over the raw tokens:
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
| 1 | 15 | 54 | 7 | 42 | 0 | 35 | \-8 | 22 | \-12 | 16 |
| 2 | 12 | 49 | \-12 | 15 | 0 | 34 | \-25 | 0 | \-28 | \-3 |
| 3 | 11 | 48 | \-21 | 4 | 0 | 34 | \-32 | \-10 | \-34 | \-11 |
| 4 | 10 | 47 | \-26 | \-1 | 0 | 34 | \-36 | \-15 | \-37 | \-17 |
| 5 | 10 | 47 | \-28 | \-4 | 0 | 34 | \-38 | \-18 | \-40 | \-20 |
| 6 | 10 | 47 | \-30 | \-7 | 0 | 34 | \-40 | \-20 | \-40 | \-20 |
| 7 | 10 | 46 | \-31 | \-8 | 0 | 34 | \-41 | \-21 | \-42 | \-22 |
| 8 | 9 | 46 | \-32 | \-10 | 0 | 34 | \-42 | \-22 | \-42 | \-23 |
| 9 | 9 | 46 | \-33 | \-11 | 0 | 34 | \-43 | \-23 | \-43 | \-24 |
| 10 | 9 | 46 | \-34 | \-12 | 0 | 34 | \-43 | \-25 | \-44 | \-25 |
![Overhead %](img/overhead_percent.png)
| Token count | car | carBase64 | carGzip | carGzipBase64 | cbor | cborBase64 | cborGzip | cborGzipBase64 | cborFlate | cborFlateBase64 |
|-------------|-----|-----------|---------|---------------|------|------------|----------|----------------|-----------|-----------------|
| 1 | 64 | 226 | 29 | 178 | 4 | 146 | \-35 | 94 | \-52 | 70 |
| 2 | 102 | 412 | \-107 | 128 | 7 | 288 | \-211 | 0 | \-234 | \-32 |
| 3 | 140 | 602 | \-270 | 58 | 10 | 430 | \-405 | \-126 | \-429 | \-146 |
| 4 | 178 | 792 | \-432 | \-28 | 13 | 572 | \-602 | \-252 | \-617 | \-288 |
| 5 | 216 | 978 | \-582 | \-94 | 16 | 714 | \-805 | \-386 | \-839 | \-418 |
| 6 | 254 | 1168 | \-759 | \-176 | 19 | 856 | \-1001 | \-508 | \-1018 | \-520 |
| 7 | 292 | 1358 | \-908 | \-246 | 22 | 998 | \-1204 | \-634 | \-1229 | \-650 |
| 8 | 330 | 1544 | \-1085 | \-332 | 25 | 1140 | \-1398 | \-756 | \-1423 | \-792 |
| 9 | 368 | 1734 | \-1257 | \-414 | 28 | 1282 | \-1614 | \-894 | \-1625 | \-930 |
| 10 | 406 | 1924 | \-1408 | \-508 | 31 | 1424 | \-1804 | \-1040 | \-1826 | \-1060 |
![img.png](img/overhead_bytes.png)
Following is the performance aspect, with CPU usage and memory allocation:
| | Write ns/op | Read ns/op | Write B/op | Read B/op | Write allocs/op | Read allocs/op |
|-----------------|-------------|------------|------------|-----------|-----------------|----------------|
| car | 8451 | 1474630 | 17928 | 149437 | 59 | 2631 |
| carBase64 | 16750 | 1437678 | 24232 | 151502 | 61 | 2633 |
| carGzip | 320253 | 1581412 | 823887 | 192272 | 76 | 2665 |
| carGzipBase64 | 343305 | 1486269 | 828782 | 198543 | 77 | 2669 |
| cbor | 6419 | 1301554 | 16368 | 138891 | 25 | 2534 |
| cborBase64 | 12860 | 1386728 | 20720 | 140962 | 26 | 2536 |
| cborGzip | 310106 | 1379146 | 822742 | 182003 | 42 | 2585 |
| cborGzipBase64 | 317001 | 1462548 | 827640 | 189283 | 43 | 2594 |
| cborFlate | 327112 | 1555007 | 822473 | 181537 | 40 | 2591 |
| cborFlateBase64 | 311276 | 1456562 | 826042 | 188665 | 41 | 2596 |
(BEWARE: logarithmic scale)
![img.png](img/cpu.png)
![img_1.png](img/alloc_byte.png)
![img_2.png](img/alloc_count.png)
Conclusion:
- CAR files are heavy for this usage, notably because they carry the CIDs of the tokens
- compression works quite well and warrants its usage even with a single token
- DAG-CBOR outperform CAR files everywhere, and comes with a tiny ~3 bytes per token overhead.
**Formats beside DAG-CBOR and CAR, with or without base64, have been removed. They are in the git history though.**

263
pkg/container/car.go Normal file
View File

@@ -0,0 +1,263 @@
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)
}

78
pkg/container/car_test.go Normal file
View File

@@ -0,0 +1,78 @@
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.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
pkg/container/img/cpu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

122
pkg/container/reader.go Normal file
View File

@@ -0,0 +1,122 @@
package container
import (
"encoding/base64"
"fmt"
"io"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/token"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"
)
var ErrNotFound = fmt.Errorf("not found")
// Reader is a token container reader. It exposes the tokens conveniently decoded.
type Reader map[cid.Cid]token.Token
// GetToken returns an arbitrary decoded token, from its CID.
// If not found, ErrNotFound is returned.
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
tkn, ok := ctn[cid]
if !ok {
return nil, ErrNotFound
}
return tkn, nil
}
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
tkn, err := ctn.GetToken(cid)
if err != nil {
return nil, err
}
if tkn, ok := tkn.(*delegation.Token); ok {
return tkn, nil
}
return nil, fmt.Errorf("not a delegation token")
}
// GetInvocation returns the first found invocation.Token.
// If none are found, ErrNotFound is returned.
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
for _, t := range ctn {
if inv, ok := t.(*invocation.Token); ok {
return inv, nil
}
}
return nil, ErrNotFound
}
func FromCar(r io.Reader) (Reader, error) {
_, it, err := readCar(r)
if err != nil {
return nil, err
}
ctn := make(Reader)
for block, err := range it {
if err != nil {
return nil, err
}
err = ctn.addToken(block.data)
if err != nil {
return nil, err
}
}
return ctn, nil
}
func FromCarBase64(r io.Reader) (Reader, error) {
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
}
func FromCbor(r io.Reader) (Reader, error) {
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
if err != nil {
return nil, err
}
if n.Kind() != datamodel.Kind_List {
return nil, fmt.Errorf("not a list")
}
ctn := make(Reader, n.Length())
it := n.ListIterator()
for !it.Done() {
_, val, err := it.Next()
if err != nil {
return nil, err
}
data, err := val.AsBytes()
if err != nil {
return nil, err
}
err = ctn.addToken(data)
if err != nil {
return nil, err
}
}
return ctn, nil
}
func FromCborBase64(r io.Reader) (Reader, error) {
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
}
func (ctn Reader) addToken(data []byte) error {
tkn, c, err := token.FromSealed(data)
if err != nil {
return err
}
ctn[c] = tkn
return nil
}

View File

@@ -0,0 +1,184 @@
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 Normal file

Binary file not shown.

61
pkg/container/writer.go Normal file
View File

@@ -0,0 +1,61 @@
package container
import (
"encoding/base64"
"io"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
)
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
type Writer map[cid.Cid][]byte
func NewWriter() Writer {
return make(Writer)
}
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
ctn[cid] = data
}
func (ctn Writer) ToCar(w io.Writer) error {
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
for c, bytes := range ctn {
if !yield(carBlock{c: c, data: bytes}, nil) {
return
}
}
})
}
func (ctn Writer) ToCarBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCar(w2)
}
func (ctn Writer) ToCbor(w io.Writer) error {
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
for _, bytes := range ctn {
qp.ListEntry(la, qp.Bytes(bytes))
}
})
if err != nil {
return err
}
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
}
func (ctn Writer) ToCborBase64(w io.Writer) error {
w2 := base64.NewEncoder(base64.StdEncoding, w)
defer w2.Close()
return ctn.ToCbor(w2)
}

View File

@@ -4,10 +4,12 @@ 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")
@@ -123,6 +125,41 @@ 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

View File

@@ -61,7 +61,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
if err != nil {
return nil, err
}
return Not(statement), nil
return negation{statement: 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))
}
res, err := Like(sel, must.String(pattern))
g, err := parseGlob(must.String(pattern))
if err != nil {
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
}
return res, nil
return wildcard{selector: sel, pattern: g}, nil
case KindAll, KindAny:
sel, err := arg2AsSelector(op)

View File

@@ -1,20 +1,13 @@
// 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)
@@ -45,6 +38,16 @@ 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()

View File

@@ -7,13 +7,11 @@ 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 matches the policy document.
func Match(policy Policy, node ipld.Node) bool {
for _, stmt := range policy {
// Match determines if the IPLD node satisfies the policy.
func (p Policy) Match(node datamodel.Node) bool {
for _, stmt := range p {
ok := matchStatement(stmt, node)
if !ok {
return false
@@ -22,30 +20,37 @@ func Match(policy Policy, node ipld.Node) bool {
return true
}
// Filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path
func (p Policy) Filter(path ...string) Policy {
var filtered Policy
for _, stmt := range p {
newChild, remain := filter(stmt, path)
if newChild != nil && len(remain) == 0 {
filtered = append(filtered, newChild)
}
}
return filtered
}
func matchStatement(statement Statement, node ipld.Node) bool {
switch statement.Kind() {
case KindEqual:
if s, ok := statement.(equality); ok {
one, many, err := selector.Select(s.selector, node)
one, _, err := s.selector.Select(node)
if err != nil {
return false
}
if one != nil {
return datamodel.DeepEqual(s.value, one)
}
if many != nil {
for _, n := range many {
if eq := datamodel.DeepEqual(s.value, n); eq {
return true
}
}
}
return false
}
case KindGreaterThan:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
one, _, err := s.selector.Select(node)
if err != nil || one == nil {
return false
}
@@ -53,7 +58,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
}
case KindGreaterThanOrEqual:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
one, _, err := s.selector.Select(node)
if err != nil || one == nil {
return false
}
@@ -61,7 +66,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
}
case KindLessThan:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
one, _, err := s.selector.Select(node)
if err != nil || one == nil {
return false
}
@@ -69,7 +74,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
}
case KindLessThanOrEqual:
if s, ok := statement.(equality); ok {
one, _, err := selector.Select(s.selector, node)
one, _, err := s.selector.Select(node)
if err != nil || one == nil {
return false
}
@@ -104,7 +109,7 @@ func matchStatement(statement Statement, node ipld.Node) bool {
}
case KindLike:
if s, ok := statement.(wildcard); ok {
one, _, err := selector.Select(s.selector, node)
one, _, err := s.selector.Select(node)
if err != nil || one == nil {
return false
}
@@ -116,31 +121,72 @@ func matchStatement(statement Statement, node ipld.Node) bool {
}
case KindAll:
if s, ok := statement.(quantifier); ok {
_, many, err := selector.Select(s.selector, node)
if err != nil || many == nil {
one, many, err := s.selector.Select(node)
if err != nil {
return false
}
for _, n := range many {
ok := matchStatement(s.statement, n)
if !ok {
return false
if one != nil {
it := one.ListIterator()
if it != nil {
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return false
}
ok := matchStatement(s.statement, v)
if !ok {
return false
}
}
} else {
ok := matchStatement(s.statement, one)
if !ok {
return false
}
}
}
if len(many) > 0 {
for _, n := range many {
ok := matchStatement(s.statement, n)
if !ok {
return false
}
}
}
return true
}
case KindAny:
if s, ok := statement.(quantifier); ok {
one, many, err := selector.Select(s.selector, node)
one, many, err := s.selector.Select(node)
if err != nil {
return false
}
if one != nil {
ok := matchStatement(s.statement, one)
if ok {
return true
it := one.ListIterator()
if it != nil {
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return false
}
ok := matchStatement(s.statement, v)
if ok {
return true
}
}
} else {
ok := matchStatement(s.statement, one)
if ok {
return true
}
}
}
if many != nil {
if len(many) > 0 {
for _, n := range many {
ok := matchStatement(s.statement, n)
if ok {
@@ -155,6 +201,70 @@ 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)

View File

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

View File

@@ -3,9 +3,13 @@ package policy
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
import (
"github.com/ipld/go-ipld-prime"
"fmt"
"strings"
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagjson"
selpkg "github.com/ucan-wg/go-ucan/pkg/policy/selector"
)
const (
@@ -24,13 +28,43 @@ 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
}
type equality struct {
kind string
selector selector.Selector
selector selpkg.Selector
value ipld.Node
}
@@ -38,24 +72,47 @@ func (e equality) Kind() string {
return e.kind
}
func Equal(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindEqual, selector: selector, value: value}
func (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 GreaterThan(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindGreaterThan, selector: selector, value: value}
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 GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindGreaterThanOrEqual, 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 LessThan(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindLessThan, 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 LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
return equality{kind: KindLessThanOrEqual, 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 LessThanOrEqual(selector string, value ipld.Node) Constructor {
return func() (Statement, error) {
sel, err := selpkg.Parse(selector)
return equality{kind: KindLessThanOrEqual, selector: sel, value: value}, err
}
}
type negation struct {
@@ -66,8 +123,16 @@ func (n negation) Kind() string {
return KindNot
}
func Not(stmt Statement) Statement {
return negation{statement: stmt}
func (n negation) String() string {
child := n.statement.String()
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
}
func Not(cstor Constructor) Constructor {
return func() (Statement, error) {
stmt, err := cstor()
return negation{statement: stmt}, err
}
}
type connective struct {
@@ -79,16 +144,36 @@ func (c connective) Kind() string {
return c.kind
}
func And(stmts ...Statement) Statement {
return connective{kind: KindAnd, statements: stmts}
func (c connective) String() string {
childs := make([]string, len(c.statements))
for i, statement := range c.statements {
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
}
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
}
func Or(stmts ...Statement) Statement {
return connective{kind: KindOr, statements: stmts}
func And(cstors ...Constructor) Constructor {
return func() (Statement, error) {
stmts, err := assemble(cstors)
if err != nil {
return nil, err
}
return connective{kind: KindAnd, statements: stmts}, nil
}
}
func Or(cstors ...Constructor) Constructor {
return func() (Statement, error) {
stmts, err := assemble(cstors)
if err != nil {
return nil, err
}
return connective{kind: KindOr, statements: stmts}, nil
}
}
type wildcard struct {
selector selector.Selector
selector selpkg.Selector
pattern glob
}
@@ -96,18 +181,24 @@ func (n wildcard) Kind() string {
return KindLike
}
func Like(selector selector.Selector, pattern string) (Statement, error) {
g, err := parseGlob(pattern)
if err != nil {
return nil, err
}
func (n wildcard) String() string {
return fmt.Sprintf(`["%s", "%s", "%s"]`, n.Kind(), n.selector, n.pattern)
}
return wildcard{selector: selector, pattern: g}, nil
func Like(selector string, pattern string) Constructor {
return func() (Statement, error) {
g, err := parseGlob(pattern)
if err != nil {
return nil, err
}
sel, err := selpkg.Parse(selector)
return wildcard{selector: sel, pattern: g}, err
}
}
type quantifier struct {
kind string
selector selector.Selector
selector selpkg.Selector
statement Statement
}
@@ -115,10 +206,41 @@ func (n quantifier) Kind() string {
return n.kind
}
func All(selector selector.Selector, statement Statement) Statement {
return quantifier{kind: KindAll, selector: selector, statement: statement}
func (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 Any(selector selector.Selector, statement Statement) Statement {
return quantifier{kind: KindAny, selector: selector, statement: statement}
func All(selector string, cstor Constructor) Constructor {
return func() (Statement, error) {
stmt, err := cstor()
if err != nil {
return nil, err
}
sel, err := selpkg.Parse(selector)
return quantifier{kind: KindAll, selector: sel, statement: stmt}, err
}
}
func Any(selector string, cstor Constructor) Constructor {
return func() (Statement, error) {
stmt, err := cstor()
if err != nil {
return nil, err
}
sel, err := selpkg.Parse(selector)
return quantifier{kind: KindAny, selector: sel, statement: stmt}, err
}
}
func assemble(cstors []Constructor) ([]Statement, error) {
stmts := make([]Statement, 0, len(cstors))
for _, cstor := range cstors {
stmt, err := cstor()
if err != nil {
return nil, err
}
stmts = append(stmts, stmt)
}
return stmts, nil
}

60
pkg/policy/policy_test.go Normal file
View File

@@ -0,0 +1,60 @@
package policy_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
)
func ExamplePolicy() {
pol := policy.MustConstruct(
policy.Equal(".status", literal.String("draft")),
policy.All(".reviewer",
policy.Like(".email", "*@example.com"),
),
policy.Any(".tags", policy.Or(
policy.Equal(".", literal.String("news")),
policy.Equal(".", literal.String("press")),
)),
)
fmt.Println(pol)
// Output:
// [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ]
}
func TestConstruct(t *testing.T) {
pol, err := policy.Construct(
policy.Equal(".status", literal.String("draft")),
policy.All(".reviewer",
policy.Like(".email", "*@example.com"),
),
)
require.NoError(t, err)
require.NotNil(t, pol)
// check if errors cascade correctly
pol, err = policy.Construct(
policy.Equal(".status", literal.String("draft")),
policy.All(".reviewer", policy.Or(
policy.Like(".email", "*@example.com"),
policy.Like(".", "\\"), // invalid pattern
)),
)
require.Error(t, err)
require.Nil(t, pol)
}

View File

@@ -2,10 +2,18 @@ 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, "")
@@ -13,6 +21,9 @@ 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
@@ -27,9 +38,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, Identity)
sel = append(sel, segment{str: ".", identity: true})
case "[]":
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
default:
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
lookup := seg[1 : len(seg)-1]

View File

@@ -2,7 +2,6 @@ package selector
import (
"fmt"
"regexp"
"strings"
"github.com/ipld/go-ipld-prime"
@@ -15,6 +14,18 @@ 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 {
@@ -23,14 +34,6 @@ func (s Selector) String() string {
return res.String()
}
var Identity = segment{".", true, false, false, nil, "", 0}
var (
indexRegex = regexp.MustCompile(`^-?\d+$`)
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
)
type segment struct {
str string
identity bool
@@ -76,12 +79,6 @@ 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 {
@@ -318,7 +315,7 @@ func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.No
}
}
default:
default: // Index()
at = append(at, fmt.Sprintf("%d", seg.Index()))
if cur == nil {
if seg.Optional() {
@@ -380,6 +377,39 @@ 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,

View File

@@ -313,7 +313,7 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".")
require.NoError(t, err)
one, many, err := Select(sel, anode)
one, many, err := sel.Select(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 := Select(sel, anode)
one, many, err := sel.Select(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 = Select(sel, bnode)
one, many, err = sel.Select(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 := Select(sel, anode)
one, many, err := sel.Select(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 = Select(sel, bnode)
one, many, err = sel.Select(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 := Select(sel, anode)
one, many, err := sel.Select(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 := Select(sel, anode)
one, many, err := sel.Select(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 := Select(sel, anode)
one, many, err := sel.Select(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 := Select(sel, anode)
one, many, err := sel.Select(anode)
require.NoError(t, err)
require.Empty(t, one)
require.NotEmpty(t, many)
@@ -431,6 +431,32 @@ 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]`,
@@ -494,6 +520,6 @@ func FuzzParseAndSelect(f *testing.F) {
}
// look for panic()
_, _, _ = Select(sel, node)
_, _, _ = sel.Select(node)
})
}

View File

@@ -52,7 +52,7 @@ func TestSupportedForms(t *testing.T) {
require.NoError(t, err)
// attempt to select
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
node, nodes, err := sel.Select(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 := selector.Select(sel, makeNode(t, tc.Input))
node, nodes, err := sel.Select(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 := selector.Select(sel, makeNode(t, tc.Input))
node, nodes, err := sel.Select(makeNode(t, tc.Input))
require.Error(t, err)
assert.Nil(t, node)
assert.Empty(t, nodes)

View File

@@ -15,7 +15,6 @@ import (
"fmt"
"time"
"github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
@@ -44,8 +43,6 @@ 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.
@@ -67,7 +64,6 @@ 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 {
@@ -156,13 +152,6 @@ 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

View File

@@ -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/tokens/delegation"
"github.com/ucan-wg/go-ucan/token/delegation"
)
const (

View File

@@ -0,0 +1,331 @@
package delegation_test
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"time"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
// The following example shows how to create a delegation.Token with
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
func ExampleNew() {
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
printThenPanicOnErr(err)
issDid, err := did.FromPubKey(issPub)
printThenPanicOnErr(err)
fmt.Println("issDid:", issDid)
audDid := did.MustParse(AudienceDID)
subDid := did.MustParse(subjectDID)
// The command defines the shape of the arguments that will be evaluated against the policy
cmd := command.MustParse("/foo/bar")
// The policy defines what is allowed to do.
pol := policy.MustConstruct(
policy.Equal(".status", literal.String("draft")),
policy.All(".reviewer",
policy.Like(".email", "*@example.com"),
),
policy.Any(".tags", policy.Or(
policy.Equal(".", literal.String("news")),
policy.Equal(".", literal.String("press")),
)),
)
tkn, err := delegation.New(issPriv, audDid, cmd, pol,
delegation.WithSubject(subDid),
delegation.WithExpirationIn(time.Hour),
delegation.WithNotBeforeIn(time.Minute),
delegation.WithMeta("foo", "bar"),
delegation.WithMeta("baz", 123),
)
printThenPanicOnErr(err)
// "Seal", meaning encode and wrap into a signed envelope.
data, id, err := tkn.ToSealed(issPriv)
printThenPanicOnErr(err)
printCIDAndSealed(id, data)
// Example output:
//
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe
//
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW
//
// DAG-CBOR (base64) out: glhA5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDWzqY2lzc3g4ZGlkOmtleTp6Nk1raFZGem5QZVI1NzJyVEs1MVVqb1ROcG5GOGN4dVdmUG05b0JNUHI3eThBQmVjbmJmGmcNXxZjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3RBMXVCZENwcTR1SkJxRTlqak1pTHl4WkJnOWE2eGdQUEtKak1xc3M2WmMyZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMu0HMgJ5Y+M84I/66
//
// Converted to DAG-JSON out:
// [
// {
// "/": {
// "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
// }
// },
// {
// "h": {
// "/": {
// "bytes": "NO0BcQ"
// }
// },
// "ucan/dlg@1.0.0-rc.1": {
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
// "cmd": "/foo/bar",
// "exp": 1728933098,
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
// "meta": {
// "baz": 123,
// "foo": "bar"
// },
// "nbf": 1728929558,
// "nonce": {
// "/": {
// "bytes": "u0HMgJ5Y+M84I/66"
// }
// },
// "pol": [
// [
// "==",
// ".status",
// "draft"
// ],
// [
// "all",
// ".reviewer",
// [
// "like",
// ".email",
// "*@example.com"
// ]
// ],
// [
// "any",
// ".tags",
// [
// "or",
// [
// [
// "==",
// ".",
// "news"
// ],
// [
// "==",
// ".",
// "press"
// ]
// ]
// ]
// ]
// ],
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
// }
// }
// ]
}
// The following example shows how to create a UCAN root delegation.Token
// - a delegation.Token with the subject (sub) set to the value of issuer
// (iss).
func ExampleRoot() {
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
printThenPanicOnErr(err)
issDid, err := did.FromPubKey(issPub)
printThenPanicOnErr(err)
fmt.Println("issDid:", issDid)
audDid := did.MustParse(AudienceDID)
// The command defines the shape of the arguments that will be evaluated against the policy
cmd := command.MustParse("/foo/bar")
// The policy defines what is allowed to do.
pol := policy.MustConstruct(
policy.Equal(".status", literal.String("draft")),
policy.All(".reviewer",
policy.Like(".email", "*@example.com"),
),
policy.Any(".tags", policy.Or(
policy.Equal(".", literal.String("news")),
policy.Equal(".", literal.String("press")),
)),
)
tkn, err := delegation.Root(issPriv, audDid, cmd, pol,
delegation.WithExpirationIn(time.Hour),
delegation.WithNotBeforeIn(time.Minute),
delegation.WithMeta("foo", "bar"),
delegation.WithMeta("baz", 123),
)
printThenPanicOnErr(err)
// "Seal", meaning encode and wrap into a signed envelope.
data, id, err := tkn.ToSealed(issPriv)
printThenPanicOnErr(err)
printCIDAndSealed(id, data)
// Example output:
//
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
//
// CID (base58BTC): zdpuAwLojgfvFCbjz2FsKrvN1khDQ9mFGT6b6pxjMfz73Roed
//
// DAG-CBOR (base64) out: glhA6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDW0wY2lzc3g4ZGlkOmtleTp6Nk1rbldKcXoxN1k0QWZzWFNKVUZLb211QlI0R1RrVmlNN2tKWXV0elRNa0N5RkZjbmJmGmcNX1xjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa25XSnF6MTdZNEFmc1hTSlVGS29tdUJSNEdUa1ZpTTdrSll1dHpUTWtDeUZGZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMJOsjYi1Pq3OIB0La
//
// Converted to DAG-JSON out:
// [
// {
// "/": {
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
// }
// },
// {
// "h": {
// "/": {
// "bytes": "NO0BcQ"
// }
// },
// "ucan/dlg@1.0.0-rc.1": {
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
// "cmd": "/foo/bar",
// "exp": 1728933168,
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
// "meta": {
// "baz": 123,
// "foo": "bar"
// },
// "nbf": 1728929628,
// "nonce": {
// "/": {
// "bytes": "JOsjYi1Pq3OIB0La"
// }
// },
// "pol": [
// [
// "==",
// ".status",
// "draft"
// ],
// [
// "all",
// ".reviewer",
// [
// "like",
// ".email",
// "*@example.com"
// ]
// ],
// [
// "any",
// ".tags",
// [
// "or",
// [
// [
// "==",
// ".",
// "news"
// ],
// [
// "==",
// ".",
// "press"
// ]
// ]
// ]
// ]
// ],
// "sub": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF"
// }
// }
// ]
}
// The following example demonstrates how to get a delegation.Token from
// a DAG-CBOR []byte.
func ExampleToken_FromSealed() {
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
printThenPanicOnErr(err)
tkn, c, err := delegation.FromSealed(cborBytes)
printThenPanicOnErr(err)
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(c))
fmt.Println("Issuer (iss):", tkn.Issuer().String())
fmt.Println("Audience (aud):", tkn.Audience().String())
fmt.Println("Subject (sub):", tkn.Subject().String())
fmt.Println("Command (cmd):", tkn.Command().String())
fmt.Println("Policy (pol):", tkn.Policy().String())
fmt.Println("Nonce (nonce):", hex.EncodeToString(tkn.Nonce()))
fmt.Println("Meta (meta):", tkn.Meta().String())
fmt.Println("NotBefore (nbf):", tkn.NotBefore())
fmt.Println("Expiration (exp):", tkn.Expiration())
// Output:
// CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
// Issuer (iss): did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2
// Audience (aud): did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv
// Subject (sub): did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2
// Command (cmd): /foo/bar
// Policy (pol): [
// ["==", ".status", "draft"],
// ["all", ".reviewer",
// ["like", ".email", "*@example.com"]],
// ["any", ".tags",
// ["or", [
// ["==", ".", "news"],
// ["==", ".", "press"]]]
// ]
// ]
// Nonce (nonce): 000102030405060708090a0b
// Meta (meta): {}
// NotBefore (nbf): <nil>
// Expiration (exp): <nil>
}
func printCIDAndSealed(id cid.Cid, data []byte) {
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(id))
fmt.Println("DAG-CBOR (base64) out:", base64.StdEncoding.EncodeToString(data))
fmt.Println("Converted to DAG-JSON out:")
node, err := ipld.Decode(data, dagcbor.Decode)
printThenPanicOnErr(err)
rawJSON, err := ipld.Encode(node, dagjson.Encode)
printThenPanicOnErr(err)
prettyJSON := &bytes.Buffer{}
err = json.Indent(prettyJSON, rawJSON, "", "\t")
printThenPanicOnErr(err)
fmt.Println(prettyJSON.String())
}
func printThenPanicOnErr(err error) {
if err != nil {
panic(err)
}
}

View File

@@ -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/tokens/internal/envelope"
"github.com/ucan-wg/go-ucan/token/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 Seal but accepts an io.Writer.
// 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)
@@ -47,42 +47,38 @@ 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, error) {
func FromSealed(data []byte) (*Token, cid.Cid, error) {
tkn, err := FromDagCbor(data)
if err != nil {
return nil, err
return nil, cid.Undef, err
}
id, err := envelope.CIDFromBytes(data)
if err != nil {
return nil, err
return nil, cid.Undef, err
}
tkn.cid = id
return tkn, nil
return tkn, id, nil
}
// FromSealedReader is the same as Unseal but accepts an io.Reader.
func FromSealedReader(r io.Reader) (*Token, error) {
func FromSealedReader(r io.Reader) (*Token, cid.Cid, error) {
cidReader := envelope.NewCIDReader(r)
tkn, err := FromDagCborReader(cidReader)
if err != nil {
return nil, err
return nil, cid.Undef, err
}
id, err := cidReader.CID()
if err != nil {
return nil, err
return nil, cid.Undef, err
}
tkn.cid = id
return tkn, nil
return tkn, id, nil
}
// Encode marshals a View to the format specified by the provided
// 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)
@@ -93,7 +89,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 {
@@ -103,37 +99,37 @@ func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.En
return ipld.EncodeStreaming(w, node, encFn)
}
// ToDagCbor marshals the View to the DAG-CBOR format.
// 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.
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
return t.EncodeWriter(w, privKey, dagcbor.Encode)
}
// ToDagJson marshals the View to the DAG-JSON format.
// 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.
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
return t.EncodeWriter(w, privKey, dagjson.Encode)
}
// Decode unmarshals the input data using the format specified by the
// provided codec.Decoder into a View.
// provided codec.Decoder into a Token.
//
// An error is returned if the conversion fails, or if the resulting
// View is invalid.
// 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)
return FromIPLD(node)
}
// DecodeReader is the same as Decode, but accept an io.Reader.
@@ -142,13 +138,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 View.
// FromDagCbor unmarshals the input data into a Token.
//
// An error is returned if the conversion fails, or if the resulting
// View is invalid.
// Token is invalid.
func FromDagCbor(data []byte) (*Token, error) {
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
if err != nil {
@@ -168,10 +164,10 @@ func FromDagCborReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagcbor.Decode)
}
// FromDagJson unmarshals the input data into a View.
// FromDagJson unmarshals the input data into a Token.
//
// An error is returned if the conversion fails, or if the resulting
// View is invalid.
// Token is invalid.
func FromDagJson(data []byte) (*Token, error) {
return Decode(data, dagjson.Decode)
}
@@ -181,7 +177,8 @@ func FromDagJsonReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagjson.Decode)
}
func fromIPLD(node datamodel.Node) (*Token, error) {
// 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

View File

@@ -24,6 +24,15 @@ 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.
@@ -48,11 +57,21 @@ 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 {

View File

@@ -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/tokens/internal/envelope"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
// [Tag] is the string used as a key within the SigPayload that identifies

View File

@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/tokens/delegation"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/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, err := delegation.FromSealed(cborBytes)
p2, c2, err := delegation.FromSealed(cborBytes)
require.NoError(t, err)
assert.Equal(t, id, p2.CID())
assert.Equal(t, id, c2)
fmt.Println("read Cbor", p2)
readJson, err := p2.ToDagJson(privKey)
@@ -70,10 +70,9 @@ func TestSchemaRoundTrip(t *testing.T) {
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
// buf = bytes.NewBuffer(cborBytes.Bytes())
p2, err := delegation.FromSealedReader(cborBytes)
p2, c2, err := delegation.FromSealedReader(cborBytes)
require.NoError(t, err)
t.Log(len(p2.CID().Bytes()), p2.CID().Bytes())
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(p2.CID()))
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(c2))
readJson := &bytes.Buffer{}
require.NoError(t, p2.ToDagJsonWriter(readJson, privKey))
@@ -96,7 +95,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()
@@ -117,7 +116,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)
}
})
@@ -134,7 +133,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()
@@ -161,7 +160,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)
}
})

19
token/inspect.go Normal file
View File

@@ -0,0 +1,19 @@
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)
}

41
token/interface.go Normal file
View File

@@ -0,0 +1,41 @@
package token
import (
"io"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/codec"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/meta"
)
type Token interface {
Marshaller
// Issuer returns the did.DID representing the Token's issuer.
Issuer() did.DID
// Meta returns the Token's metadata.
Meta() *meta.Meta
}
type Marshaller interface {
// ToSealed wraps the token in an envelope, generates the signature, encodes
// the result to DAG-CBOR and calculates the CID of the resulting binary data.
ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error)
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error)
// Encode marshals a Token to the format specified by the provided codec.Encoder.
Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error)
// EncodeWriter is the same as Encode, but accepts an io.Writer.
EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error
// ToDagCbor marshals the Token to the DAG-CBOR format.
ToDagCbor(privKey crypto.PrivKey) ([]byte, error)
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error
// ToDagJson marshals the Token to the DAG-JSON format.
ToDagJson(privKey crypto.PrivKey) ([]byte, error)
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
func TestCidFromBytes(t *testing.T) {

View File

@@ -16,8 +16,9 @@ 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 (

View File

@@ -27,7 +27,9 @@ package envelope
import (
"errors"
"fmt"
"io"
"strings"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec"
@@ -41,10 +43,13 @@ import (
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
"github.com/ucan-wg/go-ucan/token/internal/varsig"
)
const varsigHeaderKey = "h"
const (
VarsigHeaderKey = "h"
UCANTagPrefix = "ucan/"
)
// Tokener must be implemented by types that wish to be enclosed in a
// UCAN Envelope (presumbably one of the UCAN token types).
@@ -89,19 +94,7 @@ 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) {
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
return Decode[T](b, dagcbor.Decode)
}
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
@@ -127,113 +120,81 @@ 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) {
undef := *new(T)
zero := *new(T)
tkn, err := fromIPLD[T](node)
info, err := Inspect(node)
if err != nil {
return undef, err
return zero, err
}
return tkn, nil
}
func fromIPLD[T Tokener](node datamodel.Node) (T, error) {
undef := *new(T)
signatureNode, err := node.LookupByIndex(0)
if err != nil {
return undef, err
}
signature, err := signatureNode.AsBytes()
if err != nil {
return undef, err
}
sigPayloadNode, err := node.LookupByIndex(1)
if err != nil {
return undef, err
}
varsigHeaderNode, err := sigPayloadNode.LookupByString(varsigHeaderKey)
if err != nil {
return undef, err
}
tokenPayloadNode, err := sigPayloadNode.LookupByString(undef.Tag())
if err != nil {
return undef, err
if info.Tag != zero.Tag() {
return zero, errors.New("data doesn't match the expected type")
}
// This needs to be done before converting this node to its schema
// representation (afterwards, the field might be renamed os it's safer
// to use the wire name).
issuerNode, err := tokenPayloadNode.LookupByString("iss")
issuerNode, err := info.tokenPayloadNode.LookupByString("iss")
if err != nil {
return undef, err
return zero, err
}
// Replaces the datamodel.Node in tokenPayloadNode with a
// schema.TypedNode so that we can cast it to a *token.Token after
// unwrapping it.
nb := undef.Prototype().Representation().NewBuilder()
nb := zero.Prototype().Representation().NewBuilder()
err = nb.AssignNode(tokenPayloadNode)
err = nb.AssignNode(info.tokenPayloadNode)
if err != nil {
return undef, err
return zero, err
}
tokenPayloadNode = nb.Build()
tokenPayloadNode := nb.Build()
tokenPayload := bindnode.Unwrap(tokenPayloadNode)
if tokenPayload == nil {
return undef, errors.New("failed to Unwrap the TokenPayload")
return zero, errors.New("failed to Unwrap the TokenPayload")
}
tkn, ok := tokenPayload.(T)
if !ok {
return undef, errors.New("failed to assert the TokenPayload type as *token.Token")
return zero, errors.New("failed to assert the TokenPayload type as *token.Token")
}
// Check that the issuer's DID contains a public key with a type that
// matches the VarsigHeader and then verify the SigPayload.
issuer, err := issuerNode.AsString()
if err != nil {
return undef, err
return zero, err
}
issuerDID, err := did.Parse(issuer)
if err != nil {
return undef, err
return zero, err
}
issuerPubKey, err := issuerDID.PubKey()
if err != nil {
return undef, err
return zero, err
}
issuerVarsigHeader, err := varsig.Encode(issuerPubKey.Type())
if err != nil {
return undef, err
return zero, err
}
varsigHeader, err := varsigHeaderNode.AsBytes()
if string(info.VarsigHeader) != string(issuerVarsigHeader) {
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
}
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
if err != nil {
return undef, err
return zero, err
}
if string(varsigHeader) != string(issuerVarsigHeader) {
return undef, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
}
data, err := ipld.Encode(sigPayloadNode, dagcbor.Encode)
if err != nil {
return undef, err
}
ok, err = issuerPubKey.Verify(data, signature)
ok, err = issuerPubKey.Verify(data, info.Signature)
if err != nil || !ok {
return undef, errors.New("failed to verify the token's signature")
return zero, errors.New("failed to verify the token's signature")
}
return tkn, nil
@@ -293,7 +254,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))
})
@@ -312,3 +273,121 @@ 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
}

View File

@@ -3,12 +3,17 @@ 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) {
@@ -149,3 +154,56 @@ 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()
}
})
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/stretchr/testify/assert"
"github.com/ucan-wg/go-ucan/tokens/internal/varsig"
"github.com/ucan-wg/go-ucan/token/internal/varsig"
)
func TestDecode(t *testing.T) {

View File

@@ -0,0 +1,124 @@
// Package invocation implements the UCAN [invocation] specification with
// an immutable Token type as well as methods to convert the Token to and
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
// should most commonly be used for transport and storage.
//
// [envelope]: https://github.com/ucan-wg/spec#envelope
// [invocation]: https://github.com/ucan-wg/invocation
package invocation
import (
"crypto/rand"
"errors"
"fmt"
"time"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/meta"
)
// Token is an immutable type that holds the fields of a UCAN invocation.
type Token struct {
// Issuer DID (invoker)
issuer did.DID
// Audience DID (receiver/executor)
audience did.DID
// Subject DID (subject being invoked)
subject did.DID
// The Command to invoke
command command.Command
// TODO: args
// TODO: prf
// A unique, random nonce
nonce []byte
// Arbitrary Metadata
meta *meta.Meta
// The timestamp at which the Invocation becomes invalid
expiration *time.Time
// The timestamp at which the Invocation was created
invokedAt *time.Time
// TODO: cause
}
// Issuer returns the did.DID representing the Token's issuer.
func (t *Token) Issuer() did.DID {
return t.issuer
}
// Audience returns the did.DID representing the Token's audience.
func (t *Token) Audience() did.DID {
return t.audience
}
// Subject returns the did.DID representing the Token's subject.
//
// This field may be did.Undef for delegations that are [Powerlined] but
// must be equal to the value returned by the Issuer method for root
// tokens.
func (t *Token) Subject() did.DID {
return t.subject
}
// Command returns the capability's command.Command.
func (t *Token) Command() command.Command {
return t.command
}
// Nonce returns the random Nonce encapsulated in this Token.
func (t *Token) Nonce() []byte {
return t.nonce
}
// Meta returns the Token's metadata.
func (t *Token) Meta() *meta.Meta {
return t.meta
}
// Expiration returns the time at which the Token expires.
func (t *Token) Expiration() *time.Time {
return t.expiration
}
func (t *Token) validate() error {
var errs error
requiredDID := func(id did.DID, fieldname string) {
if !id.Defined() {
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
}
}
requiredDID(t.issuer, "Issuer")
// TODO
if len(t.nonce) < 12 {
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
}
return errs
}
// tokenFromModel build a decoded view of the raw IPLD data.
// This function also serves as validation.
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
var (
tkn Token
)
// TODO
return &tkn, nil
}
// generateNonce creates a 12-byte random nonce.
// TODO: some crypto scheme require more, is that our case?
func generateNonce() ([]byte, error) {
res := make([]byte, 12)
_, err := rand.Read(res)
if err != nil {
return nil, err
}
return res, nil
}

View File

@@ -0,0 +1,23 @@
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
}

222
token/invocation/ipld.go Normal file
View File

@@ -0,0 +1,222 @@
package invocation
import (
"io"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
)
// ToSealed wraps the invocation token in an envelope, generates the
// signature, encodes the result to DAG-CBOR and calculates the CID of
// the resulting binary data.
func (t *Token) ToSealed(privKey crypto.PrivKey) ([]byte, cid.Cid, error) {
data, err := t.ToDagCbor(privKey)
if err != nil {
return nil, cid.Undef, err
}
id, err := envelope.CIDFromBytes(data)
if err != nil {
return nil, cid.Undef, err
}
return data, id, nil
}
// ToSealedWriter is the same as ToSealed but accepts an io.Writer.
func (t *Token) ToSealedWriter(w io.Writer, privKey crypto.PrivKey) (cid.Cid, error) {
cidWriter := envelope.NewCIDWriter(w)
if err := t.ToDagCborWriter(cidWriter, privKey); err != nil {
return cid.Undef, err
}
return cidWriter.CID()
}
// FromSealed decodes the provided binary data from the DAG-CBOR format,
// verifies that the envelope's signature is correct based on the public
// key taken from the issuer (iss) field and calculates the CID of the
// incoming data.
func FromSealed(data []byte) (*Token, cid.Cid, error) {
tkn, err := FromDagCbor(data)
if err != nil {
return nil, cid.Undef, err
}
id, err := envelope.CIDFromBytes(data)
if err != nil {
return nil, cid.Undef, err
}
return tkn, id, nil
}
// FromSealedReader is the same as Unseal but accepts an io.Reader.
func FromSealedReader(r io.Reader) (*Token, cid.Cid, error) {
cidReader := envelope.NewCIDReader(r)
tkn, err := FromDagCborReader(cidReader)
if err != nil {
return nil, cid.Undef, err
}
id, err := cidReader.CID()
if err != nil {
return nil, cid.Undef, err
}
return tkn, id, nil
}
// Encode marshals a Token to the format specified by the provided
// codec.Encoder.
func (t *Token) Encode(privKey crypto.PrivKey, encFn codec.Encoder) ([]byte, error) {
node, err := t.toIPLD(privKey)
if err != nil {
return nil, err
}
return ipld.Encode(node, encFn)
}
// EncodeWriter is the same as Encode, but accepts an io.Writer.
func (t *Token) EncodeWriter(w io.Writer, privKey crypto.PrivKey, encFn codec.Encoder) error {
node, err := t.toIPLD(privKey)
if err != nil {
return err
}
return ipld.EncodeStreaming(w, node, encFn)
}
// ToDagCbor marshals the Token to the DAG-CBOR format.
func (t *Token) ToDagCbor(privKey crypto.PrivKey) ([]byte, error) {
return t.Encode(privKey, dagcbor.Encode)
}
// ToDagCborWriter is the same as ToDagCbor, but it accepts an io.Writer.
func (t *Token) ToDagCborWriter(w io.Writer, privKey crypto.PrivKey) error {
return t.EncodeWriter(w, privKey, dagcbor.Encode)
}
// ToDagJson marshals the Token to the DAG-JSON format.
func (t *Token) ToDagJson(privKey crypto.PrivKey) ([]byte, error) {
return t.Encode(privKey, dagjson.Encode)
}
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
func (t *Token) ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error {
return t.EncodeWriter(w, privKey, dagjson.Encode)
}
// Decode unmarshals the input data using the format specified by the
// provided codec.Decoder into a Token.
//
// An error is returned if the conversion fails, or if the resulting
// Token is invalid.
func Decode(b []byte, decFn codec.Decoder) (*Token, error) {
node, err := ipld.Decode(b, decFn)
if err != nil {
return nil, err
}
return FromIPLD(node)
}
// DecodeReader is the same as Decode, but accept an io.Reader.
func DecodeReader(r io.Reader, decFn codec.Decoder) (*Token, error) {
node, err := ipld.DecodeStreaming(r, decFn)
if err != nil {
return nil, err
}
return FromIPLD(node)
}
// FromDagCbor unmarshals the input data into a Token.
//
// An error is returned if the conversion fails, or if the resulting
// Token is invalid.
func FromDagCbor(data []byte) (*Token, error) {
pay, err := envelope.FromDagCbor[*tokenPayloadModel](data)
if err != nil {
return nil, err
}
tkn, err := tokenFromModel(*pay)
if err != nil {
return nil, err
}
return tkn, err
}
// FromDagCborReader is the same as FromDagCbor, but accept an io.Reader.
func FromDagCborReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagcbor.Decode)
}
// FromDagJson unmarshals the input data into a Token.
//
// An error is returned if the conversion fails, or if the resulting
// Token is invalid.
func FromDagJson(data []byte) (*Token, error) {
return Decode(data, dagjson.Decode)
}
// FromDagJsonReader is the same as FromDagJson, but accept an io.Reader.
func FromDagJsonReader(r io.Reader) (*Token, error) {
return DecodeReader(r, dagjson.Decode)
}
// FromIPLD decode the given IPLD representation into a Token.
func FromIPLD(node datamodel.Node) (*Token, error) {
pay, err := envelope.FromIPLD[*tokenPayloadModel](node)
if err != nil {
return nil, err
}
tkn, err := tokenFromModel(*pay)
if err != nil {
return nil, err
}
return tkn, err
}
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
var sub *string
if t.subject != did.Undef {
s := t.subject.String()
sub = &s
}
// TODO
var exp *int64
if t.expiration != nil {
u := t.expiration.Unix()
exp = &u
}
model := &tokenPayloadModel{
Iss: t.issuer.String(),
Aud: t.audience.String(),
Sub: sub,
Cmd: t.command.String(),
Nonce: t.nonce,
Meta: *t.meta,
Exp: exp,
}
return envelope.ToIPLD(privKey, model)
}

View File

@@ -0,0 +1,77 @@
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 Normal file
View File

@@ -0,0 +1,125 @@
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)
}
}

View File

@@ -1,313 +0,0 @@
package delegation_test
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/tokens/delegation"
"github.com/ucan-wg/go-ucan/tokens/internal/envelope"
)
// The following example shows how to create a delegation.Token with
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
func ExampleNew() {
issuerPrivKey := examplePrivKey(issuerPrivKeyCfg)
audienceDID := exampleDID(AudienceDID)
command := exampleCommand(subJectCmd)
policy := examplePolicy(subjectPol)
subjectDID := exampleDID(subjectDID)
// Don't do this in your code - a nonce should be a cryptographically
// strong random slice of bytes to ensure the integrity of your private
// key. For this example, a fixed nonce is required to obtain the fixed
// printed output (below). If unsure of what value to supply for the
// nonce, don't pass the WithNonce option and one will be generated
// when the token is created.
nonce := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
tkn, err := delegation.New(
issuerPrivKey,
audienceDID,
command,
policy,
delegation.WithSubject(subjectDID),
delegation.WithNonce(nonce),
)
printThenPanicOnErr(err)
data, id, err := tkn.ToSealed(issuerPrivKey)
printThenPanicOnErr(err)
printCIDAndSealed(id, data)
//Output:
//CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
//DAG-CBOR (base64) out: glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL
//Converted to DAG-JSON out:
//[
// {
// "/": {
// "bytes": "mnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBQ"
// }
// },
// {
// "h": {
// "/": {
// "bytes": "NO0BcQ"
// }
// },
// "ucan/dlg@1.0.0-rc.1": {
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
// "cmd": "/foo/bar",
// "exp": null,
// "iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
// "meta": {},
// "nonce": {
// "/": {
// "bytes": "AAECAwQFBgcICQoL"
// }
// },
// "pol": [
// [
// "==",
// ".status",
// "draft"
// ],
// [
// "all",
// ".reviewer",
// [
// "like",
// ".email",
// "*@example.com"
// ]
// ],
// [
// "any",
// ".tags",
// [
// "or",
// [
// [
// "==",
// ".",
// "news"
// ],
// [
// "==",
// ".",
// "press"
// ]
// ]
// ]
// ]
// ],
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
// }
// }
//]
}
// The following example shows how to create a UCAN root delegation.Token
// - a delegation.Token with the subject (sub) set to the value of issuer
// (iss).
func ExampleRoot() {
issuerPrivKey := examplePrivKey(issuerPrivKeyCfg)
audienceDID := exampleDID(AudienceDID)
command := exampleCommand(subJectCmd)
policy := examplePolicy(subjectPol)
// Don't do this in your code - a nonce should be a cryptographically
// strong random slice of bytes to ensure the integrity of your private
// key. For this example, a fixed nonce is required to obtain the fixed
// printed output (below). If unsure of what value to supply for the
// nonce, don't pass the WithNonce option and one will be generated
// when the token is created.
nonce := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
tkn, err := delegation.Root(
issuerPrivKey,
audienceDID,
command,
policy,
delegation.WithNonce(nonce),
)
printThenPanicOnErr(err)
data, id, err := tkn.ToSealed(issuerPrivKey)
printThenPanicOnErr(err)
printCIDAndSealed(id, data)
//Output:
//CID (base58BTC): zdpuAnbsR3e6DK8hBk5WA7KwbHYN6CKY4a3Bv1GNehvFYShQ8
//DAG-CBOR (base64) out: glhA67ASBczF/wlIP0ESENn+4ZNQKukjcTNz+fo7K2tYa6OUm0rWICDJJkDWm7lJeQt+KvSA+Y4ctHTQbAr3Lr7mDqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rcHpuMm4zWkdUMlZhcU1HU1FDM3R6bXpWNFRTOVM3MWlGc0RYRTFXbm9OSDJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL
//Converted to DAG-JSON out:
//[
// {
// "/": {
// "bytes": "67ASBczF/wlIP0ESENn+4ZNQKukjcTNz+fo7K2tYa6OUm0rWICDJJkDWm7lJeQt+KvSA+Y4ctHTQbAr3Lr7mDg"
// }
// },
// {
// "h": {
// "/": {
// "bytes": "NO0BcQ"
// }
// },
// "ucan/dlg@1.0.0-rc.1": {
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
// "cmd": "/foo/bar",
// "exp": null,
// "iss": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2",
// "meta": {},
// "nonce": {
// "/": {
// "bytes": "AAECAwQFBgcICQoL"
// }
// },
// "pol": [
// [
// "==",
// ".status",
// "draft"
// ],
// [
// "all",
// ".reviewer",
// [
// "like",
// ".email",
// "*@example.com"
// ]
// ],
// [
// "any",
// ".tags",
// [
// "or",
// [
// [
// "==",
// ".",
// "news"
// ],
// [
// "==",
// ".",
// "press"
// ]
// ]
// ]
// ]
// ],
// "sub": "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
// }
// }
//]
}
// The following example demonstrates how to get a delegation.Token from
// a DAG-CBOR []byte.
func ExampleToken_FromSealed() {
cborBytes := exampleCBORData()
fmt.Println("DAG-CBOR (base64) in:", base64.StdEncoding.EncodeToString(cborBytes))
tkn, err := delegation.FromSealed(cborBytes)
printThenPanicOnErr(err)
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(tkn.CID()))
fmt.Println("Issuer (iss):", tkn.Issuer().String())
fmt.Println("Audience (aud):", tkn.Audience().String())
fmt.Println("Subject (sub):", tkn.Subject().String())
fmt.Println("Command (cmd):", tkn.Command().String())
fmt.Println("Policy (pol): TODO")
fmt.Println("Nonce (nonce):", hex.EncodeToString(tkn.Nonce()))
fmt.Println("Meta (meta): TODO")
fmt.Println("NotBefore (nbf):", tkn.NotBefore())
fmt.Println("Expiration (exp):", tkn.Expiration())
//Output:
//DAG-CBOR (base64) in: glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL
//CID (base58BTC): zdpuAw26pFuvZa2Z9YAtpZZnWN6VmnRFr7Z8LVY5c7RVWoxGY
//Issuer (iss): did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2
//Audience (aud): did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv
//Subject (sub): did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2
//Command (cmd): /foo/bar
//Policy (pol): TODO
//Nonce (nonce): 000102030405060708090a0b
//Meta (meta): TODO
//NotBefore (nbf): <nil>
//Expiration (exp): <nil>
}
func exampleCBORData() []byte {
data, err := base64.StdEncoding.DecodeString("glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL")
printThenPanicOnErr(err)
return data
}
func exampleDID(didStr string) did.DID {
id, err := did.Parse(didStr)
printThenPanicOnErr(err)
return id
}
func exampleCommand(cmdStr string) command.Command {
cmd, err := command.Parse(cmdStr)
printThenPanicOnErr(err)
return cmd
}
func examplePolicy(policyJSON string) policy.Policy {
pol, err := policy.FromDagJson(policyJSON)
printThenPanicOnErr(err)
return pol
}
func examplePrivKey(privKeyCfg string) crypto.PrivKey {
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
printThenPanicOnErr(err)
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
printThenPanicOnErr(err)
return privKey
}
func printCIDAndSealed(id cid.Cid, data []byte) {
fmt.Println("CID (base58BTC):", envelope.CIDToBase58BTC(id))
fmt.Println("DAG-CBOR (base64) out:", base64.StdEncoding.EncodeToString(data))
fmt.Println("Converted to DAG-JSON out:")
node, err := ipld.Decode(data, dagcbor.Decode)
printThenPanicOnErr(err)
rawJSON, err := ipld.Encode(node, dagjson.Encode)
printThenPanicOnErr(err)
prettyJSON := &bytes.Buffer{}
err = json.Indent(prettyJSON, rawJSON, "", "\t")
printThenPanicOnErr(err)
fmt.Println(prettyJSON.String())
}
func printThenPanicOnErr(err error) {
if err != nil {
panic(err)
}
}