Compare commits
323 Commits
invocation
...
cont-namin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c141029c3 | ||
|
|
0c6717cfbc | ||
|
|
6d7fd28324 | ||
|
|
ccc0120e78 | ||
|
|
bf27b97a57 | ||
|
|
10dd4fa6d1 | ||
|
|
5695609f8b | ||
|
|
b7ddead7c8 | ||
|
|
9cbec37685 | ||
|
|
00dbd975b1 | ||
|
|
c792a4cce5 | ||
|
|
8f3f1c775e | ||
|
|
704ed25768 | ||
|
|
95bdbc4fc5 | ||
|
|
416345dba9 | ||
|
|
042d6dc52f | ||
|
|
8bb3a4f4d0 | ||
|
|
47156a8ad6 | ||
|
|
ce6d163627 | ||
|
|
c3c2c96008 | ||
|
|
903632695f | ||
|
|
f2d75b7815 | ||
|
|
4f09829abe | ||
|
|
5660df32b5 | ||
|
|
2f2a74c7ec | ||
|
|
0592717637 | ||
|
|
80c2d60ab3 | ||
|
|
c518c6657a | ||
|
|
78825f4f55 | ||
|
|
7f1adbd945 | ||
|
|
0f59088d0b | ||
|
|
72e0f353e7 | ||
|
|
d0d4ec3abe | ||
|
|
bb24081b28 | ||
|
|
3688ccea01 | ||
|
|
e9105896d7 | ||
|
|
15751c7362 | ||
|
|
d52218fa5a | ||
|
|
64d3024dec | ||
|
|
78d37d92ef | ||
|
|
da806b1bc5 | ||
|
|
311b942a6d | ||
|
|
56eab758ed | ||
|
|
105323b989 | ||
|
|
5b816ccc62 | ||
|
|
28272e6900 | ||
|
|
a854389e32 | ||
|
|
117a75e2c4 | ||
|
|
a25bfbaf45 | ||
|
|
bff482f73b | ||
|
|
ff79bbb443 | ||
|
|
3997a86184 | ||
|
|
200d6a8ae2 | ||
|
|
0349e7e463 | ||
|
|
dff52f80c4 | ||
|
|
5b7a63a2c6 | ||
|
|
66675f7030 | ||
|
|
7e54be49e1 | ||
|
|
15535d3474 | ||
|
|
170e597e71 | ||
|
|
ce1a4b6e32 | ||
|
|
d1d047cd9e | ||
|
|
3680637090 | ||
|
|
1166a68e5c | ||
|
|
ba4db9bce8 | ||
|
|
20369dba49 | ||
|
|
ade2c7f858 | ||
|
|
943a318b26 | ||
|
|
2d79cdc54e | ||
|
|
60bdc8873b | ||
|
|
820057e41e | ||
|
|
ba0038b0ae | ||
|
|
4a4b200312 | ||
|
|
caae2f58bf | ||
|
|
ec627138cb | ||
|
|
4ec409edc6 | ||
|
|
c61fc8d8b3 | ||
|
|
d90715d1fe | ||
|
|
5f8536e480 | ||
|
|
c19e38356d | ||
|
|
aea1880386 | ||
|
|
2fb5a3dc01 | ||
|
|
e980d6c0b9 | ||
|
|
1098e76cba | ||
|
|
bb36d61d93 | ||
|
|
417ef78570 | ||
|
|
00d2380f14 | ||
|
|
8ca088bf27 | ||
|
|
25ca34923f | ||
|
|
fc4c8f2de1 | ||
|
|
64b989452f | ||
|
|
92065ca0d3 | ||
|
|
814cec1495 | ||
|
|
0f70557309 | ||
|
|
89e4d5d419 | ||
|
|
9057cbcba6 | ||
|
|
98d9cadcbd | ||
|
|
e938d64220 | ||
|
|
c577d73f3e | ||
|
|
be185a8496 | ||
|
|
17a57c622a | ||
|
|
6298fa28bd | ||
|
|
d3e97aaa08 | ||
|
|
fdff79d23a | ||
|
|
a26d836025 | ||
|
|
9f47418bdf | ||
|
|
81c7a0f80d | ||
|
|
3987e8649c | ||
|
|
17a1d54b6f | ||
|
|
7cb0f97b30 | ||
|
|
c4a53f42b6 | ||
|
|
522181b16a | ||
|
|
633b3d210a | ||
|
|
3c705ca150 | ||
|
|
1fa2b5e6fc | ||
|
|
11bc085c60 | ||
|
|
a4a8634eb8 | ||
|
|
d353dfe652 | ||
|
|
1e5ecdc205 | ||
|
|
f9065d39d8 | ||
|
|
cddade4670 | ||
|
|
948087744d | ||
|
|
bfb93d6988 | ||
|
|
cfcb199818 | ||
|
|
85557ab6b5 | ||
|
|
adc2b8d0da | ||
|
|
bcdaf0cca3 | ||
|
|
d754c5837b | ||
|
|
d89fb395e3 | ||
|
|
4932e32052 | ||
|
|
a52b48cf47 | ||
|
|
e6e4d85381 | ||
|
|
962e897ff5 | ||
|
|
58bb5cdb8f | ||
|
|
ce7f653ab0 | ||
|
|
7d4f973171 | ||
|
|
3dc0011628 | ||
|
|
08f821f23d | ||
|
|
1b61f2e4db | ||
|
|
187e7a869c | ||
|
|
a98653b769 | ||
|
|
31d16ac468 | ||
|
|
d2b004c405 | ||
|
|
884d63a689 | ||
|
|
c9f3a6033a | ||
|
|
8447499c5a | ||
|
|
b4e222f8a0 | ||
|
|
41b8600fbc | ||
|
|
824c8fe523 | ||
|
|
6aeb6a8b70 | ||
|
|
a1aaf47d7c | ||
|
|
728696f169 | ||
|
|
cfb4446a05 | ||
|
|
06a72868a5 | ||
|
|
6f9a6fa5c1 | ||
|
|
f2b4c3ac20 | ||
|
|
7a7db684c3 | ||
|
|
d7454156d2 | ||
|
|
d3ad6715d9 | ||
|
|
602bdf9c7a | ||
|
|
d21c17c4ca | ||
|
|
72f4ef7b5e | ||
|
|
02be4010d6 | ||
|
|
61e031529f | ||
|
|
19721027e4 | ||
|
|
bc847ee027 | ||
|
|
5bfe430934 | ||
|
|
10b5e1e603 | ||
|
|
3cf1de6b67 | ||
|
|
400f689a85 | ||
|
|
6717a3a89c | ||
|
|
9e9c632ded | ||
|
|
b210c69173 | ||
|
|
6d85b2ba3c | ||
|
|
76c015e78b | ||
|
|
fcb527cc52 | ||
|
|
89f648a94e | ||
|
|
e1d771333c | ||
|
|
f44cf8af78 | ||
|
|
2b2fc4a13f | ||
|
|
6b72799818 | ||
|
|
ccc85d4697 | ||
|
|
0d63e90b67 | ||
|
|
d784c92c29 | ||
|
|
7662fe34db | ||
|
|
6d0fbd4d5a | ||
|
|
e76354fb0a | ||
|
|
deaf9c4fe9 | ||
|
|
a1c2c5c067 | ||
|
|
2c58fedfd5 | ||
|
|
2ea9f8c93b | ||
|
|
00ff88ef23 | ||
|
|
2ffdf004ac | ||
|
|
a8780f750c | ||
|
|
c70f68b886 | ||
|
|
a27eb258e5 | ||
|
|
866683347f | ||
|
|
2fafbe7bf3 | ||
|
|
1728bf29b8 | ||
|
|
8fac97b7e7 | ||
|
|
7ad940844c | ||
|
|
52ae2eaf60 | ||
|
|
570bcdcb6c | ||
|
|
5abb870462 | ||
|
|
4ec675861d | ||
|
|
b941b507e0 | ||
|
|
e66beb662e | ||
|
|
87e25090bb | ||
|
|
6011f0740a | ||
|
|
2bd177ce4d | ||
|
|
abda49061d | ||
|
|
fb978ee574 | ||
|
|
da1310b78a | ||
|
|
ac1b03f144 | ||
|
|
7fa3ba1492 | ||
|
|
081d382028 | ||
|
|
2ad3aeb6da | ||
|
|
030db7ec0d | ||
|
|
aa4ad2fc10 | ||
|
|
51e8d5ce04 | ||
|
|
f8b5fa3a32 | ||
|
|
59da2d1a2c | ||
|
|
88ed55b252 | ||
|
|
9051e5250b | ||
|
|
5f2877f0ff | ||
|
|
100a510097 | ||
|
|
2a51d61b46 | ||
|
|
3e3c5a83cc | ||
|
|
d60fb71156 | ||
|
|
40639b6715 | ||
|
|
60922ced96 | ||
|
|
f7b4b48791 | ||
|
|
346efbd31d | ||
|
|
df9beadf9c | ||
|
|
8615f6c72b | ||
|
|
d9739a3bab | ||
|
|
6b8fbcee0a | ||
|
|
a7037dbc47 | ||
|
|
50ea43e3fa | ||
|
|
0ec16a085c | ||
|
|
2b45f7630e | ||
|
|
637973b10b | ||
|
|
bb4725d87c | ||
|
|
8782554a7b | ||
|
|
a8302ad441 | ||
|
|
b4dd8c0757 | ||
|
|
4201ab2dca | ||
|
|
1b7059c029 | ||
|
|
f3a5209cec | ||
|
|
a2822f02c7 | ||
|
|
79955057a3 | ||
|
|
59cebf8e74 | ||
|
|
952a6bb922 | ||
|
|
2089aa2a6a | ||
|
|
4a655506f9 | ||
|
|
93dd3ef719 | ||
|
|
6075c19957 | ||
|
|
6161f2e440 | ||
|
|
5202056cc7 | ||
|
|
f779477118 | ||
|
|
4974fed931 | ||
|
|
0d9955b7b0 | ||
|
|
6dd6f8a229 | ||
|
|
5509cce513 | ||
|
|
f4ad97679c | ||
|
|
41c8bc7218 | ||
|
|
371bf3b9f5 | ||
|
|
b14671009c | ||
|
|
043c9b160d | ||
|
|
130168809b | ||
|
|
20886f1b5f | ||
|
|
684c21c7a4 | ||
|
|
4749243e3c | ||
|
|
c7f6034376 | ||
|
|
55070dcb43 | ||
|
|
fe594e9906 | ||
|
|
0781b84937 | ||
|
|
f44b6ec2c3 | ||
|
|
abe8a8150a | ||
|
|
f44b5cb921 | ||
|
|
baf3edcf88 | ||
|
|
7107d6bc85 | ||
|
|
c66dd5b2a4 | ||
|
|
70dc12d68e | ||
|
|
dd1f54694f | ||
|
|
ac73cae3ec | ||
|
|
a19d3505fe | ||
|
|
526a34b45d | ||
|
|
989f409fd0 | ||
|
|
40488dfc3d | ||
|
|
84122e57bc | ||
|
|
4e15349c5e | ||
|
|
53cb82a2b4 | ||
|
|
64936fd061 | ||
|
|
30be95b20c | ||
|
|
16ba4b392d | ||
|
|
94a0d4d56e | ||
|
|
53ef97231d | ||
|
|
c960481a10 | ||
|
|
d4d4514971 | ||
|
|
282db65900 | ||
|
|
2459f1a5c3 | ||
|
|
37f5286315 | ||
|
|
06e0674c46 | ||
|
|
ad03154b6e | ||
|
|
700f130858 | ||
|
|
d57d2a230b | ||
|
|
7060d4bb33 | ||
|
|
a183b627be | ||
|
|
dbfff3f70c | ||
|
|
cb45d9019b | ||
|
|
2e17ff8550 | ||
|
|
e86e45be73 | ||
|
|
97c9990045 | ||
|
|
6a17270545 | ||
|
|
941e6366a3 | ||
|
|
8bf7fa173c | ||
|
|
f731f56dd2 | ||
|
|
d042b5cdfd | ||
|
|
d51bbc303c | ||
|
|
dfb09cadb9 | ||
|
|
707ec46338 | ||
|
|
4dd91f95f7 |
34
.github/workflows/bench.yml
vendored
Normal file
34
.github/workflows/bench.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: go continuous benchmark
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
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@v5
|
||||||
|
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
|
||||||
8
.github/workflows/gotest.yml
vendored
8
.github/workflows/gotest.yml
vendored
@@ -7,14 +7,14 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu" ]
|
os: [ "ubuntu" ]
|
||||||
go: [ "1.21.x", "1.22.x", "1.23.x", ]
|
go: [ "1.22.x", "1.23.x", ]
|
||||||
env:
|
env:
|
||||||
COVERAGES: ""
|
COVERAGES: ""
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
name: ${{ matrix.os}} (go ${{ matrix.go }})
|
name: ${{ matrix.os}} (go ${{ matrix.go }})
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go }}
|
go-version: ${{ matrix.go }}
|
||||||
- name: Go information
|
- name: Go information
|
||||||
@@ -22,6 +22,6 @@ jobs:
|
|||||||
go version
|
go version
|
||||||
go env
|
go env
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -v ./...
|
run: go test -v ./... -tags jwx_es256k
|
||||||
- name: Check formatted
|
- name: Check formatted
|
||||||
run: gofmt -l .
|
run: gofmt -l .
|
||||||
@@ -29,7 +29,7 @@ Verbatim copies of both licenses are included below:
|
|||||||
```
|
```
|
||||||
Apache License
|
Apache License
|
||||||
Version 2.0, January 2004
|
Version 2.0, January 2004
|
||||||
http://www.apache.org/licenses/
|
https://www.apache.org/licenses/
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
|||||||
77
Readme.md
Normal file
77
Readme.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan" target="_blank">
|
||||||
|
<img src="https://raw.githubusercontent.com/ucan-wg/go-ucan/v1/assets/logo.png" alt="go-ucan Logo" height="250"></img>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1 align="center">go-ucan</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/UCAN-v1.0.0--rc.1-blue" alt="UCAN v1.0.0-rc.1">
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/tags">
|
||||||
|
<img alt="GitHub Tag" src="https://img.shields.io/github/v/tag/ucan-wg/go-ucan">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/actions?query=">
|
||||||
|
<img src="https://github.com/ucan-wg/go-ucan/actions/workflows/gotest.yml/badge.svg" alt="Build Status">
|
||||||
|
</a>
|
||||||
|
<a href="https://ucan-wg.github.io/go-ucan/dev/bench/">
|
||||||
|
<img alt="Go benchmarks" src="https://img.shields.io/badge/Benchmarks-go-blue">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md">
|
||||||
|
<img alt="Apache 2.0 + MIT License" src="https://img.shields.io/badge/License-Apache--2.0+MIT-green">
|
||||||
|
</a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/ucan-wg/go-ucan">
|
||||||
|
<img src="https://img.shields.io/badge/Docs-godoc-blue" alt="Docs">
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/JSyFG6XgVM">
|
||||||
|
<img src="https://img.shields.io/static/v1?label=Discord&message=join%20us!&color=mediumslateblue" alt="Discord">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This is a go library to help the next generation of web and decentralized applications make use
|
||||||
|
of UCANs in their authorization flows.
|
||||||
|
|
||||||
|
User Controlled Authorization Networks (UCANs) are a way of doing authorization where users are fully in control. OAuth is designed for a centralized world, UCAN is the distributed user controlled version.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Specifications
|
||||||
|
|
||||||
|
The UCAN specification is separated in multiple sub-spec:
|
||||||
|
- [Main specification](https://github.com/ucan-wg/spec)
|
||||||
|
- [Delegation](https://github.com/ucan-wg/delegation/tree/v1_ipld)
|
||||||
|
- [Invocation](https://github.com/ucan-wg/invocation)
|
||||||
|
|
||||||
|
Not implemented yet:
|
||||||
|
- [Revocation](https://github.com/ucan-wg/revocation/tree/first-draft)
|
||||||
|
- [Promise](https://github.com/ucan-wg/promise/tree/v1-rc1)
|
||||||
|
|
||||||
|
### Talks
|
||||||
|
|
||||||
|
- [Decentralizing Auth, and UCAN Too - Brooklyn Zelenka (2023)](https://www.youtube.com/watch?v=MuHfrqw9gQA)
|
||||||
|
- [What's New in UCAN 1.0 - Brooklyn Zelenka (2024)](https://www.youtube.com/watch?v=-uohQzZcwF4)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
`go-ucan` currently support the required parts of the UCAN specification: the main specification, delegation and invocation.
|
||||||
|
|
||||||
|
Besides that, `go-ucan` also includes:
|
||||||
|
- a simplified [DID](https://www.w3.org/TR/did-core/) and [did-key](https://w3c-ccg.github.io/did-method-key/) implementation
|
||||||
|
- a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together
|
||||||
|
- support for encrypted values in token's metadata
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
For usage questions, usecases, or issues reach out to us in our `go-ucan`
|
||||||
|
[Discord channel](https://discord.gg/3EHEQ6M8BC).
|
||||||
|
|
||||||
|
We would be happy to try to answer your question or try opening a new issue on
|
||||||
|
Github.
|
||||||
|
|
||||||
|
## UCAN Gopher
|
||||||
|
|
||||||
|
Artwork by [Bruno Monts](https://www.instagram.com/bruno_monts). Thank you [Renee French](http://reneefrench.blogspot.com/) for creating the [Go Gopher](https://blog.golang.org/gopher)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the double license [Apache 2.0 + MIT](https://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md).
|
||||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -1,142 +0,0 @@
|
|||||||
package command
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
separator = "/"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrNew indicates that the wrapped error was encountered while creating
|
|
||||||
// a new Command.
|
|
||||||
var ErrNew = errors.New("failed to create Command from elems")
|
|
||||||
|
|
||||||
// ErrParse indicates that the wrapped error was encountered while
|
|
||||||
// attempting to parse a string as a Command.
|
|
||||||
var ErrParse = errors.New("failed to parse Command")
|
|
||||||
|
|
||||||
// ErrorJoin indicates that the wrapped error was encountered while
|
|
||||||
// attempting to join a new segment to a Command.
|
|
||||||
var ErrJoin = errors.New("failed to join segments to Command")
|
|
||||||
|
|
||||||
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
|
||||||
// doesn't start with a [leading slash character].
|
|
||||||
//
|
|
||||||
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrRequiresLeadingSlash = parseError("a command requires a leading slash character")
|
|
||||||
|
|
||||||
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
|
||||||
// with a trailing slash character].
|
|
||||||
//
|
|
||||||
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrDisallowsTrailingSlash = parseError("a command must not include a trailing slash")
|
|
||||||
|
|
||||||
// ErrUCANNamespaceReserved is returned to indicate that a Command's
|
|
||||||
// first segment would contain the [reserved "ucan" namespace].
|
|
||||||
//
|
|
||||||
// [reserved "ucan" namespace]: https://github.com/ucan-wg/spec#ucan-namespace
|
|
||||||
var ErrUCANNamespaceReserved = errors.New("the UCAN namespace is reserved")
|
|
||||||
|
|
||||||
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
|
||||||
// [uppercase unicode characters].
|
|
||||||
//
|
|
||||||
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
var ErrRequiresLowercase = parseError("UCAN path segments must must not contain upper-case characters")
|
|
||||||
|
|
||||||
func parseError(msg string) error {
|
|
||||||
return fmt.Errorf("%w: %s", ErrParse, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ fmt.Stringer = (*Command)(nil)
|
|
||||||
|
|
||||||
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
|
||||||
// interpretable by the Subject of a UCAN.
|
|
||||||
//
|
|
||||||
// A [Command] is composed of a leading slash which is optionally followed
|
|
||||||
// by one or more slash-separated Segments of lowercase characters.
|
|
||||||
//
|
|
||||||
// [Command]: https://github.com/ucan-wg/spec#command
|
|
||||||
type Command struct {
|
|
||||||
segments []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a validated command from the provided list of segment
|
|
||||||
// strings. An error is returned if an invalid Command would be
|
|
||||||
// formed
|
|
||||||
func New(segments ...string) (*Command, error) {
|
|
||||||
return newCommand(ErrNew, segments...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCommand(err error, segments ...string) (*Command, error) {
|
|
||||||
if len(segments) > 0 && segments[0] == "ucan" {
|
|
||||||
return nil, fmt.Errorf("%w: %w", err, ErrUCANNamespaceReserved)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := Command{segments}
|
|
||||||
|
|
||||||
return &cmd, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse verifies that the provided string contains the required
|
|
||||||
// [segment structure] and, if valid, returns the resulting
|
|
||||||
// Command.
|
|
||||||
//
|
|
||||||
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
|
||||||
func Parse(s string) (*Command, error) {
|
|
||||||
if !strings.HasPrefix(s, "/") {
|
|
||||||
return nil, ErrRequiresLeadingSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
|
||||||
return nil, ErrDisallowsTrailingSlash
|
|
||||||
}
|
|
||||||
|
|
||||||
if s != strings.ToLower(s) {
|
|
||||||
return nil, ErrRequiresLowercase
|
|
||||||
}
|
|
||||||
|
|
||||||
// The leading slash will result in the first element from strings.Split
|
|
||||||
// being an empty string which is removed as strings.Join will ignore it.
|
|
||||||
return newCommand(ErrParse, strings.Split(s, "/")[1:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Top] is the most powerful capability.
|
|
||||||
//
|
|
||||||
// This function returns a Command that is a wildcard and therefore represents the
|
|
||||||
// most powerful abilily. As such it should be handle with care and used
|
|
||||||
// sparingly.
|
|
||||||
//
|
|
||||||
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
|
||||||
func Top() *Command {
|
|
||||||
cmd, _ := New()
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid returns true if the provided string is a valid UCAN command.
|
|
||||||
func IsValid(s string) bool {
|
|
||||||
_, err := Parse(s)
|
|
||||||
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join appends segments to the end of this command using the required
|
|
||||||
// segment separator.
|
|
||||||
func (c *Command) Join(segments ...string) (*Command, error) {
|
|
||||||
return newCommand(ErrJoin, append(c.segments, segments...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Segments returns the ordered segments that comprise the Command as a
|
|
||||||
// slice of strings.
|
|
||||||
func (c *Command) Segments() []string {
|
|
||||||
return c.segments
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the composed representation the command. This is also
|
|
||||||
// the required wire representation (before IPLD encoding occurs.)
|
|
||||||
func (c *Command) String() string {
|
|
||||||
return "/" + strings.Join([]string(c.segments), "/")
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package literal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"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)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Int(val int64) ipld.Node {
|
|
||||||
nb := basicnode.Prototype.Int.NewBuilder()
|
|
||||||
nb.AssignInt(val)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Float(val float64) ipld.Node {
|
|
||||||
nb := basicnode.Prototype.Float.NewBuilder()
|
|
||||||
nb.AssignFloat(val)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func String(val string) ipld.Node {
|
|
||||||
nb := basicnode.Prototype.String.NewBuilder()
|
|
||||||
nb.AssignString(val)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Bytes(val []byte) ipld.Node {
|
|
||||||
nb := basicnode.Prototype.Bytes.NewBuilder()
|
|
||||||
nb.AssignBytes(val)
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Null() ipld.Node {
|
|
||||||
nb := basicnode.Prototype.Any.NewBuilder()
|
|
||||||
nb.AssignNull()
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Match determines if the IPLD node matches the policy document.
|
|
||||||
func Match(policy Policy, node ipld.Node) bool {
|
|
||||||
for _, stmt := range policy {
|
|
||||||
ok := matchStatement(stmt, node)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchStatement(statement Statement, node ipld.Node) bool {
|
|
||||||
switch statement.Kind() {
|
|
||||||
case KindEqual:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return datamodel.DeepEqual(s.value, one)
|
|
||||||
}
|
|
||||||
case KindGreaterThan:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, gt)
|
|
||||||
}
|
|
||||||
case KindGreaterThanOrEqual:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, gte)
|
|
||||||
}
|
|
||||||
case KindLessThan:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, lt)
|
|
||||||
}
|
|
||||||
case KindLessThanOrEqual:
|
|
||||||
if s, ok := statement.(equality); ok {
|
|
||||||
one, _, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return isOrdered(s.value, one, lte)
|
|
||||||
}
|
|
||||||
case KindNot:
|
|
||||||
if s, ok := statement.(negation); ok {
|
|
||||||
return !matchStatement(s.statement, node)
|
|
||||||
}
|
|
||||||
case KindAnd:
|
|
||||||
if s, ok := statement.(connective); ok {
|
|
||||||
for _, cs := range s.statements {
|
|
||||||
r := matchStatement(cs, node)
|
|
||||||
if !r {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case KindOr:
|
|
||||||
if s, ok := statement.(connective); ok {
|
|
||||||
if len(s.statements) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, cs := range s.statements {
|
|
||||||
r := matchStatement(cs, node)
|
|
||||||
if r {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case KindLike:
|
|
||||||
if s, ok := statement.(wildcard); ok {
|
|
||||||
one, _, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || one == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
v, err := one.AsString()
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.glob.Match(v)
|
|
||||||
}
|
|
||||||
case KindAll:
|
|
||||||
if s, ok := statement.(quantifier); ok {
|
|
||||||
_, many, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || many == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, n := range many {
|
|
||||||
ok := Match(s.statements, n)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case KindAny:
|
|
||||||
if s, ok := statement.(quantifier); ok {
|
|
||||||
_, many, err := selector.Select(s.selector, node)
|
|
||||||
if err != nil || many == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, n := range many {
|
|
||||||
ok := Match(s.statements, n)
|
|
||||||
if ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
|
||||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
|
||||||
a := must.Int(actual)
|
|
||||||
b := must.Int(expected)
|
|
||||||
return satisfies(cmp.Compare(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
|
|
||||||
a, err := actual.AsFloat()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("extracting node float: %w", err))
|
|
||||||
}
|
|
||||||
b, err := expected.AsFloat()
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("extracting selector float: %w", err))
|
|
||||||
}
|
|
||||||
return satisfies(cmp.Compare(a, b))
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func gt(order int) bool { return order == 1 }
|
|
||||||
func gte(order int) bool { return order == 0 || order == 1 }
|
|
||||||
func lt(order int) bool { return order == -1 }
|
|
||||||
func lte(order int) bool { return order == 0 || order == -1 }
|
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/literal"
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMatch(t *testing.T) {
|
|
||||||
t.Run("equality", func(t *testing.T) {
|
|
||||||
t.Run("string", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.String
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignString("test")
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Equal(selector.MustParse("."), literal.String("test"))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.String("test2"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("int", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Equal(selector.MustParse("."), literal.Int(138))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.Int(1138))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("float", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Float
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignFloat(1.138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Equal(selector.MustParse("."), literal.Float(1.138))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.Float(11.38))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.String("138"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("IPLD Link", func(t *testing.T) {
|
|
||||||
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
|
||||||
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
|
||||||
|
|
||||||
np := basicnode.Prototype.Link
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignLink(l0)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Equal(selector.MustParse("."), literal.Link(l0))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.Link(l1))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse("."), literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("string in map", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Map
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
ma, _ := nb.BeginMap(1)
|
|
||||||
ma.AssembleKey().AssignString("foo")
|
|
||||||
ma.AssembleValue().AssignString("bar")
|
|
||||||
ma.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Equal(selector.MustParse(".foo"), literal.String("bar"))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse(".[\"foo\"]"), literal.String("bar"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse(".foo"), literal.String("baz"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse(".foobar"), literal.String("bar"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("string in list", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.List
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
la, _ := nb.BeginList(1)
|
|
||||||
la.AssembleValue().AssignString("foo")
|
|
||||||
la.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Equal(selector.MustParse(".[0]"), literal.String("foo"))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Equal(selector.MustParse(".[1]"), literal.String("foo"))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("inequality", func(t *testing.T) {
|
|
||||||
t.Run("gt int", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{GreaterThan(selector.MustParse("."), literal.Int(1))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("gte int", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(1))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("gt float", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Float
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignFloat(1.38)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{GreaterThan(selector.MustParse("."), literal.Float(1))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("gte float", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Float
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignFloat(1.38)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{GreaterThanOrEqual(selector.MustParse("."), literal.Float(1.38))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("lt int", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{LessThan(selector.MustParse("."), literal.Int(1138))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("lte int", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(1138))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{LessThanOrEqual(selector.MustParse("."), literal.Int(138))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("negation", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Bool
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignBool(false)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{Not(Equal(selector.MustParse("."), literal.Bool(true)))}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Not(Equal(selector.MustParse("."), literal.Bool(false)))}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("conjunction", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{
|
|
||||||
And(
|
|
||||||
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
|
||||||
LessThan(selector.MustParse("."), literal.Int(1138)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{
|
|
||||||
And(
|
|
||||||
GreaterThan(selector.MustParse("."), literal.Int(1)),
|
|
||||||
Equal(selector.MustParse("."), literal.Int(1138)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{And()}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("disjunction", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.Int
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{
|
|
||||||
Or(
|
|
||||||
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
|
||||||
LessThan(selector.MustParse("."), literal.Int(1138)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{
|
|
||||||
Or(
|
|
||||||
GreaterThan(selector.MustParse("."), literal.Int(138)),
|
|
||||||
Equal(selector.MustParse("."), literal.Int(1138)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{Or()}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("wildcard", func(t *testing.T) {
|
|
||||||
pattern := `Alice\*, Bob*, Carol.`
|
|
||||||
|
|
||||||
for _, s := range []string{
|
|
||||||
"Alice*, Bob, Carol.",
|
|
||||||
"Alice*, Bob, Dan, Erin, Carol.",
|
|
||||||
"Alice*, Bob , Carol.",
|
|
||||||
"Alice*, Bob*, Carol.",
|
|
||||||
} {
|
|
||||||
func(s string) {
|
|
||||||
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.String
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignString(s)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
statement, err := Like(selector.MustParse("."), pattern)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pol := Policy{statement}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
})
|
|
||||||
}(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range []string{
|
|
||||||
"Alice*, Bob, Carol",
|
|
||||||
"Alice*, Bob*, Carol!",
|
|
||||||
"Alice Cooper, Bob, Carol.",
|
|
||||||
"Alice, Bob, Carol.",
|
|
||||||
" Alice*, Bob, Carol. ",
|
|
||||||
} {
|
|
||||||
func(s string) {
|
|
||||||
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.String
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignString(s)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
statement, err := Like(selector.MustParse("."), pattern)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pol := Policy{statement}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
}(s)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("quantification", func(t *testing.T) {
|
|
||||||
buildValueNode := func(v int64) ipld.Node {
|
|
||||||
np := basicnode.Prototype.Map
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
ma, _ := nb.BeginMap(1)
|
|
||||||
ma.AssembleKey().AssignString("value")
|
|
||||||
ma.AssembleValue().AssignInt(v)
|
|
||||||
ma.Finish()
|
|
||||||
return nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("all", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.List
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
la, _ := nb.BeginList(5)
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
|
||||||
la.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{
|
|
||||||
All(
|
|
||||||
selector.MustParse(".[]"),
|
|
||||||
GreaterThan(selector.MustParse(".value"), literal.Int(2)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{
|
|
||||||
All(
|
|
||||||
selector.MustParse(".[]"),
|
|
||||||
GreaterThan(selector.MustParse(".value"), literal.Int(20)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("any", func(t *testing.T) {
|
|
||||||
np := basicnode.Prototype.List
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
la, _ := nb.BeginList(5)
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
|
||||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
|
||||||
la.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := Policy{
|
|
||||||
Any(
|
|
||||||
selector.MustParse(".[]"),
|
|
||||||
GreaterThan(selector.MustParse(".value"), literal.Int(10)),
|
|
||||||
LessThan(selector.MustParse(".value"), literal.Int(50)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok := Match(pol, nd)
|
|
||||||
require.True(t, ok)
|
|
||||||
|
|
||||||
pol = Policy{
|
|
||||||
Any(
|
|
||||||
selector.MustParse(".[]"),
|
|
||||||
GreaterThan(selector.MustParse(".value"), literal.Int(100)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
ok = Match(pol, nd)
|
|
||||||
require.False(t, ok)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package policy
|
|
||||||
|
|
||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gobwas/glob"
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
KindEqual = "==" // implemented by equality
|
|
||||||
KindGreaterThan = ">" // implemented by equality
|
|
||||||
KindGreaterThanOrEqual = ">=" // implemented by equality
|
|
||||||
KindLessThan = "<" // implemented by equality
|
|
||||||
KindLessThanOrEqual = "<=" // implemented by equality
|
|
||||||
KindNot = "not" // implemented by negation
|
|
||||||
KindAnd = "and" // implemented by connective
|
|
||||||
KindOr = "or" // implemented by connective
|
|
||||||
KindLike = "like" // implemented by wildcard
|
|
||||||
KindAll = "all" // implemented by quantifier
|
|
||||||
KindAny = "any" // implemented by quantifier
|
|
||||||
)
|
|
||||||
|
|
||||||
type Policy []Statement
|
|
||||||
|
|
||||||
type Statement interface {
|
|
||||||
Kind() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type equality struct {
|
|
||||||
kind string
|
|
||||||
selector selector.Selector
|
|
||||||
value ipld.Node
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e equality) Kind() string {
|
|
||||||
return e.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func Equal(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{KindEqual, selector, value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThan(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{KindGreaterThan, selector, value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GreaterThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{KindGreaterThanOrEqual, selector, value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThan(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{KindLessThan, selector, value}
|
|
||||||
}
|
|
||||||
|
|
||||||
func LessThanOrEqual(selector selector.Selector, value ipld.Node) Statement {
|
|
||||||
return equality{KindLessThanOrEqual, selector, value}
|
|
||||||
}
|
|
||||||
|
|
||||||
type negation struct {
|
|
||||||
statement Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n negation) Kind() string {
|
|
||||||
return KindNot
|
|
||||||
}
|
|
||||||
|
|
||||||
func Not(stmt Statement) Statement {
|
|
||||||
return negation{stmt}
|
|
||||||
}
|
|
||||||
|
|
||||||
type connective struct {
|
|
||||||
kind string
|
|
||||||
statements []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c connective) Kind() string {
|
|
||||||
return c.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func And(stmts ...Statement) Statement {
|
|
||||||
return connective{KindAnd, stmts}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Or(stmts ...Statement) Statement {
|
|
||||||
return connective{KindOr, stmts}
|
|
||||||
}
|
|
||||||
|
|
||||||
type wildcard struct {
|
|
||||||
selector selector.Selector
|
|
||||||
pattern string
|
|
||||||
glob glob.Glob // not serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n wildcard) Kind() string {
|
|
||||||
return KindLike
|
|
||||||
}
|
|
||||||
|
|
||||||
func Like(selector selector.Selector, pattern string) (Statement, error) {
|
|
||||||
g, err := glob.Compile(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return wildcard{selector, pattern, g}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type quantifier struct {
|
|
||||||
kind string
|
|
||||||
selector selector.Selector
|
|
||||||
statements []Statement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n quantifier) Kind() string {
|
|
||||||
return n.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
func All(selector selector.Selector, policy ...Statement) Statement {
|
|
||||||
return quantifier{KindAll, selector, policy}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Any(selector selector.Selector, policy ...Statement) Statement {
|
|
||||||
return quantifier{KindAny, selector, policy}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Parse(str string) (Selector, error) {
|
|
||||||
if len(str) == 0 {
|
|
||||||
return nil, newParseError("empty selector", str, 0, "")
|
|
||||||
}
|
|
||||||
if string(str[0]) != "." {
|
|
||||||
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
|
||||||
}
|
|
||||||
|
|
||||||
col := 0
|
|
||||||
var sel Selector
|
|
||||||
for _, tok := range tokenize(str) {
|
|
||||||
seg := tok
|
|
||||||
opt := strings.HasSuffix(tok, "?")
|
|
||||||
if opt {
|
|
||||||
seg = tok[0 : len(tok)-1]
|
|
||||||
}
|
|
||||||
switch seg {
|
|
||||||
case ".":
|
|
||||||
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)
|
|
||||||
case "[]":
|
|
||||||
sel = append(sel, segment{tok, false, opt, true, nil, "", 0})
|
|
||||||
default:
|
|
||||||
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
|
|
||||||
lookup := seg[1 : len(seg)-1]
|
|
||||||
|
|
||||||
if indexRegex.MatchString(lookup) { // index
|
|
||||||
idx, err := strconv.Atoi(lookup)
|
|
||||||
if err != nil {
|
|
||||||
return nil, newParseError("invalid index", str, col, tok)
|
|
||||||
}
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, index: idx})
|
|
||||||
} else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]})
|
|
||||||
} else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:]
|
|
||||||
var rng []int
|
|
||||||
splt := strings.Split(lookup, ":")
|
|
||||||
if splt[0] == "" {
|
|
||||||
rng = append(rng, 0)
|
|
||||||
} else {
|
|
||||||
i, err := strconv.Atoi(splt[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, newParseError("invalid slice index", str, col, tok)
|
|
||||||
}
|
|
||||||
rng = append(rng, i)
|
|
||||||
}
|
|
||||||
if splt[1] != "" {
|
|
||||||
i, err := strconv.Atoi(splt[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, newParseError("invalid slice index", str, col, tok)
|
|
||||||
}
|
|
||||||
rng = append(rng, i)
|
|
||||||
}
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, slice: rng})
|
|
||||||
} else {
|
|
||||||
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
|
||||||
}
|
|
||||||
} else if fieldRegex.MatchString(seg) {
|
|
||||||
sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]})
|
|
||||||
} else {
|
|
||||||
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
col += len(tok)
|
|
||||||
}
|
|
||||||
return sel, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MustParse(sel string) Selector {
|
|
||||||
s, err := Parse(sel)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func tokenize(str string) []string {
|
|
||||||
var toks []string
|
|
||||||
col := 0
|
|
||||||
ofs := 0
|
|
||||||
ctx := ""
|
|
||||||
|
|
||||||
for col < len(str) {
|
|
||||||
char := string(str[col])
|
|
||||||
|
|
||||||
if char == "\"" && string(str[col-1]) != "\\" {
|
|
||||||
col++
|
|
||||||
if ctx == "\"" {
|
|
||||||
ctx = ""
|
|
||||||
} else {
|
|
||||||
ctx = "\""
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx == "\"" {
|
|
||||||
col++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if char == "." || char == "[" {
|
|
||||||
if ofs < col {
|
|
||||||
toks = append(toks, str[ofs:col])
|
|
||||||
}
|
|
||||||
ofs = col
|
|
||||||
}
|
|
||||||
col++
|
|
||||||
}
|
|
||||||
|
|
||||||
if ofs < col && ctx != "\"" {
|
|
||||||
toks = append(toks, str[ofs:col])
|
|
||||||
}
|
|
||||||
|
|
||||||
return toks
|
|
||||||
}
|
|
||||||
|
|
||||||
type parseerr struct {
|
|
||||||
msg string
|
|
||||||
src string
|
|
||||||
col int
|
|
||||||
tok string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseerr) Name() string {
|
|
||||||
return "ParseError"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseerr) Message() string {
|
|
||||||
return p.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseerr) Column() int {
|
|
||||||
return p.col
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseerr) Error() string {
|
|
||||||
return p.msg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseerr) Source() string {
|
|
||||||
return p.src
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p parseerr) Token() string {
|
|
||||||
return p.tok
|
|
||||||
}
|
|
||||||
|
|
||||||
func newParseError(message string, source string, column int, token string) error {
|
|
||||||
return parseerr{message, source, column, token}
|
|
||||||
}
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Selector describes a UCAN policy selector, as specified here:
|
|
||||||
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
|
||||||
type Selector []segment
|
|
||||||
|
|
||||||
func (s Selector) String() string {
|
|
||||||
var res strings.Builder
|
|
||||||
for _, seg := range s {
|
|
||||||
res.WriteString(seg.String())
|
|
||||||
}
|
|
||||||
return res.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
var Identity = segment{".", true, false, false, nil, "", 0}
|
|
||||||
|
|
||||||
var (
|
|
||||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
|
||||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
|
||||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
type segment struct {
|
|
||||||
str string
|
|
||||||
identity bool
|
|
||||||
optional bool
|
|
||||||
iterator bool
|
|
||||||
slice []int
|
|
||||||
field string
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the segment's string representation.
|
|
||||||
func (s segment) String() string {
|
|
||||||
return s.str
|
|
||||||
}
|
|
||||||
|
|
||||||
// Identity flags that this selector is the identity selector.
|
|
||||||
func (s segment) Identity() bool {
|
|
||||||
return s.identity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional flags that this selector is optional.
|
|
||||||
func (s segment) Optional() bool {
|
|
||||||
return s.optional
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterator flags that this selector is an iterator segment.
|
|
||||||
func (s segment) Iterator() bool {
|
|
||||||
return s.iterator
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slice flags that this segment targets a range of a slice.
|
|
||||||
func (s segment) Slice() []int {
|
|
||||||
return s.slice
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field is the name of a field in a struct/map.
|
|
||||||
func (s segment) Field() string {
|
|
||||||
return s.field
|
|
||||||
}
|
|
||||||
|
|
||||||
// Index is an index of a slice.
|
|
||||||
func (s segment) Index() int {
|
|
||||||
return s.index
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select uses a selector to extract an IPLD node or set of nodes from the
|
|
||||||
// passed subject node.
|
|
||||||
func Select(sel Selector, subject ipld.Node) (ipld.Node, []ipld.Node, error) {
|
|
||||||
return resolve(sel, subject, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) {
|
|
||||||
cur := subject
|
|
||||||
for i, seg := range sel {
|
|
||||||
if seg.Identity() {
|
|
||||||
continue
|
|
||||||
} else if seg.Iterator() {
|
|
||||||
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
|
||||||
var many []ipld.Node
|
|
||||||
it := cur.ListIterator()
|
|
||||||
for {
|
|
||||||
if it.Done() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
k, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key := fmt.Sprintf("%d", k)
|
|
||||||
o, m, err := resolve(sel[i+1:], v, append(at[:], key))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m != nil {
|
|
||||||
many = append(many, m...)
|
|
||||||
} else {
|
|
||||||
many = append(many, o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, many, nil
|
|
||||||
} else if cur != nil && cur.Kind() == datamodel.Kind_Map {
|
|
||||||
var many []ipld.Node
|
|
||||||
it := cur.MapIterator()
|
|
||||||
for {
|
|
||||||
if it.Done() {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
k, v, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, _ := k.AsString()
|
|
||||||
o, m, err := resolve(sel[i+1:], v, append(at[:], key))
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m != nil {
|
|
||||||
many = append(many, m...)
|
|
||||||
} else {
|
|
||||||
many = append(many, o)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, many, nil
|
|
||||||
} else if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if seg.Field() != "" {
|
|
||||||
at = append(at, seg.Field())
|
|
||||||
if cur != nil && cur.Kind() == datamodel.Kind_Map {
|
|
||||||
n, err := cur.LookupByString(seg.Field())
|
|
||||||
if err != nil {
|
|
||||||
if isMissing(err) {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cur = n
|
|
||||||
} else if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
} else if seg.Slice() != nil {
|
|
||||||
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
|
||||||
return nil, nil, newResolutionError("list slice selection not yet implemented", at)
|
|
||||||
} else if cur != nil && cur.Kind() == datamodel.Kind_Bytes {
|
|
||||||
return nil, nil, newResolutionError("bytes slice selection not yet implemented", at)
|
|
||||||
} else if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not index: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
at = append(at, fmt.Sprintf("%d", seg.Index()))
|
|
||||||
if cur != nil && cur.Kind() == datamodel.Kind_List {
|
|
||||||
idx := int64(seg.Index())
|
|
||||||
if idx < 0 {
|
|
||||||
idx = cur.Length() + idx
|
|
||||||
}
|
|
||||||
n, err := cur.LookupByIndex(idx)
|
|
||||||
if err != nil {
|
|
||||||
if isMissing(err) {
|
|
||||||
if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cur = n
|
|
||||||
} else if seg.Optional() {
|
|
||||||
cur = nil
|
|
||||||
} else {
|
|
||||||
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cur, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func kindString(n datamodel.Node) string {
|
|
||||||
if n == nil {
|
|
||||||
return "null"
|
|
||||||
}
|
|
||||||
return n.Kind().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMissing(err error) bool {
|
|
||||||
if _, ok := err.(datamodel.ErrNotExists); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := err.(schema.ErrNoSuchField); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, ok := err.(schema.ErrInvalidKey); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type resolutionerr struct {
|
|
||||||
msg string
|
|
||||||
at []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) Name() string {
|
|
||||||
return "ResolutionError"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) Message() string {
|
|
||||||
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) At() []string {
|
|
||||||
return r.at
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r resolutionerr) Error() string {
|
|
||||||
return r.Message()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newResolutionError(message string, at []string) error {
|
|
||||||
return resolutionerr{message, at}
|
|
||||||
}
|
|
||||||
@@ -1,499 +0,0 @@
|
|||||||
package selector
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
"github.com/ipld/go-ipld-prime/must"
|
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
t.Run("identity", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("field", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".foo")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("explicit field", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.["foo"]`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Equal(t, sel[1].Field(), "foo")
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[138]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), 138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("negative index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[-138]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), -138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("iterator", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.True(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional field", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".foo?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.True(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional explicit field", func(t *testing.T) {
|
|
||||||
sel, err := Parse(`.["foo"]?`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Equal(t, sel[1].Field(), "foo")
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional index", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[138]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Equal(t, sel[1].Index(), 138)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional iterator", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".[]?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(sel))
|
|
||||||
require.True(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Empty(t, sel[0].Field())
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.False(t, sel[1].Identity())
|
|
||||||
require.True(t, sel[1].Optional())
|
|
||||||
require.True(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nesting", func(t *testing.T) {
|
|
||||||
str := `.foo.["bar"].[138]?.baz[1:]`
|
|
||||||
sel, err := Parse(str)
|
|
||||||
require.NoError(t, err)
|
|
||||||
printSegments(sel)
|
|
||||||
require.Equal(t, str, sel.String())
|
|
||||||
require.Equal(t, 7, len(sel))
|
|
||||||
require.False(t, sel[0].Identity())
|
|
||||||
require.False(t, sel[0].Optional())
|
|
||||||
require.False(t, sel[0].Iterator())
|
|
||||||
require.Empty(t, sel[0].Slice())
|
|
||||||
require.Equal(t, sel[0].Field(), "foo")
|
|
||||||
require.Empty(t, sel[0].Index())
|
|
||||||
require.True(t, sel[1].Identity())
|
|
||||||
require.False(t, sel[1].Optional())
|
|
||||||
require.False(t, sel[1].Iterator())
|
|
||||||
require.Empty(t, sel[1].Slice())
|
|
||||||
require.Empty(t, sel[1].Field())
|
|
||||||
require.Empty(t, sel[1].Index())
|
|
||||||
require.False(t, sel[2].Identity())
|
|
||||||
require.False(t, sel[2].Optional())
|
|
||||||
require.False(t, sel[2].Iterator())
|
|
||||||
require.Empty(t, sel[2].Slice())
|
|
||||||
require.Equal(t, sel[2].Field(), "bar")
|
|
||||||
require.Empty(t, sel[2].Index())
|
|
||||||
require.True(t, sel[3].Identity())
|
|
||||||
require.False(t, sel[3].Optional())
|
|
||||||
require.False(t, sel[3].Iterator())
|
|
||||||
require.Empty(t, sel[3].Slice())
|
|
||||||
require.Empty(t, sel[3].Field())
|
|
||||||
require.Empty(t, sel[3].Index())
|
|
||||||
require.False(t, sel[4].Identity())
|
|
||||||
require.True(t, sel[4].Optional())
|
|
||||||
require.False(t, sel[4].Iterator())
|
|
||||||
require.Empty(t, sel[4].Slice())
|
|
||||||
require.Empty(t, sel[4].Field())
|
|
||||||
require.Equal(t, sel[4].Index(), 138)
|
|
||||||
require.False(t, sel[5].Identity())
|
|
||||||
require.False(t, sel[5].Optional())
|
|
||||||
require.False(t, sel[5].Iterator())
|
|
||||||
require.Empty(t, sel[5].Slice())
|
|
||||||
require.Equal(t, sel[5].Field(), "baz")
|
|
||||||
require.Empty(t, sel[5].Index())
|
|
||||||
require.False(t, sel[6].Identity())
|
|
||||||
require.False(t, sel[6].Optional())
|
|
||||||
require.False(t, sel[6].Iterator())
|
|
||||||
require.Equal(t, sel[6].Slice(), []int{1})
|
|
||||||
require.Empty(t, sel[6].Field())
|
|
||||||
require.Empty(t, sel[6].Index())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non dotted", func(t *testing.T) {
|
|
||||||
_, err := Parse("foo")
|
|
||||||
require.NotNil(t, err)
|
|
||||||
fmt.Println(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("non quoted", func(t *testing.T) {
|
|
||||||
_, err := Parse(".[foo]")
|
|
||||||
require.NotNil(t, err)
|
|
||||||
fmt.Println(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func printSegments(s Selector) {
|
|
||||||
for i, seg := range s {
|
|
||||||
fmt.Printf("%d: %s\n", i, seg.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSelect(t *testing.T) {
|
|
||||||
type name struct {
|
|
||||||
First string
|
|
||||||
Middle *string
|
|
||||||
Last string
|
|
||||||
}
|
|
||||||
type interest struct {
|
|
||||||
Name string
|
|
||||||
Outdoor bool
|
|
||||||
Experience int
|
|
||||||
}
|
|
||||||
type user struct {
|
|
||||||
Name name
|
|
||||||
Age int
|
|
||||||
Nationalities []string
|
|
||||||
Interests []interest
|
|
||||||
}
|
|
||||||
|
|
||||||
ts, err := ipld.LoadSchemaBytes([]byte(`
|
|
||||||
type User struct {
|
|
||||||
name Name
|
|
||||||
age Int
|
|
||||||
nationalities [String]
|
|
||||||
interests [Interest]
|
|
||||||
}
|
|
||||||
type Name struct {
|
|
||||||
first String
|
|
||||||
middle optional String
|
|
||||||
last String
|
|
||||||
}
|
|
||||||
type Interest struct {
|
|
||||||
name String
|
|
||||||
outdoor Bool
|
|
||||||
experience Int
|
|
||||||
}
|
|
||||||
`))
|
|
||||||
require.NoError(t, err)
|
|
||||||
typ := ts.TypeByName("User")
|
|
||||||
|
|
||||||
am := "Joan"
|
|
||||||
alice := user{
|
|
||||||
Name: name{First: "Alice", Middle: &am, Last: "Wonderland"},
|
|
||||||
Age: 24,
|
|
||||||
Nationalities: []string{"British"},
|
|
||||||
Interests: []interest{
|
|
||||||
{Name: "Cycling", Outdoor: true, Experience: 4},
|
|
||||||
{Name: "Chess", Outdoor: false, Experience: 2},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bob := user{
|
|
||||||
Name: name{First: "Bob", Last: "Builder"},
|
|
||||||
Age: 35,
|
|
||||||
Nationalities: []string{"Canadian", "South African"},
|
|
||||||
Interests: []interest{
|
|
||||||
{Name: "Snowboarding", Outdoor: true, Experience: 8},
|
|
||||||
{Name: "Reading", Outdoor: false, Experience: 25},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
anode := bindnode.Wrap(&alice, typ)
|
|
||||||
bnode := bindnode.Wrap(&bob, typ)
|
|
||||||
|
|
||||||
t.Run("identity", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(one))
|
|
||||||
|
|
||||||
age := must.Int(must.Node(one.LookupByString("age")))
|
|
||||||
require.Equal(t, int64(alice.Age), age)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("nested property", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.first")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(one))
|
|
||||||
|
|
||||||
name := must.String(one)
|
|
||||||
require.Equal(t, alice.Name.First, name)
|
|
||||||
|
|
||||||
one, many, err = Select(sel, bnode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(one))
|
|
||||||
|
|
||||||
name = must.String(one)
|
|
||||||
require.Equal(t, bob.Name.First, name)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional nested property", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.middle?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotEmpty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(one))
|
|
||||||
|
|
||||||
name := must.String(one)
|
|
||||||
require.Equal(t, *alice.Name.Middle, name)
|
|
||||||
|
|
||||||
one, many, err = Select(sel, bnode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("not exists", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.foo")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Empty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
|
|
||||||
fmt.Println(err)
|
|
||||||
|
|
||||||
require.ErrorAs(t, err, &resolutionerr{}, "error was not a resolution error")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("optional not exists", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".name.foo?")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, one)
|
|
||||||
require.Empty(t, many)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("iterator", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".interests[]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, one)
|
|
||||||
require.NotEmpty(t, many)
|
|
||||||
|
|
||||||
for _, n := range many {
|
|
||||||
fmt.Println(printer.Sprint(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
iname := must.String(must.Node(many[0].LookupByString("name")))
|
|
||||||
require.Equal(t, alice.Interests[0].Name, iname)
|
|
||||||
|
|
||||||
iname = must.String(must.Node(many[1].LookupByString("name")))
|
|
||||||
require.Equal(t, alice.Interests[1].Name, iname)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("map iterator", func(t *testing.T) {
|
|
||||||
sel, err := Parse(".interests[0][]")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
one, many, err := Select(sel, anode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Empty(t, one)
|
|
||||||
require.NotEmpty(t, many)
|
|
||||||
|
|
||||||
for _, n := range many {
|
|
||||||
fmt.Println(printer.Sprint(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, alice.Interests[0].Name, must.String(many[0]))
|
|
||||||
require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2])))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzParse(f *testing.F) {
|
|
||||||
selectorCorpus := []string{
|
|
||||||
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
|
||||||
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
|
||||||
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
|
||||||
`.length`, `.[4]`,
|
|
||||||
}
|
|
||||||
for _, selector := range selectorCorpus {
|
|
||||||
f.Add(selector)
|
|
||||||
}
|
|
||||||
f.Fuzz(func(t *testing.T, selector string) {
|
|
||||||
// only look for panic()
|
|
||||||
_, _ = Parse(selector)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func FuzzParseAndSelect(f *testing.F) {
|
|
||||||
selectorCorpus := []string{
|
|
||||||
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
|
||||||
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
|
||||||
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
|
||||||
`.length`, `.[4]`,
|
|
||||||
}
|
|
||||||
subjectCorpus := []string{
|
|
||||||
`{"x":1}`, `[1, 2]`, `null`, `[[1], 2, [3]]`, `{"x": 1 }`, `{"x": 1}`,
|
|
||||||
`[1, 2]`, `[1, 2]`, `"Hi"`, `{"/":{"bytes":"AAE"}`, `[0, 1, 2]`,
|
|
||||||
`[0, 1, 2]`, `[0, 1, 2]`, `"hello"`, `{"/":{"bytes":"AAEC"}}`, `{}`,
|
|
||||||
`null`, `[]`, `{}`, `[1, 2]`, `[0, 1]`, `null`, `[[1], 2, [3]]`, `{}`,
|
|
||||||
`null`, `[]`, `[1, 2]`, `[0, 1]`,
|
|
||||||
}
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
switch {
|
|
||||||
case i < len(selectorCorpus) && i < len(subjectCorpus):
|
|
||||||
f.Add(selectorCorpus[i], subjectCorpus[i])
|
|
||||||
continue
|
|
||||||
case i > len(selectorCorpus):
|
|
||||||
f.Add("", subjectCorpus[i])
|
|
||||||
continue
|
|
||||||
case i > len(subjectCorpus):
|
|
||||||
f.Add(selectorCorpus[i], "")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, selector, subject string) {
|
|
||||||
sel, err := Parse(selector)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
np := basicnode.Prototype.Any
|
|
||||||
nb := np.NewBuilder()
|
|
||||||
err = dagjson.Decode(nb, strings.NewReader(subject))
|
|
||||||
if err != nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
node := nb.Build()
|
|
||||||
if node == nil {
|
|
||||||
t.Skip()
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for panic()
|
|
||||||
_, _, _ = Select(sel, node)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *PayloadModel) EncodeDagCbor() ([]byte, error) {
|
|
||||||
return ipld.Marshal(dagcbor.Encode, p, PayloadType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PayloadModel) EncodeDagJson() ([]byte, error) {
|
|
||||||
return ipld.Marshal(dagjson.Encode, p, PayloadType())
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeDagCbor(data []byte) (*PayloadModel, error) {
|
|
||||||
var p PayloadModel
|
|
||||||
_, err := ipld.Unmarshal(data, dagcbor.Decode, &p, PayloadType())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeDagJson(data []byte) (*PayloadModel, error) {
|
|
||||||
var p PayloadModel
|
|
||||||
_, err := ipld.Unmarshal(data, dagjson.Decode, &p, PayloadType())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &p, nil
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed delegation.ipldsch
|
|
||||||
var schemaBytes []byte
|
|
||||||
|
|
||||||
var (
|
|
||||||
once sync.Once
|
|
||||||
ts *schema.TypeSystem
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
func mustLoadSchema() *schema.TypeSystem {
|
|
||||||
once.Do(func() {
|
|
||||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
|
||||||
}
|
|
||||||
return ts
|
|
||||||
}
|
|
||||||
|
|
||||||
func PayloadType() schema.Type {
|
|
||||||
return mustLoadSchema().TypeByName("Payload")
|
|
||||||
}
|
|
||||||
|
|
||||||
type PayloadModel struct {
|
|
||||||
// Issuer DID (sender)
|
|
||||||
Iss string
|
|
||||||
// Audience DID (receiver)
|
|
||||||
Aud string
|
|
||||||
// Principal that the chain is about (the Subject)
|
|
||||||
// optional: can be nil
|
|
||||||
Sub *string
|
|
||||||
|
|
||||||
// The Command to eventually invoke
|
|
||||||
Cmd string
|
|
||||||
|
|
||||||
// The delegation policy
|
|
||||||
Pol datamodel.Node
|
|
||||||
|
|
||||||
// A unique, random nonce
|
|
||||||
Nonce []byte
|
|
||||||
|
|
||||||
// Arbitrary Metadata
|
|
||||||
// optional: can be nil
|
|
||||||
Meta MetaModel
|
|
||||||
|
|
||||||
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
|
||||||
// optional: can be nil
|
|
||||||
Nbf *int64
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
|
||||||
// optional: can be nil
|
|
||||||
Exp *int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type MetaModel struct {
|
|
||||||
Keys []string
|
|
||||||
Values map[string]datamodel.Node
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSchemaRoundTrip(t *testing.T) {
|
|
||||||
const delegationJson = `
|
|
||||||
{
|
|
||||||
"aud":"did:key:def456",
|
|
||||||
"cmd":"/foo/bar",
|
|
||||||
"exp":123456,
|
|
||||||
"iss":"did:key:abc123",
|
|
||||||
"meta":{
|
|
||||||
"bar":"baaar",
|
|
||||||
"foo":"fooo"
|
|
||||||
},
|
|
||||||
"nbf":123456,
|
|
||||||
"nonce":{
|
|
||||||
"/":{
|
|
||||||
"bytes":"c3VwZXItcmFuZG9t"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pol":[
|
|
||||||
["==", ".status", "draft"],
|
|
||||||
["all", ".reviewer", [
|
|
||||||
["like", ".email", "*@example.com"]]
|
|
||||||
],
|
|
||||||
["any", ".tags", [
|
|
||||||
["or", [
|
|
||||||
["==", ".", "news"],
|
|
||||||
["==", ".", "press"]]
|
|
||||||
]]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"sub":""
|
|
||||||
}
|
|
||||||
`
|
|
||||||
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
|
||||||
// function: DecodeDagJson() EncodeDagCbor() DecodeDagCbor() EncodeDagJson()
|
|
||||||
|
|
||||||
p1, err := DecodeDagJson([]byte(delegationJson))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cborBytes, err := p1.EncodeDagCbor()
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("cborBytes length", len(cborBytes))
|
|
||||||
fmt.Println("cbor", string(cborBytes))
|
|
||||||
|
|
||||||
p2, err := DecodeDagCbor(cborBytes)
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("read Cbor", p2)
|
|
||||||
|
|
||||||
readJson, err := p2.EncodeDagJson()
|
|
||||||
require.NoError(t, err)
|
|
||||||
fmt.Println("readJson length", len(readJson))
|
|
||||||
fmt.Println("json: ", string(readJson))
|
|
||||||
|
|
||||||
require.JSONEq(t, delegationJson, string(readJson))
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkSchemaLoad(b *testing.B) {
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, _ = ipld.LoadSchemaBytes(schemaBytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package delegation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy"
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/did"
|
|
||||||
)
|
|
||||||
|
|
||||||
type View struct {
|
|
||||||
// Issuer DID (sender)
|
|
||||||
Issuer did.DID
|
|
||||||
// Audience DID (receiver)
|
|
||||||
Audience did.DID
|
|
||||||
// Principal that the chain is about (the Subject)
|
|
||||||
Subject did.DID
|
|
||||||
// The Command to eventually invoke
|
|
||||||
Command *command.Command
|
|
||||||
// The delegation policy
|
|
||||||
Policy policy.Policy
|
|
||||||
// A unique, random nonce
|
|
||||||
Nonce []byte
|
|
||||||
// Arbitrary Metadata
|
|
||||||
Meta map[string]datamodel.Node
|
|
||||||
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
|
||||||
NotBefore time.Time
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
|
||||||
Expiration time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewFromModel build a decoded view of the raw IPLD data.
|
|
||||||
// This function also serves as validation.
|
|
||||||
func ViewFromModel(m PayloadModel) (*View, error) {
|
|
||||||
var view View
|
|
||||||
var err error
|
|
||||||
|
|
||||||
view.Issuer, err = did.Parse(m.Iss)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse iss: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.Audience, err = did.Parse(m.Aud)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse audience: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Sub != nil {
|
|
||||||
view.Subject, err = did.Parse(*m.Sub)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse subject: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
view.Subject = did.Undef
|
|
||||||
}
|
|
||||||
|
|
||||||
view.Command, err = command.Parse(m.Cmd)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
view.Policy, err = policy.FromIPLD(m.Pol)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse policy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.Nonce) == 0 {
|
|
||||||
return nil, fmt.Errorf("nonce is required")
|
|
||||||
}
|
|
||||||
view.Nonce = m.Nonce
|
|
||||||
|
|
||||||
// TODO: copy?
|
|
||||||
view.Meta = m.Meta.Values
|
|
||||||
|
|
||||||
if m.Nbf != nil {
|
|
||||||
view.NotBefore = time.Unix(*m.Nbf, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Exp != nil {
|
|
||||||
view.Expiration = time.Unix(*m.Exp, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &view, nil
|
|
||||||
}
|
|
||||||
31
did/README.md
Normal file
31
did/README.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## did
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
The test suite for this package includes test vectors provided by the
|
||||||
|
authors of the [`did:key` method specification](https://w3c-ccg.github.io/did-method-key/).
|
||||||
|
Some of these tests provide the public key associated with a `did:key`
|
||||||
|
as JWKs and an extra (test-only) dependency has been added to unmarshal
|
||||||
|
the JWK into a Go `struct`. Support for the `secp256k1` encryption
|
||||||
|
algorithm is experimental (but stable in my experience) and requires the
|
||||||
|
addition of the following build tag to properly run:
|
||||||
|
|
||||||
|
```
|
||||||
|
// go:build jwx_es256k
|
||||||
|
```
|
||||||
|
|
||||||
|
WARNING: These tests will not run by default!
|
||||||
|
|
||||||
|
To include these tests from the CLI, execute the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
go test -v ./did -tags jwx_es256k
|
||||||
|
```
|
||||||
|
|
||||||
|
It should also be possible to configure your IDE to run these tests. For
|
||||||
|
instance, in Codium, add the following JSON snippet to your local project
|
||||||
|
configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
"go.testTags": "jwx_es256k",
|
||||||
|
```
|
||||||
231
did/crypto.go
Normal file
231
did/crypto.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package did
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
|
"github.com/multiformats/go-multicodec"
|
||||||
|
"github.com/multiformats/go-varint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateEd25519 generates an Ed25519 private key and the matching DID.
|
||||||
|
// This is the RECOMMENDED algorithm.
|
||||||
|
func GenerateEd25519() (crypto.PrivKey, DID, error) {
|
||||||
|
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Undef, nil
|
||||||
|
}
|
||||||
|
did, err := FromPubKey(pub)
|
||||||
|
return priv, did, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRSA generates a RSA private key and the matching DID.
|
||||||
|
func GenerateRSA() (crypto.PrivKey, DID, error) {
|
||||||
|
// NIST Special Publication 800-57 Part 1 Revision 5
|
||||||
|
// Section 5.6.1.1 (Table 2)
|
||||||
|
// Paraphrased: 2048-bit RSA keys are secure until 2030 and 3072-bit keys are recommended for longer-term security.
|
||||||
|
const keyLength = 3072
|
||||||
|
|
||||||
|
priv, pub, err := crypto.GenerateRSAKeyPair(keyLength, rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Undef, nil
|
||||||
|
}
|
||||||
|
did, err := FromPubKey(pub)
|
||||||
|
return priv, did, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateEd25519 generates a Secp256k1 private key and the matching DID.
|
||||||
|
func GenerateSecp256k1() (crypto.PrivKey, DID, error) {
|
||||||
|
priv, pub, err := crypto.GenerateSecp256k1Key(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Undef, nil
|
||||||
|
}
|
||||||
|
did, err := FromPubKey(pub)
|
||||||
|
return priv, did, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateECDSA generates an ECDSA private key and the matching DID
|
||||||
|
// for the default P256 curve.
|
||||||
|
func GenerateECDSA() (crypto.PrivKey, DID, error) {
|
||||||
|
return GenerateECDSAWithCurve(P256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateECDSAWithCurve generates an ECDSA private key and matching
|
||||||
|
// DID for the user-supplied curve
|
||||||
|
func GenerateECDSAWithCurve(code multicodec.Code) (crypto.PrivKey, DID, error) {
|
||||||
|
var curve elliptic.Curve
|
||||||
|
|
||||||
|
switch code {
|
||||||
|
case P256:
|
||||||
|
curve = elliptic.P256()
|
||||||
|
case P384:
|
||||||
|
curve = elliptic.P384()
|
||||||
|
case P521:
|
||||||
|
curve = elliptic.P521()
|
||||||
|
default:
|
||||||
|
return nil, Undef, errors.New("unsupported ECDSA curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, pub, err := crypto.GenerateECDSAKeyPairWithCurve(curve, rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
did, err := FromPubKey(pub)
|
||||||
|
|
||||||
|
return priv, did, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromPrivKey is a convenience function that returns the DID associated
|
||||||
|
// with the public key associated with the provided private key.
|
||||||
|
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
|
||||||
|
return FromPubKey(privKey.GetPublic())
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromPubKey returns a did:key constructed from the provided public key.
|
||||||
|
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
|
||||||
|
var code multicodec.Code
|
||||||
|
|
||||||
|
switch pubKey.Type() {
|
||||||
|
case pb.KeyType_Ed25519:
|
||||||
|
code = multicodec.Ed25519Pub
|
||||||
|
case pb.KeyType_RSA:
|
||||||
|
code = RSA
|
||||||
|
case pb.KeyType_Secp256k1:
|
||||||
|
code = Secp256k1
|
||||||
|
case pb.KeyType_ECDSA:
|
||||||
|
var err error
|
||||||
|
if code, err = codeForCurve(pubKey); err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return Undef, errors.New("unsupported key type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubKey.Type() == pb.KeyType_ECDSA && code == Secp256k1 {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
pubKey, err = coerceECDSAToSecp256k1(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes []byte
|
||||||
|
|
||||||
|
switch pubKey.Type() {
|
||||||
|
case pb.KeyType_ECDSA:
|
||||||
|
pkix, err := pubKey.Raw()
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err := x509.ParsePKIXPublicKey(pkix)
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ecdsaPublicKey := publicKey.(*ecdsa.PublicKey)
|
||||||
|
|
||||||
|
bytes = elliptic.MarshalCompressed(ecdsaPublicKey.Curve, ecdsaPublicKey.X, ecdsaPublicKey.Y)
|
||||||
|
case pb.KeyType_Ed25519, pb.KeyType_Secp256k1:
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if bytes, err = pubKey.Raw(); err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
case pb.KeyType_RSA:
|
||||||
|
var err error
|
||||||
|
|
||||||
|
pkix, err := pubKey.Raw()
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err := x509.ParsePKIXPublicKey(pkix)
|
||||||
|
if err != nil {
|
||||||
|
return Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes = x509.MarshalPKCS1PublicKey(publicKey.(*rsa.PublicKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
return DID{
|
||||||
|
code: code,
|
||||||
|
bytes: string(append(varint.ToUvarint(uint64(code)), bytes...)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPubKey returns the crypto.PubKey encapsulated in the DID formed by
|
||||||
|
// parsing the provided string.
|
||||||
|
func ToPubKey(s string) (crypto.PubKey, error) {
|
||||||
|
id, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id.PubKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
func codeForCurve(pubKey crypto.PubKey) (multicodec.Code, error) {
|
||||||
|
stdPub, err := crypto.PubKeyToStdKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return multicodec.Identity, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return multicodec.Identity, errors.New("failed to assert type for code to curve")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ecdsaPub.Curve {
|
||||||
|
case elliptic.P256():
|
||||||
|
return P256, nil
|
||||||
|
case elliptic.P384():
|
||||||
|
return P384, nil
|
||||||
|
case elliptic.P521():
|
||||||
|
return P521, nil
|
||||||
|
case secp256k1.S256():
|
||||||
|
return Secp256k1, nil
|
||||||
|
default:
|
||||||
|
return multicodec.Identity, fmt.Errorf("unsupported ECDSA curve: %s", ecdsaPub.Curve.Params().Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// secp256k1.S256 is a valid ECDSA curve, but the go-libp2p/core/crypto
|
||||||
|
// package treats it as a different type and has a different format for
|
||||||
|
// the raw bytes of the public key.
|
||||||
|
//
|
||||||
|
// If a valid ECDSA public key was created using the secp256k1.S256 curve,
|
||||||
|
// this function will "convert" it from a crypto.ECDSAPubKey to a
|
||||||
|
// crypto.Secp256k1PublicKey.
|
||||||
|
func coerceECDSAToSecp256k1(pubKey crypto.PubKey) (crypto.PubKey, error) {
|
||||||
|
stdPub, err := crypto.PubKeyToStdKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("failed to assert type for secp256k1 coersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
ecdsaPubBytes := append([]byte{0x04}, append(ecdsaPub.X.Bytes(), ecdsaPub.Y.Bytes()...)...)
|
||||||
|
|
||||||
|
secp256k1Pub, err := secp256k1.ParsePubKey(ecdsaPubBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoPub := crypto.Secp256k1PublicKey(*secp256k1Pub)
|
||||||
|
|
||||||
|
return &cryptoPub, nil
|
||||||
|
}
|
||||||
108
did/crypto_test.go
Normal file
108
did/crypto_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package did_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||||
|
"github.com/multiformats/go-multicodec"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
exampleDIDStr = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
|
||||||
|
examplePubKeyStr = "Lm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY="
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFromPubKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, ecdsaP256, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ecdsaP384, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P384(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ecdsaP521, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P521(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ecdsaSecp256k1, err := crypto.GenerateECDSAKeyPairWithCurve(secp256k1.S256(), rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, ed25519, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, rsa, err := crypto.GenerateRSAKeyPair(2048, rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, secp256k1PubKey1, err := crypto.GenerateSecp256k1Key(rand.Reader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
test := func(pub crypto.PubKey, code multicodec.Code) func(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(pub)
|
||||||
|
require.NoError(t, err)
|
||||||
|
p, err := id.PubKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, pub, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ECDSA with P256 curve", test(ecdsaP256, did.P256))
|
||||||
|
t.Run("ECDSA with P384 curve", test(ecdsaP384, did.P384))
|
||||||
|
t.Run("ECDSA with P521 curve", test(ecdsaP521, did.P521))
|
||||||
|
t.Run("Ed25519", test(ed25519, did.Ed25519))
|
||||||
|
t.Run("RSA", test(rsa, did.RSA))
|
||||||
|
t.Run("secp256k1", test(secp256k1PubKey1, did.Secp256k1))
|
||||||
|
|
||||||
|
t.Run("ECDSA with secp256k1 curve (coerced)", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(ecdsaSecp256k1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
p, err := id.PubKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, pb.KeyType_Secp256k1, p.Type())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unmarshaled example key (secp256k1)", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(examplePubKey(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, exampleDID(t), id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToPubKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
pubKey, err := did.ToPubKey(exampleDIDStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, examplePubKey(t), pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exampleDID(t *testing.T) did.DID {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
id, err := did.Parse(exampleDIDStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePubKey(t *testing.T) crypto.PubKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pubKeyCfg, err := crypto.ConfigDecodeKey(examplePubKeyStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pubKey, err := crypto.UnmarshalEd25519PublicKey(pubKeyCfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return pubKey
|
||||||
|
}
|
||||||
161
did/did.go
161
did/did.go
@@ -1,86 +1,143 @@
|
|||||||
package did
|
package did
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||||
mbase "github.com/multiformats/go-multibase"
|
mbase "github.com/multiformats/go-multibase"
|
||||||
|
"github.com/multiformats/go-multicodec"
|
||||||
varint "github.com/multiformats/go-varint"
|
varint "github.com/multiformats/go-varint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Prefix = "did:"
|
// Signature algorithms from the [did:key specification]
|
||||||
const KeyPrefix = "did:key:"
|
//
|
||||||
|
// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
|
||||||
const DIDCore = 0x0d1d
|
const (
|
||||||
const Ed25519 = 0xed
|
X25519 = multicodec.X25519Pub
|
||||||
|
Ed25519 = multicodec.Ed25519Pub // UCAN required/recommended
|
||||||
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
|
P256 = multicodec.P256Pub // UCAN required
|
||||||
|
P384 = multicodec.P384Pub
|
||||||
type DID struct {
|
P521 = multicodec.P521Pub
|
||||||
key bool
|
Secp256k1 = multicodec.Secp256k1Pub // UCAN required
|
||||||
str string
|
RSA = multicodec.RsaPub
|
||||||
}
|
)
|
||||||
|
|
||||||
// Undef can be used to represent a nil or undefined DID, using DID{}
|
// Undef can be used to represent a nil or undefined DID, using DID{}
|
||||||
// directly is also acceptable.
|
// directly is also acceptable.
|
||||||
var Undef = DID{}
|
var Undef = DID{}
|
||||||
|
|
||||||
func (d DID) Defined() bool {
|
// DID is a Decentralized Identifier of the did:key type, directly holding a cryptographic public key.
|
||||||
return d.str != ""
|
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
|
||||||
|
type DID struct {
|
||||||
|
code multicodec.Code
|
||||||
|
bytes string // as string instead of []byte to allow the == operator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DID) Bytes() []byte {
|
// Parse returns the DID from the string representation or an error if
|
||||||
if !d.Defined() {
|
// the prefix and method are incorrect, if an unknown encryption algorithm
|
||||||
return nil
|
// is specified or if the method-specific-identifier's bytes don't
|
||||||
|
// represent a public key for the specified encryption algorithm.
|
||||||
|
func Parse(str string) (DID, error) {
|
||||||
|
const keyPrefix = "did:key:"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(str, keyPrefix) {
|
||||||
|
return Undef, fmt.Errorf("must start with 'did:key'")
|
||||||
}
|
}
|
||||||
return []byte(d.str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DID) DID() DID {
|
baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):])
|
||||||
return d
|
if err != nil {
|
||||||
}
|
return Undef, err
|
||||||
|
}
|
||||||
// String formats the decentralized identity document (DID) as a string.
|
if baseCodec != mbase.Base58BTC {
|
||||||
func (d DID) String() string {
|
return Undef, fmt.Errorf("not Base58BTC encoded")
|
||||||
if d.key {
|
|
||||||
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.str))
|
|
||||||
return "did:key:" + key
|
|
||||||
}
|
}
|
||||||
return "did:" + d.str[MethodOffset:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func Decode(bytes []byte) (DID, error) {
|
|
||||||
code, _, err := varint.FromUvarint(bytes)
|
code, _, err := varint.FromUvarint(bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Undef, err
|
return Undef, err
|
||||||
}
|
}
|
||||||
if code == Ed25519 {
|
switch multicodec.Code(code) {
|
||||||
return DID{str: string(bytes), key: true}, nil
|
case Ed25519, P256, Secp256k1, RSA:
|
||||||
} else if code == DIDCore {
|
return DID{bytes: string(bytes), code: multicodec.Code(code)}, nil
|
||||||
return DID{str: string(bytes)}, nil
|
default:
|
||||||
|
return Undef, fmt.Errorf("unsupported did:key multicodec: 0x%x", code)
|
||||||
}
|
}
|
||||||
return Undef, fmt.Errorf("unsupported DID encoding: 0x%x", code)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(str string) (DID, error) {
|
// MustParse is like Parse but panics instead of returning an error.
|
||||||
if !strings.HasPrefix(str, Prefix) {
|
func MustParse(str string) DID {
|
||||||
return Undef, fmt.Errorf("must start with 'did:'")
|
did, err := Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return did
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined tells if the DID is defined, not equal to Undef.
|
||||||
|
func (d DID) Defined() bool {
|
||||||
|
return d.code != 0 || len(d.bytes) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubKey returns the public key encapsulated by the did:key.
|
||||||
|
func (d DID) PubKey() (crypto.PubKey, error) {
|
||||||
|
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
|
||||||
|
X25519: crypto.UnmarshalEd25519PublicKey,
|
||||||
|
Ed25519: crypto.UnmarshalEd25519PublicKey,
|
||||||
|
P256: ecdsaPubKeyUnmarshaler(elliptic.P256()),
|
||||||
|
P384: ecdsaPubKeyUnmarshaler(elliptic.P384()),
|
||||||
|
P521: ecdsaPubKeyUnmarshaler(elliptic.P521()),
|
||||||
|
Secp256k1: crypto.UnmarshalSecp256k1PublicKey,
|
||||||
|
RSA: rsaPubKeyUnmarshaller,
|
||||||
|
}[d.code]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(str, KeyPrefix) {
|
codeSize := varint.UvarintSize(uint64(d.code))
|
||||||
code, bytes, err := mbase.Decode(str[len(KeyPrefix):])
|
return unmarshaler([]byte(d.bytes)[codeSize:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// String formats the decentralized identity document (DID) as a string.
|
||||||
|
func (d DID) String() string {
|
||||||
|
if d == Undef {
|
||||||
|
return "(undefined)"
|
||||||
|
}
|
||||||
|
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
|
||||||
|
return "did:key:" + key
|
||||||
|
}
|
||||||
|
|
||||||
|
func ecdsaPubKeyUnmarshaler(curve elliptic.Curve) crypto.PubKeyUnmarshaller {
|
||||||
|
return func(data []byte) (crypto.PubKey, error) {
|
||||||
|
x, y := elliptic.UnmarshalCompressed(curve, data)
|
||||||
|
|
||||||
|
ecdsaPublicKey := &ecdsa.PublicKey{
|
||||||
|
Curve: curve,
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
|
||||||
|
pkix, err := x509.MarshalPKIXPublicKey(ecdsaPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Undef, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if code != mbase.Base58BTC {
|
|
||||||
return Undef, fmt.Errorf("not Base58BTC encoded")
|
return crypto.UnmarshalECDSAPublicKey(pkix)
|
||||||
}
|
}
|
||||||
return Decode(bytes)
|
}
|
||||||
|
|
||||||
|
func rsaPubKeyUnmarshaller(data []byte) (crypto.PubKey, error) {
|
||||||
|
rsaPublicKey, err := x509.ParsePKCS1PublicKey(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := make([]byte, MethodOffset)
|
pkix, err := x509.MarshalPKIXPublicKey(rsaPublicKey)
|
||||||
varint.PutUvarint(buf, DIDCore)
|
if err != nil {
|
||||||
suffix, _ := strings.CutPrefix(str, Prefix)
|
return nil, err
|
||||||
buf = append(buf, suffix...)
|
}
|
||||||
return DID{str: string(buf)}, nil
|
|
||||||
|
return crypto.UnmarshalRsaPublicKey(pkix)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,78 +2,40 @@ package did
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseDIDKey(t *testing.T) {
|
func TestParseDIDKey(t *testing.T) {
|
||||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
d, err := Parse(str)
|
d, err := Parse(str)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("%v", err)
|
require.Equal(t, str, d.String())
|
||||||
}
|
|
||||||
if d.String() != str {
|
|
||||||
t.Fatalf("expected %v to equal %v", d.String(), str)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecodeDIDKey(t *testing.T) {
|
func TestMustParseDIDKey(t *testing.T) {
|
||||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
d0, err := Parse(str)
|
require.NotPanics(t, func() {
|
||||||
if err != nil {
|
d := MustParse(str)
|
||||||
t.Fatalf("%v", err)
|
require.Equal(t, str, d.String())
|
||||||
}
|
})
|
||||||
d1, err := Decode(d0.Bytes())
|
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||||
if err != nil {
|
require.Panics(t, func() {
|
||||||
t.Fatalf("%v", err)
|
MustParse(str)
|
||||||
}
|
})
|
||||||
if d1.String() != str {
|
|
||||||
t.Fatalf("expected %v to equal %v", d1.String(), str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseDIDWeb(t *testing.T) {
|
|
||||||
str := "did:web:up.web3.storage"
|
|
||||||
d, err := Parse(str)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
if d.String() != str {
|
|
||||||
t.Fatalf("expected %v to equal %v", d.String(), str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeDIDWeb(t *testing.T) {
|
|
||||||
str := "did:web:up.web3.storage"
|
|
||||||
d0, err := Parse(str)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
d1, err := Decode(d0.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
if d1.String() != str {
|
|
||||||
t.Fatalf("expected %v to equal %v", d1.String(), str)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEquivalence(t *testing.T) {
|
func TestEquivalence(t *testing.T) {
|
||||||
u0 := DID{}
|
undef0 := DID{}
|
||||||
u1 := Undef
|
undef1 := Undef
|
||||||
if u0 != u1 {
|
|
||||||
t.Fatalf("undef DID not equivalent")
|
|
||||||
}
|
|
||||||
|
|
||||||
d0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
did0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatalf("%v", err)
|
did1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||||
}
|
require.NoError(t, err)
|
||||||
|
|
||||||
d1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
require.True(t, undef0 == undef1)
|
||||||
if err != nil {
|
require.False(t, undef0 == did0)
|
||||||
t.Fatalf("%v", err)
|
require.True(t, did0 == did1)
|
||||||
}
|
require.False(t, undef1 == did1)
|
||||||
|
|
||||||
if d0 != d1 {
|
|
||||||
t.Fatalf("two equivalent DID not equivalent")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
150
did/didtest/crypto.go
Normal file
150
did/didtest/crypto.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// Package didtest provides Personas that can be used for testing. Each
|
||||||
|
// Persona has a name, crypto.PrivKey and associated crypto.PubKey and
|
||||||
|
// did.DID.
|
||||||
|
package didtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alicePrivKeyB64 = "CAESQHdNJLBBiuc1AdwPHBkubB2KS1p0cv2JEF7m8tfwtrcm5ajaYPm+XmVCmtcHOF2lGDlmaiDA7emfwD3IrcyES0M="
|
||||||
|
bobPrivKeyB64 = "CAESQHBz+AIop1g+9iBDj+ufUc/zm9/ry7c6kDFO8Wl/D0+H63V9hC6s9l4npf3pYEFCjBtlR0AMNWMoFQKSlYNKo20="
|
||||||
|
carolPrivKeyB64 = "CAESQPrCgkcHnYFXDT9AlAydhPECBEivEuuVx9dJxLjVvDTmJIVNivfzg6H4mAiPfYS+5ryVVUZTHZBzvMuvvvG/Ks0="
|
||||||
|
danPrivKeyB64 = "CAESQCgNhzofKhC+7hW6x+fNd7iMPtQHeEmKRhhlduf/I7/TeOEFYAEflbJ0sAhMeDJ/HQXaAvsWgHEbJ3ZLhP8q2B0="
|
||||||
|
erinPrivKeyB64 = "CAESQKhCJo5UBpQcthko8DKMFsbdZ+qqQ5oc01CtLCqrE90dF2GfRlrMmot3WPHiHGCmEYi5ZMEHuiSI095e/6O4Bpw="
|
||||||
|
frankPrivKeyB64 = "CAESQDlXPKsy3jHh7OWTWQqyZF95Ueac5DKo7xD0NOBE5F2BNr1ZVxRmJ2dBELbOt8KP9sOACcO9qlCB7uMA1UQc7sk="
|
||||||
|
)
|
||||||
|
|
||||||
|
// Persona is a generic participant used for cryptographic testing.
|
||||||
|
type Persona int
|
||||||
|
|
||||||
|
// The provided Personas were selected from the first few generic
|
||||||
|
// participants listed in this [table].
|
||||||
|
//
|
||||||
|
// [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems
|
||||||
|
const (
|
||||||
|
PersonaAlice Persona = iota + 1
|
||||||
|
PersonaBob
|
||||||
|
PersonaCarol
|
||||||
|
PersonaDan
|
||||||
|
PersonaErin
|
||||||
|
PersonaFrank
|
||||||
|
)
|
||||||
|
|
||||||
|
var privKeys map[Persona]crypto.PrivKey
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
privKeys = make(map[Persona]crypto.PrivKey, 6)
|
||||||
|
for persona, privKeyCfg := range privKeyB64() {
|
||||||
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privKeys[persona] = privKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DID returns a did.DID based on the Persona's Ed25519 public key.
|
||||||
|
func (p Persona) DID() did.DID {
|
||||||
|
d, err := did.FromPrivKey(p.PrivKey())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the username of the Persona.
|
||||||
|
func (p Persona) Name() string {
|
||||||
|
name, ok := map[Persona]string{
|
||||||
|
PersonaAlice: "Alice",
|
||||||
|
PersonaBob: "Bob",
|
||||||
|
PersonaCarol: "Carol",
|
||||||
|
PersonaDan: "Dan",
|
||||||
|
PersonaErin: "Erin",
|
||||||
|
PersonaFrank: "Frank",
|
||||||
|
}[p]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivKey returns the Ed25519 private key for the Persona.
|
||||||
|
func (p Persona) PrivKey() crypto.PrivKey {
|
||||||
|
res, ok := privKeys[p]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Persona) PrivKeyConfig() string {
|
||||||
|
res, ok := privKeyB64()[p]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubKey returns the Ed25519 public key for the Persona.
|
||||||
|
func (p Persona) PubKey() crypto.PubKey {
|
||||||
|
return p.PrivKey().GetPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubKeyConfig returns the marshaled and encoded Ed25519 public key
|
||||||
|
// for the Persona.
|
||||||
|
func (p Persona) PubKeyConfig() string {
|
||||||
|
pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey().GetPublic())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return crypto.ConfigEncodeKey(pubKeyMar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func privKeyB64() map[Persona]string {
|
||||||
|
return map[Persona]string{
|
||||||
|
PersonaAlice: alicePrivKeyB64,
|
||||||
|
PersonaBob: bobPrivKeyB64,
|
||||||
|
PersonaCarol: carolPrivKeyB64,
|
||||||
|
PersonaDan: danPrivKeyB64,
|
||||||
|
PersonaErin: erinPrivKeyB64,
|
||||||
|
PersonaFrank: frankPrivKeyB64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Personas returns an (alphabetically) ordered list of the defined
|
||||||
|
// Persona values.
|
||||||
|
func Personas() []Persona {
|
||||||
|
return []Persona{
|
||||||
|
PersonaAlice,
|
||||||
|
PersonaBob,
|
||||||
|
PersonaCarol,
|
||||||
|
PersonaDan,
|
||||||
|
PersonaErin,
|
||||||
|
PersonaFrank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DidToName retrieve the persona's name from its DID.
|
||||||
|
func DidToName(d did.DID) string {
|
||||||
|
return map[did.DID]string{
|
||||||
|
PersonaAlice.DID(): "Alice",
|
||||||
|
PersonaBob.DID(): "Bob",
|
||||||
|
PersonaCarol.DID(): "Carol",
|
||||||
|
PersonaDan.DID(): "Dan",
|
||||||
|
PersonaErin.DID(): "Erin",
|
||||||
|
PersonaFrank.DID(): "Frank",
|
||||||
|
}[d]
|
||||||
|
}
|
||||||
82
did/key_spec_test.go
Normal file
82
did/key_spec_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//go:build jwx_es256k
|
||||||
|
|
||||||
|
package did_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/did/testvectors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestDidKeyVectors executes tests read from the [test vector files] provided
|
||||||
|
// as part of the DID Key method's [specification].
|
||||||
|
//
|
||||||
|
// [test vector files]: https://github.com/w3c-ccg/did-method-key/tree/main/test-vectors
|
||||||
|
// [specification]: https://w3c-ccg.github.io/did-method-key
|
||||||
|
func TestDidKeyVectors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, f := range []string{
|
||||||
|
// TODO: These test vectors are not supported by go-libp2p/core/crypto
|
||||||
|
// "bls12381.json",
|
||||||
|
"ed25519-x25519.json",
|
||||||
|
"nist-curves.json",
|
||||||
|
"rsa.json",
|
||||||
|
"secp256k1.json",
|
||||||
|
// This test vector only contains a DID Document
|
||||||
|
// "x25519.json",
|
||||||
|
} {
|
||||||
|
vectors := loadTestVectors(t, f)
|
||||||
|
|
||||||
|
t.Run(f, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for k, vector := range vectors {
|
||||||
|
t.Run(k, func(t *testing.T) {
|
||||||
|
// round-trip pubkey-->did-->pubkey, verified against the test vectors.
|
||||||
|
|
||||||
|
exp := vectorPubKey(t, vector)
|
||||||
|
|
||||||
|
id, err := did.FromPubKey(exp)
|
||||||
|
require.NoError(t, err, f, k)
|
||||||
|
act, err := id.PubKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, k, id.String(), f, k)
|
||||||
|
assert.Equal(t, exp, act)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTestVectors(t *testing.T, filename string) testvectors.Vectors {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join("testvectors", filename))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var vs testvectors.Vectors
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal(data, &vs))
|
||||||
|
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
|
||||||
|
func vectorPubKey(t *testing.T, v testvectors.Vector) crypto.PubKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pubKey, err := v.PubKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotZero(t, pubKey)
|
||||||
|
|
||||||
|
return pubKey
|
||||||
|
}
|
||||||
231
did/testvectors/bls12381.json
Normal file
231
did/testvectors/bls12381.json
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
{
|
||||||
|
"did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY",
|
||||||
|
"publicKeyBase58": "25EEkQtcLKsEzQ6JTo9cg4W7NHpaurn4Wg6LaNPFq6JQXnrP91SDviUz7KrJVMJd76CtAZFsRLYzvgX2JGxo2ccUHtuHk7ELCWwrkBDfrXCFVfqJKDootee9iVaF6NpdJtBE",
|
||||||
|
"privateKeyBase58": "8TXrPTbhefHvcz2vkGsDLBZT2UMeemveLKbdh5JZCvvn"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/bls12381-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY#zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY",
|
||||||
|
"publicKeyBase58": "25EEkQtcLKsEzQ6JTo9cg4W7NHpaurn4Wg6LaNPFq6JQXnrP91SDviUz7KrJVMJd76CtAZFsRLYzvgX2JGxo2ccUHtuHk7ELCWwrkBDfrXCFVfqJKDootee9iVaF6NpdJtBE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY#zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY#zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY#zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY#zUC7K4ndUaGZgV7Cp2yJy6JtMoUHY6u7tkcSYUvPrEidqBmLCTLmi6d5WvwnUqejscAkERJ3bfjEiSYtdPkRSE8kSa11hFBr4sTgnbZ95SJj19PN2jdvJjyzpSZgxkyyxNnBNnY"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY",
|
||||||
|
"publicKeyBase58": "t5QqHdxR4C6QJWJAnk3qVd2DMr4MVFEefdP43i7fLbR5A2qJkE5bqgEtyzpNsDViGEsMKHMdpo7fKbPMhGihbfxz3Dv2Hw36XvprLHBA5DDFSphmy91oHQFdahQMei2HjoE",
|
||||||
|
"privateKeyBase58": "URWBZN9g2ZfKVdAz1L8pvVwEBqCbGBozt4p8Cootb35"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/bls12381-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY#zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY",
|
||||||
|
"publicKeyBase58": "t5QqHdxR4C6QJWJAnk3qVd2DMr4MVFEefdP43i7fLbR5A2qJkE5bqgEtyzpNsDViGEsMKHMdpo7fKbPMhGihbfxz3Dv2Hw36XvprLHBA5DDFSphmy91oHQFdahQMei2HjoE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY#zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY#zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY#zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY#zUC77uxiMKceQoxciSy1xgk3nvP8c8NZXDnaY1xsXZaU5UmsZdnwStUke8Ca8zAdPX3MQTHEMhDTCgfdGU7UrY4RRdVhqZp8FaAaoaXFEVp2ZAM7oj3P45BuTCfc3t9FEGBAEQY"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW",
|
||||||
|
"publicKeyBase58": "25VFRgQEfbJ3Pit6Z3mnZbKPK9BdQYGwdmfdcmderjYZ12BFNQYeowjMN1AYKKKcacF3UH35ZNpBqCR8y8QLeeaGLL7UKdKLcFje3VQnosesDNHsU8jBvtvYmLJusxXsSUBC",
|
||||||
|
"privateKeyBase58": "48FTGTBBhezV7Ldk5g392NSxP2hwgEgWiSZQkMoNri7E"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/bls12381-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW#zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW",
|
||||||
|
"publicKeyBase58": "25VFRgQEfbJ3Pit6Z3mnZbKPK9BdQYGwdmfdcmderjYZ12BFNQYeowjMN1AYKKKcacF3UH35ZNpBqCR8y8QLeeaGLL7UKdKLcFje3VQnosesDNHsU8jBvtvYmLJusxXsSUBC"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW#zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW#zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW#zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW#zUC7KKoJk5ttwuuc8pmQDiUmtckEPTwcaFVZe4DSFV7fURuoRnD17D3xkBK3A9tZqdADkTTMKSwNkhjo9Hs6HfgNUXo48TNRaxU6XPLSPdRgMc15jCD5DfN34ixjoVemY62JxnW"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU",
|
||||||
|
"publicKeyBase58": "21LWABB5R6mqxvcU6LWMMt9yCAVyt8C1mHREs1EAX23fLcAEPMK4dWx59Jd6RpJ5geGt881vH9yPzZyC8WpHhS2g296mumPxJA3Aghp9jMoACE13rtTie8FYdgzgUw24eboA",
|
||||||
|
"privateKeyBase58": "86rp8w6Q7zgDdKqYxZsdTyhZogzwbcR7wf3VQrhV3xLG"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/bls12381-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU#zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU",
|
||||||
|
"publicKeyBase58": "21LWABB5R6mqxvcU6LWMMt9yCAVyt8C1mHREs1EAX23fLcAEPMK4dWx59Jd6RpJ5geGt881vH9yPzZyC8WpHhS2g296mumPxJA3Aghp9jMoACE13rtTie8FYdgzgUw24eboA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU#zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU#zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU#zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU#zUC7FB43ErjeTPiBLZ8wWT3aBTL7QnJ6AAZh9opgV5dKkw291mC23yTnKQ2pTcSgLbdKnVJ1ARn6XrwxWqvFg5dRFzCjwSg1j35nRgs5c2nbqkJ4auPTyPtkJ3xcABRNWaDX6QU"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA",
|
||||||
|
"publicKeyBase58": "21XhJ3o4ZSgDgRoyP4Pp8agXMwLycuRa1U6fM4ZzJBxH3gJEQbiuwP3Qh2zNoofNrBKPqp3FgXxGvW84cFwMD29oA7Q9w3L8Sjcc3e9mZqFgs8iWxSsDNRcbQdoYtGaxu11r",
|
||||||
|
"privateKeyBase58": "5LjJ3yibKGP4zKbNgqeiQ284g8LJYnbF7ZBve7Ke9qZ5"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/bls12381-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA#zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA",
|
||||||
|
"type": "Bls12381G2Key2020",
|
||||||
|
"controller": "did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA",
|
||||||
|
"publicKeyBase58": "21XhJ3o4ZSgDgRoyP4Pp8agXMwLycuRa1U6fM4ZzJBxH3gJEQbiuwP3Qh2zNoofNrBKPqp3FgXxGvW84cFwMD29oA7Q9w3L8Sjcc3e9mZqFgs8iWxSsDNRcbQdoYtGaxu11r"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA#zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA#zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA#zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA#zUC7FNFB7UinoJ5tqkeEELWLsytHBdHpwQ7wLVFAYRT6vqdr5uC3JPK6BVNNByj4KxvVKXoirT7VuqptSznjRCgvr7Ksuk42zyFw1GJSYNQSKCpjVcrZXoPUbR1P6zHmr97mVdA"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk#z3tEEysHYz5kkgpfDAByfDVgAuvtSFLHSqoMWmmSZBU1LZtN2sDsAS6RVQSevfxv39kyty",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "BLS12381_G1",
|
||||||
|
"x": "im0OQGMTkh4YEhAl16hQwUQTcOaRqIqThqtSwksFK7WaH6Qywypmc3VIDyydmYTe"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "BLS12381_G1",
|
||||||
|
"x": "im0OQGMTkh4YEhAl16hQwUQTcOaRqIqThqtSwksFK7WaH6Qywypmc3VIDyydmYTe",
|
||||||
|
"d": "S7Z1TuL05WHge8od0_mW8b3sRM747caCffsLwS6JZ-c"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk#z3tEEysHYz5kkgpfDAByfDVgAuvtSFLHSqoMWmmSZBU1LZtN2sDsAS6RVQSevfxv39kyty",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "BLS12381_G1",
|
||||||
|
"x": "im0OQGMTkh4YEhAl16hQwUQTcOaRqIqThqtSwksFK7WaH6Qywypmc3VIDyydmYTe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk#z3tEEysHYz5kkgpfDAByfDVgAuvtSFLHSqoMWmmSZBU1LZtN2sDsAS6RVQSevfxv39kyty"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk#z3tEEysHYz5kkgpfDAByfDVgAuvtSFLHSqoMWmmSZBU1LZtN2sDsAS6RVQSevfxv39kyty"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk#z3tEEysHYz5kkgpfDAByfDVgAuvtSFLHSqoMWmmSZBU1LZtN2sDsAS6RVQSevfxv39kyty"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z5TcCmGLu7HrkT5FTnejDTKcH11LPMQLXMPHTRyzY4KdRvqpPLprH7s1ddWFD38cAkZoiDtofUmJVZyEweUTfwjG5H3znk3ir4tzmuDBUSNbNQ7U6jJqj5bkQLKRaQB1bpFJKGLEq3EBwsfPutL5D7p78kFeLNHznqbf5oGpik7ScaDbGLaTLh1Jtadi6VmPNNd44Cojk#z3tEEysHYz5kkgpfDAByfDVgAuvtSFLHSqoMWmmSZBU1LZtN2sDsAS6RVQSevfxv39kyty"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
293
did/testvectors/ed25519-x25519.json
Normal file
293
did/testvectors/ed25519-x25519.json
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
{
|
||||||
|
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp": {
|
||||||
|
"seed": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"publicKeyBase58": "4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS"
|
||||||
|
},
|
||||||
|
"keyAgreementKeyPair": {
|
||||||
|
"id": "#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"publicKeyBase58": "7By6kV2t2d188odEM4ExAve1UithKT6dLva4dwsDT3ak",
|
||||||
|
"privateKeyBase58": "6QN8DfuN9hjgHgPvLXqgzqYE3jRRGRrmJQZkd5tL8paR"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/ed25519-2018/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"publicKeyBase58": "4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
|
||||||
|
"publicKeyBase58": "7By6kV2t2d188odEM4ExAve1UithKT6dLva4dwsDT3ak"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG": {
|
||||||
|
"seed": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"publicKeyBase58": "6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt"
|
||||||
|
},
|
||||||
|
"keyAgreementKeyPair": {
|
||||||
|
"id": "#z6LSrHyXiPBhUbvPUtyUCdf32sniiMGPTAesgHrtEa4FePtr",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"publicKeyBase58": "FcoNC5NqP9CePWbhfz95iHaEsCjGkZUioK9Ck7Qiw286",
|
||||||
|
"privateKeyBase58": "HBTcN2MrXNRj9xF9oi8QqYyuEPv3JLLjQKuEgW9oxVKP"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/ed25519-2018/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"publicKeyBase58": "6ASf5EcmmEHTgDJ4X4ZT5vT6iHVJBXPg5AN5YoTCpGWt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6LSrHyXiPBhUbvPUtyUCdf32sniiMGPTAesgHrtEa4FePtr",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
|
||||||
|
"publicKeyBase58": "FcoNC5NqP9CePWbhfz95iHaEsCjGkZUioK9Ck7Qiw286"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6LSrHyXiPBhUbvPUtyUCdf32sniiMGPTAesgHrtEa4FePtr"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf": {
|
||||||
|
"seed": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"publicKeyBase58": "8pM1DN3RiT8vbom5u1sNryaNT1nyL8CTTW3b5PwWXRBH"
|
||||||
|
},
|
||||||
|
"keyAgreementKeyPair": {
|
||||||
|
"id": "#z6LSkkqoZRC34AEpbkhZCqLDcHQVAxuLpQ7kC8XCXMVUfvjE",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"publicKeyBase58": "A5fe37PAxhX5WNKngBpGHhC1KpNE7nwbK9oX2tqwxYxU",
|
||||||
|
"privateKeyBase58": "ACa4PPJ1LnPNq1iwS33V3Akh7WtnC71WkKFZ9ccM6sX2"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/ed25519-2018/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"publicKeyBase58": "8pM1DN3RiT8vbom5u1sNryaNT1nyL8CTTW3b5PwWXRBH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6LSkkqoZRC34AEpbkhZCqLDcHQVAxuLpQ7kC8XCXMVUfvjE",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
|
||||||
|
"publicKeyBase58": "A5fe37PAxhX5WNKngBpGHhC1KpNE7nwbK9oX2tqwxYxU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6LSkkqoZRC34AEpbkhZCqLDcHQVAxuLpQ7kC8XCXMVUfvjE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ": {
|
||||||
|
"seed": "0000000000000000000000000000000000000000000000000000000000000003",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"publicKeyBase58": "HPYVwAQmskwT1qEEeRzhoomyfyupJGASQQtCXSNG8XS2"
|
||||||
|
},
|
||||||
|
"keyAgreementKeyPair": {
|
||||||
|
"id": "#z6LSiUo6AEDat8Ze4nQzDo67SGuHLLwsUGkxndHGUjsywHow",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"publicKeyBase58": "7ocvdvQinfqtyQ3Dh9aA7ggoVCQkmfaoueZazHETDv3B",
|
||||||
|
"privateKeyBase58": "FZrzd1osCnbK6y6MJzMBW1RcVfL524sNKhSbqRwMuwHT"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/ed25519-2018/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"type": "Ed25519VerificationKey2018",
|
||||||
|
"controller": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"publicKeyBase58": "HPYVwAQmskwT1qEEeRzhoomyfyupJGASQQtCXSNG8XS2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6LSiUo6AEDat8Ze4nQzDo67SGuHLLwsUGkxndHGUjsywHow",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ",
|
||||||
|
"publicKeyBase58": "7ocvdvQinfqtyQ3Dh9aA7ggoVCQkmfaoueZazHETDv3B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6LSiUo6AEDat8Ze4nQzDo67SGuHLLwsUGkxndHGUjsywHow"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU": {
|
||||||
|
"seed": "0000000000000000000000000000000000000000000000000000000000000005",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "Ed25519",
|
||||||
|
"x": "_eT7oDCtAC98L31MMx9J0T-w7HR-zuvsY08f9MvKne8"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "Ed25519",
|
||||||
|
"x": "_eT7oDCtAC98L31MMx9J0T-w7HR-zuvsY08f9MvKne8",
|
||||||
|
"d": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keyAgreementKeyPair": {
|
||||||
|
"id": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6LSmArkPSdTKjEESsExHRrSwUzYUHgDuWDewXc4nocasvFU",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "X25519",
|
||||||
|
"x": "jRIz3oriXDNZmnb35XQb7K1UIlz3ae1ao1YSqLeBXHs"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "X25519",
|
||||||
|
"x": "jRIz3oriXDNZmnb35XQb7K1UIlz3ae1ao1YSqLeBXHs",
|
||||||
|
"d": "aEAAB3VBFPCQtgF3N__wRiXhMOgeiRGstpPC3gnJ1Eo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "Ed25519",
|
||||||
|
"x": "_eT7oDCtAC98L31MMx9J0T-w7HR-zuvsY08f9MvKne8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6LSmArkPSdTKjEESsExHRrSwUzYUHgDuWDewXc4nocasvFU",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "X25519",
|
||||||
|
"x": "jRIz3oriXDNZmnb35XQb7K1UIlz3ae1ao1YSqLeBXHs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6MkwYMhwTvsq376YBAcJHy3vyRWzBgn5vKfVqqDCgm7XVKU#z6LSmArkPSdTKjEESsExHRrSwUzYUHgDuWDewXc4nocasvFU"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
371
did/testvectors/nist-curves.json
Normal file
371
did/testvectors/nist-curves.json
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
{
|
||||||
|
"did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns",
|
||||||
|
"y": "efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns",
|
||||||
|
"y": "efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM",
|
||||||
|
"d": "gPh-VvVS8MbvKQ9LSVVmfnxnKjHn4Tqj0bmbpehRlpc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "igrFmi0whuihKnj9R3Om1SoMph72wUGeFaBbzG2vzns",
|
||||||
|
"y": "efsX5b10x8yjyrj4ny3pGfLcY7Xby1KzgqOdqnsrJIM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv#zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "fyNYMN0976ci7xqiSdag3buk-ZCwgXU4kz9XNkBlNUI",
|
||||||
|
"y": "hW2ojTNfH7Jbi8--CJUo3OCbH3y5n91g-IMA9MLMbTU"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "fyNYMN0976ci7xqiSdag3buk-ZCwgXU4kz9XNkBlNUI",
|
||||||
|
"y": "hW2ojTNfH7Jbi8--CJUo3OCbH3y5n91g-IMA9MLMbTU",
|
||||||
|
"d": "YjRs6vNvw4sYrzVVY8ipkEpDAD9PFqw1sUnvPRMA-WI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-256",
|
||||||
|
"x": "fyNYMN0976ci7xqiSdag3buk-ZCwgXU4kz9XNkBlNUI",
|
||||||
|
"y": "hW2ojTNfH7Jbi8--CJUo3OCbH3y5n91g-IMA9MLMbTU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169#zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-384",
|
||||||
|
"x": "lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc",
|
||||||
|
"y": "y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-384",
|
||||||
|
"x": "lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc",
|
||||||
|
"y": "y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv",
|
||||||
|
"d": "hAyGZNj9031guBCdpAOaZkO-E5m-LKLYnMIq0-msrp8JLctseaOeNTHmP3uKVWwX"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-384",
|
||||||
|
"x": "lInTxl8fjLKp_UCrxI0WDklahi-7-_6JbtiHjiRvMvhedhKVdHBfi2HCY8t_QJyc",
|
||||||
|
"y": "y6N1IC-2mXxHreETBW7K3mBcw0qGr3CWHCs-yl09yCQRLcyfGv7XhqAngHOu51Zv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9#z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-384",
|
||||||
|
"x": "CA-iNoHDg1lL8pvX3d1uvExzVfCz7Rn6tW781Ub8K5MrDf2IMPyL0RTDiaLHC1JT",
|
||||||
|
"y": "Kpnrn8DkXUD3ge4mFxi-DKr0DYO2KuJdwNBrhzLRtfMa3WFMZBiPKUPfJj8dYNl_"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-384",
|
||||||
|
"x": "CA-iNoHDg1lL8pvX3d1uvExzVfCz7Rn6tW781Ub8K5MrDf2IMPyL0RTDiaLHC1JT",
|
||||||
|
"y": "Kpnrn8DkXUD3ge4mFxi-DKr0DYO2KuJdwNBrhzLRtfMa3WFMZBiPKUPfJj8dYNl_",
|
||||||
|
"d": "Xe1HHeh-UsrJPRNLR_Y06VTrWpZYBXi7a7kiRqCgwnAOlJZPwE-xzL3DIIVMavAL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-384",
|
||||||
|
"x": "CA-iNoHDg1lL8pvX3d1uvExzVfCz7Rn6tW781Ub8K5MrDf2IMPyL0RTDiaLHC1JT",
|
||||||
|
"y": "Kpnrn8DkXUD3ge4mFxi-DKr0DYO2KuJdwNBrhzLRtfMa3WFMZBiPKUPfJj8dYNl_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54#z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-521",
|
||||||
|
"x": "ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS",
|
||||||
|
"y": "AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-521",
|
||||||
|
"x": "ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS",
|
||||||
|
"y": "AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC",
|
||||||
|
"d": "AHwRaNaGs0jkj_pT6PK2aHep7dJK-yxyoL2bIfVRAceq1baxoiFDo3W14c8E2YZn1k5S53r4a11flhQdaB5guJ_X"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-521",
|
||||||
|
"x": "ASUHPMyichQ0QbHZ9ofNx_l4y7luncn5feKLo3OpJ2nSbZoC7mffolj5uy7s6KSKXFmnNWxGJ42IOrjZ47qqwqyS",
|
||||||
|
"y": "AW9ziIC4ZQQVSNmLlp59yYKrjRY0_VqO-GOIYQ9tYpPraBKUloEId6cI_vynCzlZWZtWpgOM3HPhYEgawQ703RjC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7#z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-521",
|
||||||
|
"x": "AQgyFy6EwH3_u_KXPw8aTXTY7WSVytmbuJeFpq4U6LipxtSmBJe_jjRzms9qubnwm_fGoHMQlvQ1vzS2YLusR2V0",
|
||||||
|
"y": "Ab06MCcgoG7dM2I-VppdLV1k3lDoeHMvyYqHVfP05Ep2O7Zu0Qwd6IVzfZi9K0KMDud22wdnGUpUtFukZo0EeO15"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-521",
|
||||||
|
"x": "AQgyFy6EwH3_u_KXPw8aTXTY7WSVytmbuJeFpq4U6LipxtSmBJe_jjRzms9qubnwm_fGoHMQlvQ1vzS2YLusR2V0",
|
||||||
|
"y": "Ab06MCcgoG7dM2I-VppdLV1k3lDoeHMvyYqHVfP05Ep2O7Zu0Qwd6IVzfZi9K0KMDud22wdnGUpUtFukZo0EeO15",
|
||||||
|
"d": "AbheZ-AA58LP4BpopCGCLH8ZoMdkdJaVOS6KK2NNmDCisr5_Ifxl-qcunrkOJ0CSauA4LJyNbCWcy28Bo6zgHTXQ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "P-521",
|
||||||
|
"x": "AQgyFy6EwH3_u_KXPw8aTXTY7WSVytmbuJeFpq4U6LipxtSmBJe_jjRzms9qubnwm_fGoHMQlvQ1vzS2YLusR2V0",
|
||||||
|
"y": "Ab06MCcgoG7dM2I-VppdLV1k3lDoeHMvyYqHVfP05Ep2O7Zu0Qwd6IVzfZi9K0KMDud22wdnGUpUtFukZo0EeO15"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f#z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb": {
|
||||||
|
"verificationMethod": {
|
||||||
|
"id": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
|
||||||
|
"type": "P256Key2021",
|
||||||
|
"controller": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
|
||||||
|
"publicKeyBase58": "ekVhkcBFq3w7jULLkBVye6PwaTuMbhJYuzwFnNcgQAPV",
|
||||||
|
"privateKeyBase58": "9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/multikey-2021/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
|
||||||
|
"type": "P256Key2021",
|
||||||
|
"controller": "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb",
|
||||||
|
"publicKeyBase58": "ekVhkcBFq3w7jULLkBVye6PwaTuMbhJYuzwFnNcgQAPV"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb#zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
did/testvectors/rsa.json
Normal file
106
did/testvectors/rsa.json
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i": {
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "sbX82NTV6IylxCh7MfV4hlyvaniCajuP97GyOqSvTmoEdBOflFvZ06kR_9D6ctt45Fk6hskfnag2GG69NALVH2o4RCR6tQiLRpKcMRtDYE_thEmfBvDzm_VVkOIYfxu-Ipuo9J_S5XDNDjczx2v-3oDh5-CIHkU46hvFeCvpUS-L8TJSbgX0kjVk_m4eIb9wh63rtmD6Uz_KBtCo5mmR4TEtcLZKYdqMp3wCjN-TlgHiz_4oVXWbHUefCEe8rFnX1iQnpDHU49_SaXQoud1jCaexFn25n-Aa8f8bc5Vm-5SeRwidHa6ErvEhTvf1dz6GoNPp2iRvm-wJ1gxwWJEYPQ",
|
||||||
|
"e": "AQAB"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "sbX82NTV6IylxCh7MfV4hlyvaniCajuP97GyOqSvTmoEdBOflFvZ06kR_9D6ctt45Fk6hskfnag2GG69NALVH2o4RCR6tQiLRpKcMRtDYE_thEmfBvDzm_VVkOIYfxu-Ipuo9J_S5XDNDjczx2v-3oDh5-CIHkU46hvFeCvpUS-L8TJSbgX0kjVk_m4eIb9wh63rtmD6Uz_KBtCo5mmR4TEtcLZKYdqMp3wCjN-TlgHiz_4oVXWbHUefCEe8rFnX1iQnpDHU49_SaXQoud1jCaexFn25n-Aa8f8bc5Vm-5SeRwidHa6ErvEhTvf1dz6GoNPp2iRvm-wJ1gxwWJEYPQ",
|
||||||
|
"e": "AQAB",
|
||||||
|
"d": "Eym3sT4KLwBzo5pl5nY83-hAti92iLQRizkrKe22RbNi9Y1kKOBatdtGaJqFVztZZu5ERGKNuTd5VdsjJeekSbXviVGRtdHNCvgmRZlWA5261AgIUPxMmKW062GmGJbKQvscFfziBgHK6tyDBd8cZavqMFHi-7ilMYF7IsFBcJKM85x_30pnfd4YwhGQIc9hzv238aOwYKg8c-MzYhEVUnL273jaiLVlfZWQ5ca-GXJHmdOb_Y4fE5gpXfPFBseqleXsMp0VuXxCEsN30LIJHYscdPtbzLD3LFbuMJglFbQqYqssqymILGqJ7Tc2mB2LmXevfqRWz5D7A_K1WzvuoQ",
|
||||||
|
"p": "ANwlk-eVXPQplCmr7VddX8MAlN5YWvfXkbJe2KOhyS7naSlfMyeW6I0z6q6MAI4h8cs9yEzwmN1oEl_6tZ_-NPd1Oda2Hq5jHx0Jq2P5exIMMbzTTHbB-LjMB4c-b1DZLOrL7ZpCS-CcEHvBz4phzHa7gqz2SrNIGozufbjS_tK5",
|
||||||
|
"q": "AM6nKRFqRgHiUtGc0xJawpXJeokGhJQFfinDlakjkptuRQNv0BOz8fRUxk6zwwYrx-T_Yk-0oAFsD8qWIgiXg8Wf0bdRW0L0dIH4c6ff3mSREXeAT2h3XDaF0F1YKns08WyYWtOuIiYWChyO9sweK7AUuaOJ-6lr6lElzTGHVf-l",
|
||||||
|
"dp": "AIHFBPK2cRzchaIq3rVpLVHdveNzYexG_nOOxVVvwRANCUiB_b2Qj3Ts7aIGlS0zhTyxJql0Cig5eNtrBjVRvBdC2t1ebaeOdoC_enBsV8fDuG3-gExg-ySz4JwwiZ2252tg2qbb_a5hULYjARwpmkVDMzyR0mbsUfpRe3q_pcbB",
|
||||||
|
"dq": "Id2bCVOVLXHdiKReor9k7A8cmaAL0gYkasu2lwVRXU9w1-NXAiOXHydVaEhlSXmbRJflkJJVNmZzIAwCf830tko-oAAhKJPPFA2XRoeVdn2fkynf2YrV_cloICP2skI23kkJeW8sAXnTJmL3ZvP6zNxYn8hZCaa5u5qqSdeX7FE",
|
||||||
|
"qi": "WKIToXXnjl7GDbz7jCNbX9nWYOE5BDNzVmwiVOnyGoTZfwJ_qtgizj7pOapxi6dT9S9mMavmeAi6LAsEe1WUWtaKSNhbNh0PUGGXlXHGlhkS8jI1ot0e-scrHAuACE567YQ4VurpNorPKtZ5UENXIn74DEmt4l5m6902VF3X5Wo"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i#z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "sbX82NTV6IylxCh7MfV4hlyvaniCajuP97GyOqSvTmoEdBOflFvZ06kR_9D6ctt45Fk6hskfnag2GG69NALVH2o4RCR6tQiLRpKcMRtDYE_thEmfBvDzm_VVkOIYfxu-Ipuo9J_S5XDNDjczx2v-3oDh5-CIHkU46hvFeCvpUS-L8TJSbgX0kjVk_m4eIb9wh63rtmD6Uz_KBtCo5mmR4TEtcLZKYdqMp3wCjN-TlgHiz_4oVXWbHUefCEe8rFnX1iQnpDHU49_SaXQoud1jCaexFn25n-Aa8f8bc5Vm-5SeRwidHa6ErvEhTvf1dz6GoNPp2iRvm-wJ1gxwWJEYPQ",
|
||||||
|
"e": "AQAB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i#z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i#z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i#z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i#z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i#z4MXj1wBzi9jUstyPMS4jQqB6KdJaiatPkAtVtGc6bQEQEEsKTic4G7Rou3iBf9vPmT5dbkm9qsZsuVNjq8HCuW1w24nhBFGkRE4cd2Uf2tfrB3N7h4mnyPp1BF3ZttHTYv3DLUPi1zMdkULiow3M1GfXkoC6DoxDUm1jmN6GBj22SjVsr6dxezRVQc7aj9TxE7JLbMH1wh5X3kA58H3DFW8rnYMakFGbca5CB2Jf6CnGQZmL7o5uJAdTwXfy2iiiyPxXEGerMhHwhjTA1mKYobyk2CpeEcmvynADfNZ5MBvcCS7m3XkFCMNUYBS9NQ3fze6vMSUPsNa6GVYmKx2x6JrdEjCk3qRMMmyjnjCMfR4pXbRMZa3i"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2": {
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "qMCkFFRFWtzUyZeK8mgJdyM6SEQcXC5E6JwCRVDld-jlJs8sXNOE_vliexq34wZRQ4hk53-JPFlvZ_QjRgIxdUxSMiZ3S5hlNVvvRaue6SMakA9ugQhnfXaWORro0UbPuHLms-bg5StDP8-8tIezu9c1H1FjwPcdbV6rAvKhyhnsM10qP3v2CPbdE0q3FOsihoKuTelImtO110E7N6fLn4U3EYbC4OyViqlrP1o_1M-R-tiM1cb4pD7XKJnIs6ryZdfOQSPBJwjNqSdN6Py_tdrFgPDTyacSSdpTVADOM2IMAoYbhV1N5APhnjOHBRFyKkF1HffQKpmXQLBqvUNNjuhmpVKWBtrTdcCKrglFXiw0cKGHKxIirjmiOlB_HYHg5UdosyE3_1Txct2U7-WBB6QXak1UgxCzgKYBDI8UPA0RlkUuHHP_Zg0fVXrXIInHO04MYxUeSps5qqyP6dJBu_v_BDn3zUq6LYFwJ_-xsU7zbrKYB4jaRlHPoCj_eDC-rSA2uQ4KXHBB8_aAqNFC9ukWxc26Ifz9dF968DLuL30bi-ZAa2oUh492Pw1bg89J7i4qTsOOfpQvGyDV7TGhKuUG3Hbumfr2w16S-_3EI2RIyd1nYsflE6ZmCkZQMG_lwDAFXaqfyGKEDouJuja4XH8r4fGWeGTrozIoniXT1HU",
|
||||||
|
"e": "AQAB"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "qMCkFFRFWtzUyZeK8mgJdyM6SEQcXC5E6JwCRVDld-jlJs8sXNOE_vliexq34wZRQ4hk53-JPFlvZ_QjRgIxdUxSMiZ3S5hlNVvvRaue6SMakA9ugQhnfXaWORro0UbPuHLms-bg5StDP8-8tIezu9c1H1FjwPcdbV6rAvKhyhnsM10qP3v2CPbdE0q3FOsihoKuTelImtO110E7N6fLn4U3EYbC4OyViqlrP1o_1M-R-tiM1cb4pD7XKJnIs6ryZdfOQSPBJwjNqSdN6Py_tdrFgPDTyacSSdpTVADOM2IMAoYbhV1N5APhnjOHBRFyKkF1HffQKpmXQLBqvUNNjuhmpVKWBtrTdcCKrglFXiw0cKGHKxIirjmiOlB_HYHg5UdosyE3_1Txct2U7-WBB6QXak1UgxCzgKYBDI8UPA0RlkUuHHP_Zg0fVXrXIInHO04MYxUeSps5qqyP6dJBu_v_BDn3zUq6LYFwJ_-xsU7zbrKYB4jaRlHPoCj_eDC-rSA2uQ4KXHBB8_aAqNFC9ukWxc26Ifz9dF968DLuL30bi-ZAa2oUh492Pw1bg89J7i4qTsOOfpQvGyDV7TGhKuUG3Hbumfr2w16S-_3EI2RIyd1nYsflE6ZmCkZQMG_lwDAFXaqfyGKEDouJuja4XH8r4fGWeGTrozIoniXT1HU",
|
||||||
|
"e": "AQAB",
|
||||||
|
"d": "TMq1H-clVG7PihkjCqJbRFLMj9wmx6_qfauYwPBKK-HYfWujdW5vxBO6Q-jpqy7RxhiISmxYCBVuw_BuKMqQtR8Q_G9StBzaWYjHfn3Vp6Poz4umLqOjbI2NWNks_ybpGbd30oAK8V5ZkO04ozJpkN4i92hzK3mIc5-z1HiTNUPMn6cStab0VCn6em_ylltV774CEcRJ3OLgid7OUspRt_rID3qyreYbOulTu5WXHIGEnZDzrciIlz1dbcVldpUhD0VAP5ZErD2uUP5oztBNcTTn0YBF8CrOALuQVdaz_t_sNS3P0kWeT1eQ0QwDskO5Hw-Aey2tFeWk1bQyLoQ1A0jsw8mDbkO2zrGfJoxmVBkueTK-q64_n1kV7W1aeJFRj4NwEWmwcrs8GSOGOn38fGB_Y3Kci04qvD6L0QZbFkAVzcJracnxbTdHCEX0jsAAPbYC8M_8PyrPJvPC4IAAWTRrSRbysb7r7viRf4A1vTK9VT7uYyxj7Kzx2cU12d9QBXYfdQ2744bUE7HqN-Vh2rHvv2l5v6vzBRoZ5_OhHHVeUYwC9LouE9lSVAObbFM-Qe1SvzbbwN91LziI7UzUc_xMAEiNwt6PpnIAWAhdvSRawEllTwUcn89udHd5UhiAcm-RQOqXIdA9Aly6d8TT8R1p-ZnQ_gbZyBZeS39AuvU=",
|
||||||
|
"p": "1p4cypsJeTyVXXc5bQpvzVenPy78OHXtGcFQnbTjW8x1GsvJ-rlHAcjUImd44pgNQNe-iYpeUg3KqfONeedNgQCFd8kP7GoVAd45mEvsGBXvjoCXOBMQlsf8UU_hm_LKhVvTvTmMGoudnNv5qYNDMCGJGzwoG-aSvROlIoXzHmDnusZ-hKsDxM9j0PPz21t99Y_Fr30Oq3FIWXPVmLYmfyZYQkxm9a9WNMkqRbwJuMwGI6V9ABsQ1dW_KJzp_aEBbJLcDr9DsWhm9ErLeAlzyaDYEai6wCtKm9em4LDwCbKhJq3hWEp1sIG-hwx1sk7N4i-b8lBijjEQE-dbSQxUlw==",
|
||||||
|
"q": "yUqMejfrttGujadj7Uf7q91KM7nbQGny4TjD-CqibcFE-s2_DExCgP1wfhUPfJr2uPQDIe4g12uaNoa5GbCSDaQwEmQpurC_5mazt-z-_tbI24hoPQm5Hq67fZz-jDE_3OccLPLIWtajJqmxHbbB5VqskMuXo8KDxPRfBQBhykmb9_5M8pY2ggZOV4shCUn5E9nOnvibvw5Wx4CBtWUtca4rhpd3mVen1d8xCe4xTG_ni_w1lwdxzU1GmRFqgTuZWzL0r2FKzJg7hju1SOEe4tKMxQ-xs2HyNaMM__SLsNmS3lsYZ8r2hqcjEMQQZI0T_O-3BjIpyg986P8j055E0w==",
|
||||||
|
"dp": "DujzJRw6P0L3OYQT6EBmXgSt6NTRzvZaX4SvnhU4CmOc6xynTpTamwQhwLYhjtRzb0LNyO5k-RxeLQpvlL1-A-1OWHEOeyUvim6u36a-ozm659KFLu8cIu2H2PpMuTHX4gXsIuRBmIKEk6YwpRcqbsiVpt-6BZ4yKZKY0Vou9rhSwQYTOhJLc7vYumaIVX_4szumxzdP8pcvKI_EkhRtfj3iudBnAsCIo6gqGKgkoMMD1iwkEALRW5m66w5jrywlVi6pvRiKkmOna2da1V8KvUJAYJGxT7JyP3tu64M_Wd0gFvjTg_fAT1_kJau27YlOAl2-Xso43poH_OoAzIVfxw==",
|
||||||
|
"dq": "XI6Z76z9BxB9mgcpTLc3wzw63XQNnB3bn7JRcjBwhdVD2at3uLjsL5HaAy-98kbzQfJ56kUr9sI0o_Po8yYc0ob3z80c3wpdAx2gb-dbDWVH8KJVhBOPestPzR--cEpJGlNuwkBU3mgplyKaHZamq8a46M-lB5jurEbN1mfpj3GvdSYKzdVCdSFfLqP76eCI1pblinW4b-6w-oVdn0JJ1icHPpkxVmJW-2Hok69iHcqrBtRO9AZpTsTEvKekeI4mIyhYGLi9AzzQyhV0c3GImTXFoutng5t7GyzBUoRpI0W4YeQzYa6TEzGRTylIfGPemATF_OReENp0TlLbb3gsHw==",
|
||||||
|
"qi": "m7uZk4AsOfJ1V2RY8lmEF518toCV7juKuS_b_OUx8B0dRG0_kbF1cH-Tmrgsya3bwkYx5HeZG81rX7SRjh-0nVPOMW3tGqU5U9f59DXqvOItJIJ6wvWvWXnuna2-NstYCotFQWadIKjk4wjEKj-a4NJt4D_F4csyeyqWOH2DiUFzBGGxxdEoD5t_HEeNXuWQ6-SiV0x5ZVMln3TSh7IOMl70Smm8HcQF5mOsWg3N0wIg-yffxPrs6r15TRuW1MfT-bZk2GLrtHF1TkIoT1e00jWK4eBl2oRxiJGONUBMTEHV85Fr0yztnA99AgHnrMbE_4ehvev4h5DEWvFyFuJN_g=="
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2#zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "qMCkFFRFWtzUyZeK8mgJdyM6SEQcXC5E6JwCRVDld-jlJs8sXNOE_vliexq34wZRQ4hk53-JPFlvZ_QjRgIxdUxSMiZ3S5hlNVvvRaue6SMakA9ugQhnfXaWORro0UbPuHLms-bg5StDP8-8tIezu9c1H1FjwPcdbV6rAvKhyhnsM10qP3v2CPbdE0q3FOsihoKuTelImtO110E7N6fLn4U3EYbC4OyViqlrP1o_1M-R-tiM1cb4pD7XKJnIs6ryZdfOQSPBJwjNqSdN6Py_tdrFgPDTyacSSdpTVADOM2IMAoYbhV1N5APhnjOHBRFyKkF1HffQKpmXQLBqvUNNjuhmpVKWBtrTdcCKrglFXiw0cKGHKxIirjmiOlB_HYHg5UdosyE3_1Txct2U7-WBB6QXak1UgxCzgKYBDI8UPA0RlkUuHHP_Zg0fVXrXIInHO04MYxUeSps5qqyP6dJBu_v_BDn3zUq6LYFwJ_-xsU7zbrKYB4jaRlHPoCj_eDC-rSA2uQ4KXHBB8_aAqNFC9ukWxc26Ifz9dF968DLuL30bi-ZAa2oUh492Pw1bg89J7i4qTsOOfpQvGyDV7TGhKuUG3Hbumfr2w16S-_3EI2RIyd1nYsflE6ZmCkZQMG_lwDAFXaqfyGKEDouJuja4XH8r4fGWeGTrozIoniXT1HU",
|
||||||
|
"e": "AQAB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2#zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2"
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2#zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2#zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2#zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2#zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
did/testvectors/secp256k1.json
Normal file
257
did/testvectors/secp256k1.json
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
{
|
||||||
|
"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme": {
|
||||||
|
"seed": "9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
|
||||||
|
"publicKeyBase58": "23o6Sau8NxxzXcgSc3PLcNxrzrZpbLeBn1izfv3jbKhuv",
|
||||||
|
"privateKeyBase58": "AjA4cyPUbbfW5wr6iZeRbJLhgH3qDt6q6LMkRw36KpxT"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
|
||||||
|
"publicKeyBase58": "23o6Sau8NxxzXcgSc3PLcNxrzrZpbLeBn1izfv3jbKhuv"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme#zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2": {
|
||||||
|
"seed": "f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2",
|
||||||
|
"publicKeyBase58": "291KzQhqCPC18PqH83XKhxv1HdqrdnxyS7dh15t2uNRzJ",
|
||||||
|
"privateKeyBase58": "HDbR1D5W3CoNbUKYzUbHH2PRF1atshtVupXgXTQhNB9E"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2",
|
||||||
|
"publicKeyBase58": "291KzQhqCPC18PqH83XKhxv1HdqrdnxyS7dh15t2uNRzJ"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2#zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N": {
|
||||||
|
"seed": "6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
|
||||||
|
"publicKeyBase58": "oesQ92MLiAkt2pjBcJFbW7H4DvzKJv22cotjYbmC2JEe",
|
||||||
|
"privateKeyBase58": "8CrrWVdzDnvaS7vS5dd2HetFSebwEN46XEFrNDdtWZSZ"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
|
||||||
|
"publicKeyBase58": "oesQ92MLiAkt2pjBcJFbW7H4DvzKJv22cotjYbmC2JEe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N#zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy": {
|
||||||
|
"seed": "c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy",
|
||||||
|
"publicKeyBase58": "pg3p1vprqePgUoqfAQ1TTgxhL6zLYhHyzooR1pqLxo9F",
|
||||||
|
"privateKeyBase58": "Dy2fnt8ba4NmbRBXas9bo1BtYgpYFr6ThpFhJbuA3PRn"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy",
|
||||||
|
"publicKeyBase58": "pg3p1vprqePgUoqfAQ1TTgxhL6zLYhHyzooR1pqLxo9F"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy#zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj": {
|
||||||
|
"seed": "175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133",
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj",
|
||||||
|
"publicKeyBase58": "24waDFAUAS16UpZwQQTXVEAmm17rQRjadjuAeBDW8aqL1",
|
||||||
|
"privateKeyBase58": "2aA6WgZnPiVMBX3LvKSTg3KaFKyzfKpvEacixB3yyTgv"
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj",
|
||||||
|
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||||
|
"controller": "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj",
|
||||||
|
"publicKeyBase58": "24waDFAUAS16UpZwQQTXVEAmm17rQRjadjuAeBDW8aqL1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj#zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS": {
|
||||||
|
"verificationKeyPair": {
|
||||||
|
"id": "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "secp256k1",
|
||||||
|
"x": "TEIJN9vnTq1EXMkqzo7yN_867-foKc2pREv45Fw_QA8",
|
||||||
|
"y": "9yiymlzdxKCiRbYq7p-ArRB-C1ytjHE-eb7RDTi6rVc"
|
||||||
|
},
|
||||||
|
"privateKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "secp256k1",
|
||||||
|
"x": "TEIJN9vnTq1EXMkqzo7yN_867-foKc2pREv45Fw_QA8",
|
||||||
|
"y": "9yiymlzdxKCiRbYq7p-ArRB-C1ytjHE-eb7RDTi6rVc",
|
||||||
|
"d": "J5yKm7OXFsXDEutteGYeT0CAfQJwIlHLSYkQxKtgiyo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"didDocument": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "EC",
|
||||||
|
"crv": "secp256k1",
|
||||||
|
"x": "TEIJN9vnTq1EXMkqzo7yN_867-foKc2pREv45Fw_QA8",
|
||||||
|
"y": "9yiymlzdxKCiRbYq7p-ArRB-C1ytjHE-eb7RDTi6rVc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"assertionMethod": [
|
||||||
|
"did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS"
|
||||||
|
],
|
||||||
|
"authentication": [
|
||||||
|
"did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS"
|
||||||
|
],
|
||||||
|
"capabilityInvocation": [
|
||||||
|
"did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS"
|
||||||
|
],
|
||||||
|
"capabilityDelegation": [
|
||||||
|
"did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS"
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS#zQ3shjmnWpSDEbYKpaFm4kTs9kXyqG6N2QwCYHNPP4yubqgJS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
did/testvectors/vectors.go
Normal file
163
did/testvectors/vectors.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
//go:build jwx_es256k
|
||||||
|
|
||||||
|
package testvectors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/mr-tron/base58"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Vectors map[string]Vector
|
||||||
|
|
||||||
|
// This is pretty gross but the structure allows the repeated Verifier,
|
||||||
|
// PublicKeyJwk and PublicKeyBase58 account for the fact that the test
|
||||||
|
// files are very inconsistent.
|
||||||
|
type Vector struct {
|
||||||
|
VerificationKeyPair Verifier
|
||||||
|
VerificationMethod Verifier
|
||||||
|
PublicKeyJwk json.RawMessage
|
||||||
|
DidDocument json.RawMessage // TODO: if we start producing DID documents, we should test this too
|
||||||
|
}
|
||||||
|
|
||||||
|
type Verifier struct {
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
PublicKeyBase58 string
|
||||||
|
PublicKeyJwk json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) PubKey() (crypto.PubKey, error) {
|
||||||
|
// If the public key is in base58
|
||||||
|
if pubB58 := v.PubKeyBase58(); len(pubB58) > 0 {
|
||||||
|
pubBytes, err := base58.Decode(pubB58)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := v.PubKeyType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmarshaler crypto.PubKeyUnmarshaller
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case "Ed25519VerificationKey2018":
|
||||||
|
unmarshaler = crypto.UnmarshalEd25519PublicKey
|
||||||
|
case "EcdsaSecp256k1VerificationKey2019":
|
||||||
|
unmarshaler = crypto.UnmarshalSecp256k1PublicKey
|
||||||
|
// This is weak as it assumes the P256 curve - that's all the vectors contain (for now)
|
||||||
|
case "P256Key2021":
|
||||||
|
unmarshaler = compressedEcdsaPublicKeyUnmarshaler
|
||||||
|
default:
|
||||||
|
return nil, errors.New("failed to resolve unmarshaler")
|
||||||
|
}
|
||||||
|
|
||||||
|
return unmarshaler(pubBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the public key is in a JWK
|
||||||
|
if pubJwk := v.PubKeyJwk(); len(pubJwk) > 0 {
|
||||||
|
key, err := jwk.ParseKey(pubJwk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var a any
|
||||||
|
|
||||||
|
if err := key.Raw(&a); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch a.(type) {
|
||||||
|
case *ecdsa.PublicKey:
|
||||||
|
epub := a.(*ecdsa.PublicKey)
|
||||||
|
|
||||||
|
if epub.Curve == secp256k1.S256() {
|
||||||
|
bytes := append([]byte{0x04}, append(epub.X.Bytes(), epub.Y.Bytes()...)...)
|
||||||
|
|
||||||
|
return crypto.UnmarshalSecp256k1PublicKey(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
asn1, err := x509.MarshalPKIXPublicKey(epub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return crypto.UnmarshalECDSAPublicKey(asn1)
|
||||||
|
case ed25519.PublicKey:
|
||||||
|
return crypto.UnmarshalEd25519PublicKey(a.(ed25519.PublicKey))
|
||||||
|
case *rsa.PublicKey:
|
||||||
|
asn1, err := x509.MarshalPKIXPublicKey(a.(*rsa.PublicKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return crypto.UnmarshalRsaPublicKey(asn1)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported key type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't find a public key at all
|
||||||
|
return nil, errors.New("vector's public key not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) PubKeyBase58() string {
|
||||||
|
if len(v.VerificationKeyPair.PublicKeyBase58) > 0 {
|
||||||
|
return v.VerificationKeyPair.PublicKeyBase58
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.VerificationMethod.PublicKeyBase58
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) PubKeyJwk() json.RawMessage {
|
||||||
|
if len(v.VerificationKeyPair.PublicKeyJwk) > 0 {
|
||||||
|
return v.VerificationKeyPair.PublicKeyJwk
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.VerificationMethod.PublicKeyJwk) > 0 {
|
||||||
|
return v.VerificationMethod.PublicKeyJwk
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.PublicKeyJwk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Vector) PubKeyType() (string, error) {
|
||||||
|
if len(v.VerificationKeyPair.Type) > 0 {
|
||||||
|
return v.VerificationKeyPair.Type, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(v.VerificationMethod.Type) > 0 {
|
||||||
|
return v.VerificationMethod.Type, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("vector's type not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func compressedEcdsaPublicKeyUnmarshaler(data []byte) (crypto.PubKey, error) {
|
||||||
|
x, y := elliptic.UnmarshalCompressed(elliptic.P256(), data)
|
||||||
|
|
||||||
|
ecdsaPublicKey := ecdsa.PublicKey{
|
||||||
|
Curve: elliptic.P256(),
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
|
||||||
|
asn1, err := x509.MarshalPKIXPublicKey(&ecdsaPublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return crypto.UnmarshalECDSAPublicKey(asn1)
|
||||||
|
}
|
||||||
80
did/testvectors/x25519.json
Normal file
80
did/testvectors/x25519.json
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"didDocument": {
|
||||||
|
"did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F#z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F",
|
||||||
|
"publicKeyBase58": "4Dy8E9UaZscuPUf2GLxV44RCNL7oxmEXXkgWXaug1WKV"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F#z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"did:key:z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha#z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha",
|
||||||
|
"publicKeyBase58": "J3PiFeuSyLugy4DKn87TwK5cnruRgPtxouzXUqg99Avp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha#z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"did:key:z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/x25519-2019/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ#z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ",
|
||||||
|
"type": "X25519KeyAgreementKey2019",
|
||||||
|
"controller": "did:key:z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ",
|
||||||
|
"publicKeyBase58": "CgTbngDMe7yHHfxPMvhpaFRpFoQWKgXAgwenJj8PsFDe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ#z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"did:key:z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz": {
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/did/v1",
|
||||||
|
"https://w3id.org/security/suites/jws-2020/v1"
|
||||||
|
],
|
||||||
|
"id": "did:key:z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz",
|
||||||
|
"verificationMethod": [
|
||||||
|
{
|
||||||
|
"id": "did:key:z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz#z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz",
|
||||||
|
"type": "JsonWebKey2020",
|
||||||
|
"controller": "did:key:z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz",
|
||||||
|
"publicKeyJwk": {
|
||||||
|
"kty": "OKP",
|
||||||
|
"crv": "X25519",
|
||||||
|
"x": "467ap28wHJGEXJAb4mLrokqq8A-txA_KmoQTcj31XzU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keyAgreement": [
|
||||||
|
"did:key:z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz#z6LSrzxMVydCourtpA6JLEYupT7ZUQ34hLfQZfRN5H47zLdz"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
go.mod
46
go.mod
@@ -1,31 +1,43 @@
|
|||||||
module github.com/ucan-wg/go-ucan/v1
|
module github.com/ucan-wg/go-ucan
|
||||||
|
|
||||||
go 1.21
|
go 1.23
|
||||||
|
|
||||||
toolchain go1.22.1
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||||
github.com/ipfs/go-cid v0.4.1
|
github.com/ipfs/go-cid v0.4.1
|
||||||
github.com/ipld/go-ipld-prime v0.21.0
|
github.com/ipld/go-ipld-prime v0.21.0
|
||||||
github.com/multiformats/go-multibase v0.0.3
|
github.com/lestrrat-go/jwx/v2 v2.1.1
|
||||||
github.com/multiformats/go-varint v0.0.6
|
github.com/libp2p/go-libp2p v0.36.3
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/mr-tron/base58 v1.2.0
|
||||||
|
github.com/multiformats/go-multibase v0.2.0
|
||||||
|
github.com/multiformats/go-multicodec v0.9.0
|
||||||
|
github.com/multiformats/go-multihash v0.2.3
|
||||||
|
github.com/multiformats/go-varint v0.0.7
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
|
golang.org/x/crypto v0.32.0
|
||||||
|
gotest.tools/v3 v3.5.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/goccy/go-json v0.10.3 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||||
github.com/multiformats/go-base32 v0.0.3 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||||
github.com/multiformats/go-base36 v0.1.0 // indirect
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||||
|
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||||
|
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polydawn/refmt v0.89.0 // indirect
|
github.com/polydawn/refmt v0.89.0 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
golang.org/x/crypto v0.1.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
golang.org/x/sys v0.1.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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.1.6 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
85
go.sum
85
go.sum
@@ -1,12 +1,17 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
@@ -17,30 +22,46 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH
|
|||||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
|
||||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
|
||||||
github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||||
|
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||||
|
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.1 h1:Y2ltVl8J6izLYFs54BVcpXLv5msSW4o8eXwnzZLI32E=
|
||||||
|
github.com/lestrrat-go/jwx/v2 v2.1.1/go.mod h1:4LvZg7oxu6Q5VJwn7Mk/UwooNRnTHUpXBj2C4j3HNx0=
|
||||||
|
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||||
|
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||||
|
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||||
|
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||||
|
github.com/libp2p/go-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
|
||||||
|
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
|
||||||
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||||
github.com/multiformats/go-base32 v0.0.3 h1:tw5+NhuwaOjJCC5Pp82QuXbrmLzWg7uxlMFp8Nq/kkI=
|
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||||
github.com/multiformats/go-base32 v0.0.3/go.mod h1:pLiuGC8y0QR3Ue4Zug5UzK9LjgbkL8NSQj0zQ5Nz/AA=
|
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||||
github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4=
|
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||||
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
|
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||||
github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk=
|
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
|
||||||
github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
|
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
|
||||||
|
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||||
|
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||||
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||||
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4=
|
||||||
@@ -48,6 +69,8 @@ github.com/polydawn/refmt v0.89.0/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX
|
|||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||||
|
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
@@ -55,26 +78,38 @@ github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hg
|
|||||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
|
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||||
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
|
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||||
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
|
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
|||||||
187
pkg/args/args.go
Normal file
187
pkg/args/args.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// Package args provides the type that represents the Arguments passed to
|
||||||
|
// a command within an invocation.Token as well as a convenient Add method
|
||||||
|
// to incrementally build the underlying map.
|
||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("key not found in meta")
|
||||||
|
|
||||||
|
// Args are the Command's arguments when an invocation Token is processed by the executor.
|
||||||
|
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||||
|
// and transformations, while hiding the IPLD complexity from the caller.
|
||||||
|
type Args struct {
|
||||||
|
// This type must be compatible with the IPLD type represented by the IPLD
|
||||||
|
// schema { String : Any }.
|
||||||
|
|
||||||
|
Keys []string
|
||||||
|
Values map[string]ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a pointer to an initialized Args value.
|
||||||
|
func New() *Args {
|
||||||
|
return &Args{
|
||||||
|
Values: map[string]ipld.Node{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNode retrieves a value as a raw IPLD node.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
func (a *Args) GetNode(key string) (ipld.Node, error) {
|
||||||
|
v, ok := a.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inserts a key/value pair in the Args set.
|
||||||
|
//
|
||||||
|
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||||
|
func (a *Args) Add(key string, val any) error {
|
||||||
|
if _, ok := a.Values[key]; ok {
|
||||||
|
return fmt.Errorf("duplicate key %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := literal.Any(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
||||||
|
return fmt.Errorf("value for key %q: %w", key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Values[key] = node
|
||||||
|
a.Keys = append(a.Keys, key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Iterator interface {
|
||||||
|
Iter() iter.Seq2[string, ipld.Node]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include merges the provided arguments into the existing arguments.
|
||||||
|
//
|
||||||
|
// If duplicate keys are encountered, the new value is silently dropped
|
||||||
|
// without causing an error.
|
||||||
|
func (a *Args) Include(other Iterator) {
|
||||||
|
for key, value := range other.Iter() {
|
||||||
|
if _, ok := a.Values[key]; ok {
|
||||||
|
// don't overwrite
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.Values[key] = value
|
||||||
|
a.Keys = append(a.Keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len return the number of arguments.
|
||||||
|
func (a *Args) Len() int {
|
||||||
|
return len(a.Keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iter iterates over the args key/values
|
||||||
|
func (a *Args) Iter() iter.Seq2[string, ipld.Node] {
|
||||||
|
return func(yield func(string, ipld.Node) bool) {
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
if !yield(key, a.Values[key]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToIPLD wraps an instance of an Args with an ipld.Node.
|
||||||
|
func (a *Args) ToIPLD() (ipld.Node, error) {
|
||||||
|
sort.Strings(a.Keys)
|
||||||
|
|
||||||
|
return qp.BuildMap(basicnode.Prototype.Any, int64(len(a.Keys)), func(ma datamodel.MapAssembler) {
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
qp.MapEntry(ma, key, qp.Node(a.Values[key]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals tells if two Args hold the same values.
|
||||||
|
func (a *Args) Equals(other *Args) bool {
|
||||||
|
if len(a.Keys) != len(other.Keys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a.Values) != len(other.Values) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
if !ipld.DeepEqual(a.Values[key], other.Values[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Args) String() string {
|
||||||
|
sort.Strings(a.Keys)
|
||||||
|
|
||||||
|
buf := strings.Builder{}
|
||||||
|
buf.WriteString("{")
|
||||||
|
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
buf.WriteString("\n\t")
|
||||||
|
buf.WriteString(key)
|
||||||
|
buf.WriteString(": ")
|
||||||
|
buf.WriteString(strings.ReplaceAll(printer.Sprint(a.Values[key]), "\n", "\n\t"))
|
||||||
|
buf.WriteString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.Keys) > 0 {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
buf.WriteString("}")
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOnly returns a read-only version of Args.
|
||||||
|
func (a *Args) ReadOnly() ReadOnly {
|
||||||
|
return ReadOnly{args: a}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone makes a deep copy.
|
||||||
|
func (a *Args) Clone() *Args {
|
||||||
|
res := &Args{
|
||||||
|
Keys: make([]string, len(a.Keys)),
|
||||||
|
Values: make(map[string]ipld.Node, len(a.Values)),
|
||||||
|
}
|
||||||
|
copy(res.Keys, a.Keys)
|
||||||
|
for k, v := range a.Values {
|
||||||
|
res.Values[k] = v
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks that all values in the Args are valid according to UCAN specs
|
||||||
|
func (a *Args) Validate() error {
|
||||||
|
for key, value := range a.Values {
|
||||||
|
if err := limits.ValidateIntegerBoundsIPLD(value); err != nil {
|
||||||
|
return fmt.Errorf("value for key %q: %w", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
297
pkg/args/args_test.go
Normal file
297
pkg/args/args_test.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package args_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"maps"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/node/basicnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArgs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
intKey = "intKey"
|
||||||
|
mapKey = "mapKey"
|
||||||
|
nilKey = "nilKey"
|
||||||
|
boolKey = "boolKey"
|
||||||
|
linkKey = "linkKey"
|
||||||
|
listKey = "listKey"
|
||||||
|
nodeKey = "nodeKey"
|
||||||
|
uintKey = "uintKey"
|
||||||
|
bytesKey = "bytesKey"
|
||||||
|
floatKey = "floatKey"
|
||||||
|
stringKey = "stringKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
expIntVal = int64(-42)
|
||||||
|
expBoolVal = true
|
||||||
|
expUintVal = uint(42)
|
||||||
|
expStringVal = "stringVal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
expMapVal = map[string]string{"keyOne": "valOne", "keyTwo": "valTwo"}
|
||||||
|
// expNilVal = (map[string]string)(nil)
|
||||||
|
expLinkVal = cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")
|
||||||
|
expListVal = []string{"elem1", "elem2", "elem3"}
|
||||||
|
expNodeVal = literal.String("nodeVal")
|
||||||
|
expBytesVal = []byte{0xde, 0xad, 0xbe, 0xef}
|
||||||
|
expFloatVal = 42.0
|
||||||
|
)
|
||||||
|
|
||||||
|
argsIn := args.New()
|
||||||
|
|
||||||
|
for _, a := range []struct {
|
||||||
|
key string
|
||||||
|
val any
|
||||||
|
}{
|
||||||
|
{key: intKey, val: expIntVal},
|
||||||
|
{key: mapKey, val: expMapVal},
|
||||||
|
// {key: nilKey, val: expNilVal},
|
||||||
|
{key: boolKey, val: expBoolVal},
|
||||||
|
{key: linkKey, val: expLinkVal},
|
||||||
|
{key: listKey, val: expListVal},
|
||||||
|
{key: uintKey, val: expUintVal},
|
||||||
|
{key: nodeKey, val: expNodeVal},
|
||||||
|
{key: bytesKey, val: expBytesVal},
|
||||||
|
{key: floatKey, val: expFloatVal},
|
||||||
|
{key: stringKey, val: expStringVal},
|
||||||
|
} {
|
||||||
|
require.NoError(t, argsIn.Add(a.key, a.val))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-trip to DAG-CBOR
|
||||||
|
argsOut := roundTripThroughDAGCBOR(t, argsIn)
|
||||||
|
assert.ElementsMatch(t, argsIn.Keys, argsOut.Keys)
|
||||||
|
assert.Equal(t, argsIn.Values, argsOut.Values)
|
||||||
|
|
||||||
|
actMapVal := map[string]string{}
|
||||||
|
mit := argsOut.Values[mapKey].MapIterator()
|
||||||
|
|
||||||
|
for !mit.Done() {
|
||||||
|
k, v, err := mit.Next()
|
||||||
|
require.NoError(t, err)
|
||||||
|
ks := must(k.AsString())
|
||||||
|
vs := must(v.AsString())
|
||||||
|
actMapVal[ks] = vs
|
||||||
|
}
|
||||||
|
|
||||||
|
actListVal := []string{}
|
||||||
|
lit := argsOut.Values[listKey].ListIterator()
|
||||||
|
|
||||||
|
for !lit.Done() {
|
||||||
|
_, v, err := lit.Next()
|
||||||
|
require.NoError(t, err)
|
||||||
|
vs := must(v.AsString())
|
||||||
|
|
||||||
|
actListVal = append(actListVal, vs)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expIntVal, must(argsOut.Values[intKey].AsInt()))
|
||||||
|
assert.Equal(t, expMapVal, actMapVal) // TODO: special accessor
|
||||||
|
// TODO: the nil map comes back empty (but the right type)
|
||||||
|
// assert.Equal(t, expNilVal, actNilVal)
|
||||||
|
assert.Equal(t, expBoolVal, must(argsOut.Values[boolKey].AsBool()))
|
||||||
|
assert.Equal(t, expLinkVal.String(), must(argsOut.Values[linkKey].AsLink()).(datamodel.Link).String()) // TODO: special accessor
|
||||||
|
assert.Equal(t, expListVal, actListVal) // TODO: special accessor
|
||||||
|
assert.Equal(t, expNodeVal, argsOut.Values[nodeKey])
|
||||||
|
assert.Equal(t, expUintVal, uint(must(argsOut.Values[uintKey].AsInt())))
|
||||||
|
assert.Equal(t, expBytesVal, must(argsOut.Values[bytesKey].AsBytes()))
|
||||||
|
assert.Equal(t, expFloatVal, must(argsOut.Values[floatKey].AsFloat()))
|
||||||
|
assert.Equal(t, expStringVal, must(argsOut.Values[stringKey].AsString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgs_Include(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
argsIn := args.New()
|
||||||
|
require.NoError(t, argsIn.Add("key1", "val1"))
|
||||||
|
require.NoError(t, argsIn.Add("key2", "val2"))
|
||||||
|
|
||||||
|
argsOther := args.New()
|
||||||
|
require.NoError(t, argsOther.Add("key2", "valOther")) // This should not overwrite key2 above
|
||||||
|
require.NoError(t, argsOther.Add("key3", "val3"))
|
||||||
|
require.NoError(t, argsOther.Add("key4", "val4"))
|
||||||
|
|
||||||
|
argsIn.Include(argsOther)
|
||||||
|
|
||||||
|
assert.Len(t, argsIn.Values, 4)
|
||||||
|
assert.Equal(t, "val1", must(argsIn.Values["key1"].AsString()))
|
||||||
|
assert.Equal(t, "val2", must(argsIn.Values["key2"].AsString()))
|
||||||
|
assert.Equal(t, "val3", must(argsIn.Values["key3"].AsString()))
|
||||||
|
assert.Equal(t, "val4", must(argsIn.Values["key4"].AsString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterCloneEquals(t *testing.T) {
|
||||||
|
a := args.New()
|
||||||
|
|
||||||
|
require.NoError(t, a.Add("foo", "bar"))
|
||||||
|
require.NoError(t, a.Add("baz", 1234))
|
||||||
|
|
||||||
|
expected := map[string]ipld.Node{
|
||||||
|
"foo": basicnode.NewString("bar"),
|
||||||
|
"baz": basicnode.NewInt(1234),
|
||||||
|
}
|
||||||
|
|
||||||
|
// args -> iter
|
||||||
|
require.Equal(t, expected, maps.Collect(a.Iter()))
|
||||||
|
|
||||||
|
// readonly -> iter
|
||||||
|
ro := a.ReadOnly()
|
||||||
|
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||||
|
|
||||||
|
// args -> clone -> iter
|
||||||
|
clone := a.Clone()
|
||||||
|
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
||||||
|
|
||||||
|
// readonly -> WriteableClone -> iter
|
||||||
|
wclone := ro.WriteableClone()
|
||||||
|
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
||||||
|
|
||||||
|
require.True(t, a.Equals(wclone))
|
||||||
|
require.True(t, ro.Equals(wclone.ReadOnly()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInclude(t *testing.T) {
|
||||||
|
a1 := args.New()
|
||||||
|
|
||||||
|
require.NoError(t, a1.Add("samekey", "bar"))
|
||||||
|
require.NoError(t, a1.Add("baz", 1234))
|
||||||
|
|
||||||
|
a2 := args.New()
|
||||||
|
|
||||||
|
require.NoError(t, a2.Add("samekey", "othervalue")) // check no overwrite
|
||||||
|
require.NoError(t, a2.Add("otherkey", 1234))
|
||||||
|
|
||||||
|
a1.Include(a2)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]ipld.Node{
|
||||||
|
"samekey": basicnode.NewString("bar"),
|
||||||
|
"baz": basicnode.NewInt(1234),
|
||||||
|
"otherkey": basicnode.NewInt(1234),
|
||||||
|
}, maps.Collect(a1.Iter()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgsIntegerBounds(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
val int64
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid int",
|
||||||
|
key: "valid",
|
||||||
|
val: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max safe integer",
|
||||||
|
key: "max",
|
||||||
|
val: limits.MaxInt53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min safe integer",
|
||||||
|
key: "min",
|
||||||
|
val: limits.MinInt53,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "exceeds max safe integer",
|
||||||
|
key: "tooBig",
|
||||||
|
val: limits.MaxInt53 + 1,
|
||||||
|
wantErr: "exceeds safe integer bounds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "below min safe integer",
|
||||||
|
key: "tooSmall",
|
||||||
|
val: limits.MinInt53 - 1,
|
||||||
|
wantErr: "exceeds safe integer bounds",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate key",
|
||||||
|
key: "duplicate",
|
||||||
|
val: 42,
|
||||||
|
wantErr: "duplicate key",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
a := args.New()
|
||||||
|
require.NoError(t, a.Add("duplicate", 1)) // tests duplicate key
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := a.Add(tt.key, tt.val)
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.wantErr)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
val, err := a.GetNode(tt.key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
i, err := val.AsInt()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.val, i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
argsSchema = "type Args { String : Any }"
|
||||||
|
argsName = "Args"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
ts *schema.TypeSystem
|
||||||
|
errSchema error
|
||||||
|
)
|
||||||
|
|
||||||
|
func argsType() schema.Type {
|
||||||
|
once.Do(func() {
|
||||||
|
ts, errSchema = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||||
|
})
|
||||||
|
if errSchema != nil {
|
||||||
|
panic(errSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.TypeByName(argsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundTripThroughDAGCBOR(t *testing.T, argsIn *args.Args) *args.Args {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
node, err := argsIn.ToIPLD()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := ipld.Encode(node, dagcbor.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var argsOut args.Args
|
||||||
|
_, err = ipld.Unmarshal(data, dagcbor.Decode, &argsOut, argsType())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &argsOut
|
||||||
|
}
|
||||||
|
|
||||||
|
func must[T any](t T, err error) T {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
71
pkg/args/builder.go
Normal file
71
pkg/args/builder.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Builder allows the fluid construction of an Args.
|
||||||
|
type Builder struct {
|
||||||
|
args *Args
|
||||||
|
errs error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBuilder returns a Builder which will assemble the Args.
|
||||||
|
func NewBuilder() *Builder {
|
||||||
|
return &Builder{
|
||||||
|
args: New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inserts a new key/val into the Args being assembled while collecting
|
||||||
|
// any errors caused by duplicate keys.
|
||||||
|
func (b *Builder) Add(key string, val any) *Builder {
|
||||||
|
b.errs = errors.Join(b.errs, b.args.Add(key, val))
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build returns the assembled Args or an error containing a list of
|
||||||
|
// errors encountered while trying to build the Args.
|
||||||
|
func (b *Builder) Build() (*Args, error) {
|
||||||
|
if b.errs != nil {
|
||||||
|
return nil, b.errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildIPLD is the same as Build except it takes the additional step of
|
||||||
|
// converting the Args to an ipld.Node.
|
||||||
|
func (b *Builder) BuildIPLD() (ipld.Node, error) {
|
||||||
|
args, err := b.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.ToIPLD()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBuild is the same as Build except it panics if an error occurs.
|
||||||
|
func (b *Builder) MustBuild() *Args {
|
||||||
|
args, err := b.Build()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(b.errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBuildIPLD is the same as BuildIPLD except it panics if an error
|
||||||
|
// occurs.
|
||||||
|
func (b *Builder) MustBuildIPLD() ipld.Node {
|
||||||
|
node, err := b.BuildIPLD()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
81
pkg/args/builder_test.go
Normal file
81
pkg/args/builder_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package args_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilder_XXX(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyOne = "key1"
|
||||||
|
valOne = "string"
|
||||||
|
keyTwo = "key2"
|
||||||
|
valTwo = 42
|
||||||
|
)
|
||||||
|
|
||||||
|
exp := args.New()
|
||||||
|
exp.Add(keyOne, valOne)
|
||||||
|
exp.Add(keyTwo, valTwo)
|
||||||
|
|
||||||
|
expNode, err := exp.ToIPLD()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
disjointKeys := args.NewBuilder().
|
||||||
|
Add(keyOne, valOne).
|
||||||
|
Add(keyTwo, valTwo)
|
||||||
|
|
||||||
|
duplicateKeys := args.NewBuilder().
|
||||||
|
Add(keyOne, valOne).
|
||||||
|
Add(keyTwo, valTwo).
|
||||||
|
Add(keyOne, "oh no!")
|
||||||
|
|
||||||
|
t.Run("MustBuild succeeds with disjoint keys", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var act *args.Args
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
act = disjointKeys.MustBuild()
|
||||||
|
})
|
||||||
|
assert.Equal(t, exp, act)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MustBuild fails with duplicate keys", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var act *args.Args
|
||||||
|
|
||||||
|
require.Panics(t, func() {
|
||||||
|
act = duplicateKeys.MustBuild()
|
||||||
|
})
|
||||||
|
assert.Nil(t, act)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MustBuildIPLD succeeds with disjoint keys", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var act ipld.Node
|
||||||
|
|
||||||
|
require.NotPanics(t, func() {
|
||||||
|
act = disjointKeys.MustBuildIPLD()
|
||||||
|
})
|
||||||
|
assert.Equal(t, expNode, act)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MustBuildIPLD fails with duplicate keys", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var act ipld.Node
|
||||||
|
|
||||||
|
require.Panics(t, func() {
|
||||||
|
act = duplicateKeys.MustBuildIPLD()
|
||||||
|
})
|
||||||
|
assert.Nil(t, act)
|
||||||
|
})
|
||||||
|
}
|
||||||
39
pkg/args/readonly.go
Normal file
39
pkg/args/readonly.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReadOnly struct {
|
||||||
|
args *Args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||||
|
return r.args.GetNode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Len() int {
|
||||||
|
return r.args.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||||
|
return r.args.Iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) ToIPLD() (ipld.Node, error) {
|
||||||
|
return r.args.ToIPLD()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||||
|
return r.args.Equals(other.args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) String() string {
|
||||||
|
return r.args.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) WriteableClone() *Args {
|
||||||
|
return r.args.Clone()
|
||||||
|
}
|
||||||
135
pkg/command/command.go
Normal file
135
pkg/command/command.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const separator = "/"
|
||||||
|
|
||||||
|
var _ fmt.Stringer = (*Command)(nil)
|
||||||
|
|
||||||
|
// Command is a concrete messages (a "verb") that MUST be unambiguously
|
||||||
|
// interpretable by the Subject of a UCAN.
|
||||||
|
//
|
||||||
|
// A [Command] is composed of a leading slash which is optionally followed
|
||||||
|
// by one or more slash-separated Segments of lowercase characters.
|
||||||
|
//
|
||||||
|
// [Command]: https://github.com/ucan-wg/spec#command
|
||||||
|
type Command string
|
||||||
|
|
||||||
|
// New creates a validated command from the provided list of segment strings.
|
||||||
|
// An error is returned if an invalid Command would be formed
|
||||||
|
func New(segments ...string) Command {
|
||||||
|
return Top().Join(segments...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse verifies that the provided string contains the required
|
||||||
|
// [segment structure] and, if valid, returns the resulting
|
||||||
|
// Command.
|
||||||
|
//
|
||||||
|
// [segment structure]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
func Parse(s string) (Command, error) {
|
||||||
|
if !strings.HasPrefix(s, "/") {
|
||||||
|
return "", ErrRequiresLeadingSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > 1 && strings.HasSuffix(s, "/") {
|
||||||
|
return "", ErrDisallowsTrailingSlash
|
||||||
|
}
|
||||||
|
|
||||||
|
if s != strings.ToLower(s) {
|
||||||
|
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(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustParse is the same as Parse, but panic() if the parsing fail.
|
||||||
|
func MustParse(s string) Command {
|
||||||
|
c, err := Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top is the most powerful capability.
|
||||||
|
//
|
||||||
|
// This function returns a Command that is a wildcard and therefore represents the
|
||||||
|
// most powerful ability. As such, it should be handled with care and used sparingly.
|
||||||
|
//
|
||||||
|
// [Top]: https://github.com/ucan-wg/spec#-aka-top
|
||||||
|
func Top() Command {
|
||||||
|
return Command(separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if the provided string is a valid UCAN command.
|
||||||
|
func IsValid(s string) bool {
|
||||||
|
_, err := Parse(s)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join appends segments to the end of this command using the required
|
||||||
|
// segment separator.
|
||||||
|
func (c Command) Join(segments ...string) Command {
|
||||||
|
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 {
|
||||||
|
if c == separator {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.Split(string(c), separator)[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Covers returns true if the command is identical or a parent of the given other command.
|
||||||
|
func (c Command) Covers(other Command) bool {
|
||||||
|
// fast-path, equivalent to the code below (verified with fuzzing)
|
||||||
|
if !strings.HasPrefix(string(other), string(c)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c == separator || len(c) == len(other) || other[len(c)] == separator[0]
|
||||||
|
|
||||||
|
/* -------
|
||||||
|
|
||||||
|
otherSegments := other.Segments()
|
||||||
|
if len(otherSegments) < len(c.Segments()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, s := range c.Segments() {
|
||||||
|
if otherSegments[i] != s {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the composed representation the command. This is also
|
||||||
|
// the required wire representation (before IPLD encoding occurs.)
|
||||||
|
func (c Command) String() string {
|
||||||
|
return string(c)
|
||||||
|
}
|
||||||
21
pkg/command/command_errors.go
Normal file
21
pkg/command/command_errors.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ErrRequiresLeadingSlash is returned when a parsing a string that
|
||||||
|
// doesn't start with a [leading slash character].
|
||||||
|
//
|
||||||
|
// [leading slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrRequiresLeadingSlash = fmt.Errorf("a command requires a leading slash character")
|
||||||
|
|
||||||
|
// ErrDisallowsTrailingSlash is returned when parsing a string that [ends
|
||||||
|
// with a trailing slash character].
|
||||||
|
//
|
||||||
|
// [ends with a trailing slash character]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrDisallowsTrailingSlash = fmt.Errorf("a command must not include a trailing slash")
|
||||||
|
|
||||||
|
// ErrRequiresLowercase is returned if a Command contains, or would contain,
|
||||||
|
// [uppercase unicode characters].
|
||||||
|
//
|
||||||
|
// [uppercase unicode characters]: https://github.com/ucan-wg/spec#segment-structure
|
||||||
|
var ErrRequiresLowercase = fmt.Errorf("UCAN path segments must must not contain upper-case characters")
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestTop(t *testing.T) {
|
func TestTop(t *testing.T) {
|
||||||
@@ -13,74 +13,81 @@ func TestTop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidCommand(t *testing.T) {
|
func TestIsValidCommand(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("succeeds when", func(t *testing.T) {
|
t.Run("succeeds when", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, testcase := range validTestcases(t) {
|
for _, testcase := range validTestcases(t) {
|
||||||
testcase := testcase
|
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
require.True(t, command.IsValid(testcase.inp))
|
require.True(t, command.IsValid(testcase.inp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails when", func(t *testing.T) {
|
t.Run("fails when", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, testcase := range invalidTestcases(t) {
|
for _, testcase := range invalidTestcases(t) {
|
||||||
testcase := testcase
|
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
require.False(t, command.IsValid(testcase.inp))
|
require.False(t, command.IsValid(testcase.inp))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
require.Equal(t, command.Top(), command.New())
|
||||||
|
require.Equal(t, "/foo", command.New("foo").String())
|
||||||
|
require.Equal(t, "/foo/bar", command.New("foo", "bar").String())
|
||||||
|
require.Equal(t, "/foo/bar/baz", command.New("foo", "bar/baz").String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseCommand(t *testing.T) {
|
func TestParseCommand(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
t.Run("succeeds when", func(t *testing.T) {
|
t.Run("succeeds when", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, testcase := range validTestcases(t) {
|
for _, testcase := range validTestcases(t) {
|
||||||
testcase := testcase
|
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cmd, err := command.Parse("/elem0/elem1/elem2")
|
cmd, err := command.Parse("/elem0/elem1/elem2")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, cmd)
|
require.NotEmpty(t, cmd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("fails when", func(t *testing.T) {
|
t.Run("fails when", func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, testcase := range invalidTestcases(t) {
|
for _, testcase := range invalidTestcases(t) {
|
||||||
testcase := testcase
|
|
||||||
|
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cmd, err := command.Parse(testcase.inp)
|
cmd, err := command.Parse(testcase.inp)
|
||||||
require.ErrorIs(t, err, command.ErrParse)
|
|
||||||
require.ErrorIs(t, err, testcase.err)
|
require.ErrorIs(t, err, testcase.err)
|
||||||
require.Nil(t, 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSegments(t *testing.T) {
|
||||||
|
require.Empty(t, command.Top().Segments())
|
||||||
|
require.Equal(t, []string{"foo", "bar", "baz"}, command.MustParse("/foo/bar/baz").Segments())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCovers(t *testing.T) {
|
||||||
|
require.True(t, command.MustParse("/foo/bar/baz").Covers(command.MustParse("/foo/bar/baz")))
|
||||||
|
require.True(t, command.MustParse("/foo/bar").Covers(command.MustParse("/foo/bar/baz")))
|
||||||
|
require.False(t, command.MustParse("/foo/bar/baz").Covers(command.MustParse("/foo/bar")))
|
||||||
|
require.True(t, command.MustParse("/").Covers(command.MustParse("/foo")))
|
||||||
|
require.True(t, command.MustParse("/").Covers(command.MustParse("/foo/bar/baz")))
|
||||||
|
require.False(t, command.MustParse("/foo").Covers(command.MustParse("/foo00")))
|
||||||
|
require.False(t, command.MustParse("/foo/bar").Covers(command.MustParse("/foo/bar00")))
|
||||||
|
}
|
||||||
|
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
inp string
|
inp string
|
||||||
@@ -134,20 +141,6 @@ func invalidTestcases(t *testing.T) []errorTestcase {
|
|||||||
},
|
},
|
||||||
err: command.ErrDisallowsTrailingSlash,
|
err: command.ErrDisallowsTrailingSlash,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
testcase: testcase{
|
|
||||||
name: "only reserved ucan namespace",
|
|
||||||
inp: "/ucan",
|
|
||||||
},
|
|
||||||
err: command.ErrUCANNamespaceReserved,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
testcase: testcase{
|
|
||||||
name: "reserved ucan namespace prefix",
|
|
||||||
inp: "/ucan/elem0/elem1/elem2",
|
|
||||||
},
|
|
||||||
err: command.ErrUCANNamespaceReserved,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
testcase: testcase{
|
testcase: testcase{
|
||||||
name: "uppercase character are present",
|
name: "uppercase character are present",
|
||||||
86
pkg/container/Readme.md
Normal file
86
pkg/container/Readme.md
Normal 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 |
|
||||||
|
|
||||||
|

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

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

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Conclusion:
|
||||||
|
- CAR files are heavy for this usage, notably because they carry the CIDs of the tokens
|
||||||
|
- compression works quite well and warrants its usage even with a single token
|
||||||
|
- DAG-CBOR outperform CAR files everywhere, and comes with a tiny ~3 bytes per token overhead.
|
||||||
|
|
||||||
|
**Formats beside DAG-CBOR and CAR, with or without base64, have been removed. They are in the git history though.**
|
||||||
109
pkg/container/SPEC.md
Normal file
109
pkg/container/SPEC.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# UCAN container Specification v0.1.0
|
||||||
|
|
||||||
|
## Editors
|
||||||
|
|
||||||
|
* [Michael Muré], [Consensys]
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
* [Michael Muré], [Consensys]
|
||||||
|
* [Hugo Dias]
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14] when, and only when, they appear in all capitals, as shown here.
|
||||||
|
|
||||||
|
# 0 Abstract
|
||||||
|
|
||||||
|
[User-Controlled Authorization Network (UCAN)][UCAN] is a trustless, secure, local-first, user-originated authorization and revocation scheme. This document describes a container format for transmitting one or more UCAN tokens as bytes, regardless of the transport.
|
||||||
|
|
||||||
|
# 1 Introduction
|
||||||
|
|
||||||
|
The UCAN spec itself is transport agnostic. This specification describes how to transfer one or more [UCAN] tokens bundled together, regardless of the transport.
|
||||||
|
|
||||||
|
# 2 Container format
|
||||||
|
|
||||||
|
## 2.1 Inner structure
|
||||||
|
|
||||||
|
UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into DAG-CBOR bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored.
|
||||||
|
|
||||||
|
All the tokens' bytes MUST be assembled in a [CBOR] array. The ordering of tokens in the array MUST NOT matter. This array SHOULD NOT have duplicate entries.
|
||||||
|
|
||||||
|
That array is then inserted as the value under the `ctn-v1` string key, in a CBOR map. There MUST NOT be other keys.
|
||||||
|
|
||||||
|
For clarity, the CBOR shape is given below:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ctn-v1": [
|
||||||
|
<token1 bytes>,
|
||||||
|
<token2 bytes>,
|
||||||
|
<token3 bytes>,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.2 Serialisation
|
||||||
|
|
||||||
|
To serialize the container into bytes, the inner CBOR structure MUST then be serialized into bytes according to the CBOR specification. The resulting bytes MAY be compressed by a supported algorithm, then MAY be encoded with a supported base encoding.
|
||||||
|
|
||||||
|
The following compression algorithms are REQUIRED to be supported:
|
||||||
|
- [GZIP]
|
||||||
|
|
||||||
|
The following base encoding combinations are REQUIRED to be supported:
|
||||||
|
- base64, standard alphabet, padding
|
||||||
|
- base64, URL alphabet, no padding
|
||||||
|
|
||||||
|
The CBOR bytes MUST be prepended by a single byte header to indicate the selection combination of base encoding and compression. This header value MUST be set according to the following table:
|
||||||
|
|
||||||
|
| Header as hex | Header as ASCII | Base encoding | Compression |
|
||||||
|
|---------------|-----------------|-------------------------|----------------|
|
||||||
|
| 0x40 | @ | raw bytes | no compression |
|
||||||
|
| 0x42 | B | base64 std padding | no compression |
|
||||||
|
| 0x43 | C | base64 url (no padding) | no compression |
|
||||||
|
| 0x4D | M | raw bytes | gzip |
|
||||||
|
| 0x4F | O | base64 std padding | gzip |
|
||||||
|
| 0x50 | P | base64 url (no padding) | gzip |
|
||||||
|
|
||||||
|
For clarity, the resulting serialisation is in the form of `<header byte><cbor bytes, optionally compressed, optionally encoded>`.
|
||||||
|
|
||||||
|
# 3 FAQ
|
||||||
|
|
||||||
|
## 3.1 Why not include the UCAN CIDs?
|
||||||
|
|
||||||
|
Several attacks are possible if UCAN tokens aren't validated. If CIDs aren't validated, at least two attacks are possible: [privilege escalation] and [cache poisoning], as UCAN delegation proofs depends on a correct hash-linked structure.
|
||||||
|
|
||||||
|
By not including the CID in the container, the recipient is forced to hash (and thus validate) the CIDs for each token. If presented with a claimed CID paired with the token bytes, implementers could ignore CID validation, breaking a core part of the proof chain security model. Hash functions are very fast on a couple kilobytes of data so the overhead is still very low. It also significantly reduces the size of the container.
|
||||||
|
|
||||||
|
## 3.2 Why compress? Why not always compress?
|
||||||
|
|
||||||
|
Compression is a relatively demanding operation. As such, using it is a tradeoff between size on the wire and CPU/memory usage, both when writing and reading a container. The transport itself can make compression worthwhile or not: for example, HTTP/2 and HTTP/3 headers are already compressed, but HTTP/1 headers are not. This being highly contextual, the choice is left to the final implementer.
|
||||||
|
|
||||||
|
# 4 Implementation recommendations
|
||||||
|
|
||||||
|
## 4.1 Dissociate reader and writer
|
||||||
|
|
||||||
|
While it is tempting to write a single implementation to read and write a container, it is RECOMMENDED to separate the implementation into a reader and a writer. The writer can simply accept arbitrary tokens as bytes, while the reader provides a read-only view with convenient access functions.
|
||||||
|
|
||||||
|
# 5 Acknowledgments
|
||||||
|
|
||||||
|
Many thanks to all the [Fission] team and in particular to [Brooklyn Zelenka] for creating and pushing [UCAN] and other critical pieces like [WNFS], and generally being awesome and supportive people.
|
||||||
|
|
||||||
|
<!-- External Links -->
|
||||||
|
|
||||||
|
[BCP 14]: https://www.rfc-editor.org/info/bcp14
|
||||||
|
[Brooklyn Zelenka]: https://github.com/expede
|
||||||
|
[CBOR]: https://www.rfc-editor.org/rfc/rfc8949.html
|
||||||
|
[Consensys]: https://consensys.io/
|
||||||
|
[Delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
||||||
|
[Fission]: https://fission.codes
|
||||||
|
[GZIP]: https://datatracker.ietf.org/doc/html/rfc1952
|
||||||
|
[Hugo Dias]: https://github.com/hugomrdias
|
||||||
|
[Invocation]: https://github.com/ucan-wg/invocation
|
||||||
|
[Michael Muré]: https://github.com/MichaelMure/
|
||||||
|
[Promise]: https://github.com/ucan-wg/promise/tree/v1-rc1
|
||||||
|
[Revocation]: https://github.com/ucan-wg/revocation/tree/first-draft
|
||||||
|
[UCAN]: https://github.com/ucan-wg/spec
|
||||||
|
[WNFS]: https://github.com/wnfs-wg
|
||||||
|
[cache poisoning]: https://en.wikipedia.org/wiki/Cache_poisoning
|
||||||
|
[privilede escalation]: https://en.wikipedia.org/wiki/Privilege_escalation
|
||||||
263
pkg/container/car.go
Normal file
263
pkg/container/car.go
Normal 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)
|
||||||
|
}
|
||||||
85
pkg/container/car_test.go
Normal file
85
pkg/container/car_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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) {
|
||||||
|
// Note: this fuzzing is somewhat broken.
|
||||||
|
// After some time, the fuzzer discover that a varint can be serialized in different
|
||||||
|
// ways that lead to the same integer value. This means that the CAR format can have
|
||||||
|
// multiple legal binary representation for the exact same data, which is what we are
|
||||||
|
// trying to detect here. Ideally, the format would be stricter, but that's how things
|
||||||
|
// are.
|
||||||
|
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
pkg/container/img/alloc_byte.png
Normal file
BIN
pkg/container/img/alloc_byte.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
pkg/container/img/alloc_count.png
Normal file
BIN
pkg/container/img/alloc_count.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
pkg/container/img/cpu.png
Normal file
BIN
pkg/container/img/cpu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
pkg/container/img/overhead_bytes.png
Normal file
BIN
pkg/container/img/overhead_bytes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
pkg/container/img/overhead_percent.png
Normal file
BIN
pkg/container/img/overhead_percent.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
118
pkg/container/packaging.go
Normal file
118
pkg/container/packaging.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const containerVersionTag = "ctn-v1"
|
||||||
|
|
||||||
|
type header byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerRawBytes = header(0x40)
|
||||||
|
headerBase64StdPadding = header(0x42)
|
||||||
|
headerBase64URL = header(0x43)
|
||||||
|
headerRawBytesGzip = header(0x4D)
|
||||||
|
headerBase64StdPaddingGzip = header(0x4F)
|
||||||
|
headerBase64URLGzip = header(0x50)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h header) encoder(w io.Writer) *payloadWriter {
|
||||||
|
res := &payloadWriter{rawWriter: w, writer: w, header: h}
|
||||||
|
|
||||||
|
switch h {
|
||||||
|
case headerBase64StdPadding, headerBase64StdPaddingGzip:
|
||||||
|
b64Writer := base64.NewEncoder(base64.StdEncoding, res.writer)
|
||||||
|
res.writer = b64Writer
|
||||||
|
res.closers = append([]io.Closer{b64Writer}, res.closers...)
|
||||||
|
case headerBase64URL, headerBase64URLGzip:
|
||||||
|
b64Writer := base64.NewEncoder(base64.RawURLEncoding, res.writer)
|
||||||
|
res.writer = b64Writer
|
||||||
|
res.closers = append([]io.Closer{b64Writer}, res.closers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h {
|
||||||
|
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
|
||||||
|
gzipWriter := gzip.NewWriter(res.writer)
|
||||||
|
res.writer = gzipWriter
|
||||||
|
res.closers = append([]io.Closer{gzipWriter}, res.closers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func payloadDecoder(r io.Reader) (io.Reader, error) {
|
||||||
|
headerBuf := make([]byte, 1)
|
||||||
|
_, err := r.Read(headerBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h := header(headerBuf[0])
|
||||||
|
|
||||||
|
switch h {
|
||||||
|
case headerRawBytes,
|
||||||
|
headerBase64StdPadding,
|
||||||
|
headerBase64URL,
|
||||||
|
headerRawBytesGzip,
|
||||||
|
headerBase64StdPaddingGzip,
|
||||||
|
headerBase64URLGzip:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown container header")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h {
|
||||||
|
case headerBase64StdPadding, headerBase64StdPaddingGzip:
|
||||||
|
r = base64.NewDecoder(base64.StdEncoding, r)
|
||||||
|
case headerBase64URL, headerBase64URLGzip:
|
||||||
|
r = base64.NewDecoder(base64.RawURLEncoding, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h {
|
||||||
|
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
|
||||||
|
gzipReader, err := gzip.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r = gzipReader
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ io.WriteCloser = &payloadWriter{}
|
||||||
|
|
||||||
|
// payloadWriter is tasked with two things:
|
||||||
|
// - prepend the header byte
|
||||||
|
// - call Close() on all the underlying io.Writer
|
||||||
|
type payloadWriter struct {
|
||||||
|
rawWriter io.Writer
|
||||||
|
writer io.Writer
|
||||||
|
header header
|
||||||
|
headerWrote bool
|
||||||
|
closers []io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *payloadWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if !w.headerWrote {
|
||||||
|
_, err := w.rawWriter.Write([]byte{byte(w.header)})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
w.headerWrote = true
|
||||||
|
}
|
||||||
|
return w.writer.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *payloadWriter) Close() error {
|
||||||
|
var errs error
|
||||||
|
for _, closer := range w.closers {
|
||||||
|
if err := closer.Close(); err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
167
pkg/container/reader.go
Normal file
167
pkg/container/reader.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"iter"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/cbor"
|
||||||
|
"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")
|
||||||
|
var ErrMultipleInvocations = fmt.Errorf("multiple invocations")
|
||||||
|
|
||||||
|
// Reader is a token container reader. It exposes the tokens conveniently decoded.
|
||||||
|
type Reader map[cid.Cid]token.Token
|
||||||
|
|
||||||
|
// FromBytes decodes a container from a []byte
|
||||||
|
func FromBytes(data []byte) (Reader, error) {
|
||||||
|
return FromReader(bytes.NewReader(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromString decodes a container from a string
|
||||||
|
func FromString(s string) (Reader, error) {
|
||||||
|
return FromReader(strings.NewReader(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromReader decodes a container from an io.Reader.
|
||||||
|
func FromReader(r io.Reader) (Reader, error) {
|
||||||
|
payload, err := payloadDecoder(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := ipld.DecodeStreaming(payload, cbor.Decode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n.Kind() != datamodel.Kind_Map {
|
||||||
|
return nil, fmt.Errorf("invalid container format: expected map")
|
||||||
|
}
|
||||||
|
if n.Length() != 1 {
|
||||||
|
return nil, fmt.Errorf("invalid container format: expected single version key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the first (and only) key-value pair
|
||||||
|
it := n.MapIterator()
|
||||||
|
key, tokensNode, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := key.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid container format: version must be string")
|
||||||
|
}
|
||||||
|
if version != containerVersionTag {
|
||||||
|
return nil, fmt.Errorf("unsupported container version: %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokensNode.Kind() != datamodel.Kind_List {
|
||||||
|
return nil, fmt.Errorf("invalid container format: tokens must be a list")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctn := make(Reader, tokensNode.Length())
|
||||||
|
it2 := tokensNode.ListIterator()
|
||||||
|
for !it2.Done() {
|
||||||
|
_, val, err := it2.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
// If not found, delegation.ErrDelegationNotFound is returned.
|
||||||
|
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||||
|
tkn, err := ctn.GetToken(cid)
|
||||||
|
if err != nil { // only ErrNotFound expected
|
||||||
|
return nil, delegation.ErrDelegationNotFound
|
||||||
|
}
|
||||||
|
if tkn, ok := tkn.(*delegation.Token); ok {
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
return nil, delegation.ErrDelegationNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllDelegations returns all the delegation.Token in the container.
|
||||||
|
func (ctn Reader) GetAllDelegations() iter.Seq2[cid.Cid, *delegation.Token] {
|
||||||
|
return func(yield func(cid.Cid, *delegation.Token) bool) {
|
||||||
|
for c, t := range ctn {
|
||||||
|
if t, ok := t.(*delegation.Token); ok {
|
||||||
|
if !yield(c, t) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInvocation returns a single invocation.Token.
|
||||||
|
// If none are found, ErrNotFound is returned.
|
||||||
|
// If more than one invocation exists, ErrMultipleInvocations is returned.
|
||||||
|
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||||
|
var res *invocation.Token
|
||||||
|
for _, t := range ctn {
|
||||||
|
if inv, ok := t.(*invocation.Token); ok {
|
||||||
|
if res != nil {
|
||||||
|
return nil, ErrMultipleInvocations
|
||||||
|
}
|
||||||
|
res = inv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllInvocations returns all the invocation.Token in the container.
|
||||||
|
func (ctn Reader) GetAllInvocations() iter.Seq2[cid.Cid, *invocation.Token] {
|
||||||
|
return func(yield func(cid.Cid, *invocation.Token) bool) {
|
||||||
|
for c, t := range ctn {
|
||||||
|
if t, ok := t.(*invocation.Token); ok {
|
||||||
|
if !yield(c, t) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctn Reader) addToken(data []byte) error {
|
||||||
|
tkn, c, err := token.FromSealed(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctn[c] = tkn
|
||||||
|
return nil
|
||||||
|
}
|
||||||
245
pkg/container/serial_test.go
Normal file
245
pkg/container/serial_test.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
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
|
||||||
|
expectedHeader header
|
||||||
|
writer any
|
||||||
|
}{
|
||||||
|
{"Bytes", headerRawBytes, Writer.ToBytes},
|
||||||
|
{"BytesWriter", headerRawBytes, Writer.ToBytesWriter},
|
||||||
|
{"BytesGzipped", headerRawBytesGzip, Writer.ToBytesGzipped},
|
||||||
|
{"BytesGzippedWriter", headerRawBytesGzip, Writer.ToBytesGzippedWriter},
|
||||||
|
{"Base64StdPadding", headerBase64StdPadding, Writer.ToBase64StdPadding},
|
||||||
|
{"Base64StdPaddingWriter", headerBase64StdPadding, Writer.ToBase64StdPaddingWriter},
|
||||||
|
{"Base64StdPaddingGzipped", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzipped},
|
||||||
|
{"Base64StdPaddingGzippedWriter", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzippedWriter},
|
||||||
|
{"Base64URL", headerBase64URL, Writer.ToBase64URL},
|
||||||
|
{"Base64URLWriter", headerBase64URL, Writer.ToBase64URLWriter},
|
||||||
|
{"Base64URLGzip", headerBase64URLGzip, Writer.ToBase64URLGzip},
|
||||||
|
{"Base64URLGzipWriter", headerBase64URLGzip, Writer.ToBase64URLGzipWriter},
|
||||||
|
} {
|
||||||
|
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(data)
|
||||||
|
tokens[c] = dlg
|
||||||
|
dataSize += len(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader Reader
|
||||||
|
var serialLen int
|
||||||
|
|
||||||
|
switch fn := tc.writer.(type) {
|
||||||
|
case func(ctn Writer, w io.Writer) error:
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
err := fn(writer, buf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
serialLen = buf.Len()
|
||||||
|
|
||||||
|
h, err := buf.ReadByte()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, byte(tc.expectedHeader), h)
|
||||||
|
err = buf.UnreadByte()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reader, err = FromReader(bytes.NewReader(buf.Bytes()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
case func(ctn Writer) ([]byte, error):
|
||||||
|
b, err := fn(writer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
serialLen = len(b)
|
||||||
|
|
||||||
|
require.Equal(t, byte(tc.expectedHeader), b[0])
|
||||||
|
|
||||||
|
reader, err = FromBytes(b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
case func(ctn Writer) (string, error):
|
||||||
|
s, err := fn(writer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
serialLen = len(s)
|
||||||
|
|
||||||
|
require.Equal(t, byte(tc.expectedHeader), s[0])
|
||||||
|
|
||||||
|
reader, err = FromString(s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("data size %d, container size %d, overhead: %d%%, %d bytes",
|
||||||
|
dataSize, serialLen, int(float32(serialLen-dataSize)/float32(dataSize)*100.0), serialLen-dataSize)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}{
|
||||||
|
{"Bytes", Writer.ToBytesWriter, FromReader},
|
||||||
|
{"BytesGzipped", Writer.ToBytesGzippedWriter, FromReader},
|
||||||
|
{"Base64StdPadding", Writer.ToBase64StdPaddingWriter, FromReader},
|
||||||
|
{"Base64StdPaddingGzipped", Writer.ToBase64StdPaddingGzippedWriter, FromReader},
|
||||||
|
{"Base64URL", Writer.ToBase64URLWriter, FromReader},
|
||||||
|
{"Base64URLGzip", Writer.ToBase64URLGzipWriter, FromReader},
|
||||||
|
} {
|
||||||
|
writer := NewWriter()
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
_, _, data := randToken()
|
||||||
|
writer.AddSealed(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 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)),
|
||||||
|
}
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := delegation.Root(iss, aud, cmd, pol, opts...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
b, c, err := t.ToSealed(priv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return t, c, b
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzContainerRead(f *testing.F) {
|
||||||
|
// Generate a corpus
|
||||||
|
for tokenCount := 0; tokenCount < 10; tokenCount++ {
|
||||||
|
writer := NewWriter()
|
||||||
|
for i := 0; i < tokenCount; i++ {
|
||||||
|
_, _, data := randToken()
|
||||||
|
writer.AddSealed(data)
|
||||||
|
}
|
||||||
|
data, err := writer.ToBytes()
|
||||||
|
require.NoError(f, err)
|
||||||
|
|
||||||
|
f.Add(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// search for panics
|
||||||
|
_, _ = FromBytes(data)
|
||||||
|
|
||||||
|
if time.Since(start) > 100*time.Millisecond {
|
||||||
|
panic("too long")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
pkg/container/testdata/sample-v1.car
vendored
Normal file
BIN
pkg/container/testdata/sample-v1.car
vendored
Normal file
Binary file not shown.
1
pkg/container/testvectors/Base64StdPadding
Normal file
1
pkg/container/testvectors/Base64StdPadding
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/testvectors/Base64StdPaddingGzipped
Normal file
1
pkg/container/testvectors/Base64StdPaddingGzipped
Normal file
@@ -0,0 +1 @@
|
|||||||
|
OH4sIAAAAAAAA/5zX+ZOUxR3HcY7FwhKToFJGFAU1ghzL8/RztpxzrzsHszuzcy1Ru5/unmd6dubZnXsGE9EtFVnFSCiNEFfFiCSKqIVI8ECBKGiJiMSTFdCYeGDK6GpMWcSUtUOq8ts++Qfev7zq+6nu+5lRys+riKuTYwdvSCw9NrDty6c/Wf668n73nFsqQ988uh1Z8rTNu9X6hkO33S/vG1i92+//ontN6/BubU54fntiUnXqrPNfo9p5bnTWd3PP3IhMt/z52L5i2UD5+aQnvVRsFVqFeQWjVdxsoDKp6SRDrsjS+hUNNZjNtFdJsa1CzUwCxSK8imjO7curtFHwBqy8US3hlBT08XQlkWeGkSPmfGZZ8zEqGLTWOzX9yyemGZli8X+b1XAxESdxALIVX6E3xkiY1coeswLDMOp1BVRgOrGnqxEtukoJo9fqub7fQD09Rmv3z/vRYtZaQT1lOs4olvH/nyU5WkIPmEAQkAYg5lhDqigxjeimJoqEMgC4JipMViURiCYTBQHLBHGZCkyRNcgYzVt5gwauDa67+uiirx+KuQZnjgB9/2H3noUzrj79rQdv27HpuSufnOrfvu3Myf98rfv5Tfds2OX9de9w92Mfm/m9s2eOe/etzPnd/1jdeqI8vvuVbYOb8Q2Ltky0CZRDlAfcJKqHGSklXclQxR+mPKbgcM5n5awkRYmwM2SltSQqjRYo3RFD/lDOnW/TYrxd7ayVQaQme10dVl3RPCardrQHkFJUvTUT2QCyk20CaQpUFF1CnFJJpDqVoGYSDKhuMJVDlaoqpIIMTIZVhcoMcCYAnSgC1GkT6BF9x2d9X19nTBw/+cgI0KO+/W+sk4cmDz7/7AM7D7s2+BdIFw0vGLv9yzm7Vv3h1QN/WZHwxy9Qj0evv2ftkX891BLy7V2cWjY+sGV9f+zYHh4eYxOoXE8E3BFPNB+q0Kw7XexLpfXeXiPrd5WC3N0Wj7iBFfeQMqi06aMFKtcMf9wRrbN4EKOa7KxYfRkQ0/3ecJ/LV/dnkl2OgEPSvIViNmQDyE62CSRDgWEBilwECpWwADVsKkQnhgQZ13QZMVWVITGRCLCmI8ANCWuiSiSqNIF+ExuqnnSds6b7zv4XRoDWzN08c6H1GByE86wXj+561L/q9pc+G1jy4/ya71uPb3kXLJmwe/jlHFu3+NhFK2/8Sce4sZtiLQezq/uVBx6OXnv3n0+zC+T1F5MuENTKFW8u0ld3GcVQNl0gZodSiCk01UkMV9SVauRNXR0tEOtkic5YKtXlDgKt4VFKuhWoGiku417g6wuUimZ7DbTJDAGv2waQnWwTCIka0XVJ4JCKVJGQLGqmIamaYaiQKwpVIFQMgZgUCQLSicqxTKCuyxLATaD4JZf/4mhvbf19y15uGwFyrodHo9vOP7kzObhklvuTfQ/j3IQLn5nz6dKNn0xo7H/nkQs+HT70VvLwmWzvm3c/Ofn7g3f91jvkb7/5vpWHHl8EX3/B7sSxiq9R5bkIsAJKJurhPj9z1GgDh3gy18ViyOP2sa5oUso4C9aoJ84fq/fKKm3LtRe8gWTe568V8z7sMCU1FnA7gJnOhXq9Lk+XaHTamTgb2SaQQIFEEaJcgLIqQEEQgaloGsKIMs5ESRYFQVYlU2UEEVnEXCUSA0zTBLUJJM/nb351ecu/h6dLlREg67y3b3be96Ky/p7L7jzeO9s5Y9Ka5fNxZHc8cVC6+K83iXPfO/GjL5ybrbZrWq4QPu64rfu7adMP7v/uyt/f0r7yJjBpkk2gvgQPOTusdDGqhOuKNxROw0ba29lFUjCbqNV1MSDWZZxgViRkjBbI8jUa1aKl0Y42qz3sTVt11dQSeo5WqsGKD1NY0sN6QS8aUhXaALKTPQWEFU0nKuMi0aGCDayoJjIwZjrGHMoEAUI1QTINxUAShCo3DCwTWdOZ0QRaAhdf1yIf7unm5atGgObsH8rs5FO2wH1r/zb84c8WrNLL179/za+G51WvkvaKXjguP3XmXWetHTix59zd32Te9h2ftGTbhU/dv3PvjcvAlBnjzrAJVOjMUKhKznS4jZR10ZWMVz1JZ5zWHbmuSqnglzpSMqPVuiPgkkcL1Butd1DQVQmGgByp0S5Xo5SLKXlfvD1QDolQZ2oxXIwFk42AUrEBZCfbBBJlSZFFReIIQZlqhgR1U0MSlilROBAMTACBAjYZMyjUich1w1B+2Dd4auIOnLPCY3wFNzxIc+tGgKqX5an27Qtn37C2d+ykS7XPnz30TSi9onDd2eeMK7N4/6x78XLQMnnlm6/mFlbIeRee9qWybfCjBceSi6YVdg2s7ZxgE6ga7sp29QGTJ+LOWAooJcsXJ9loQgvFmZOXOgNt7nox1jBRyAVGC8SNbK0zCyKmrDsDRrrhShLqqGQ1DKJyJVMvknY/jNV52hQbThtAdrJNIIgVSdBljUuCwCSgMxGbBhENJBLIBWYgAzCsAZNAwdAFXeZMVVX4wywKTaBZTxz9E9y6b0JPgu8YAZo9A258I6l75KfJM2Omb+T3zjDPTW393SUXnHjswB3DY05a7wQ7ux9fXjtjx4QxS6fPfmns3598sWf9lkvB83sOXrxytt1XXK5IHCHk1Tx6sc0fTPdlCjTBYmWHHNH8XR2s3khVjKqeFD085R71BeXrdVytWKqSqCaDMiehTm/Q4jCVrUcdrBKTOfR4vEGto01N2bkgG9lTF6RBwFQicAqJALGiaropCwggCWMuAyJThAxETUIA0CVV57qkUEVlOtSbQGMGJuaOTFzyx/FHjj81AvTqwIExd3vB9FuXrdHmrp7106G33/vgo9tP3zjF8t5ZOLz52q2bjt1x6+yTH6zQbmGpiasOJ5Xw+oU7n3v8YMuH77Vs/bbF7kcVmjBmuhuhVMyVUNWiSXg9XMs2yvUkpCnma8BKxlK0eqXT8o0WqBgpmI5YKuLvyLiUeqTsp42S6qiasUCjlFG9Xk1pr3ujpN0BLWoDyE62CSRpMtBFTeaipKqYyZJKTEwURGWkcVHAWDcY0TSTYipjjFUu6YL6w2P8v0AnP45M2du/fWgHeOWD/wQAAP//IuQ6Y1MQAAA=
|
||||||
1
pkg/container/testvectors/Base64URL
Normal file
1
pkg/container/testvectors/Base64URL
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/testvectors/Base64URLGzip
Normal file
1
pkg/container/testvectors/Base64URLGzip
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PH4sIAAAAAAAA_5zWa5cUxR3HcVcRiejmuCII3siKwgnubnd1dVW1eIS5rzM9w8zs3HZETFd19_T07PTcZ2cH1KMgoB41YgIo3nM0G1GCRhCRiILIEaIhIEo0Bi9c9ewaFfEazYMd8iCPdvIGvk8-p_71e1RnZaujyt_e2_LQLYk5Fbb1QefFR9-_5LzWO59-9PfWoWV_nN_f177z06eH31j13Je8-Wpk35bTccKjtV_22MRfnz9DufuSg8Ntn02KzZtw83sLxj-mGE441FIoVZhidal9qTl8J9fJdRRZJz_IlIpaI2pavTKjDVxZR_5MQXa4aFIFgr-gMCrrOTWUqWNajNZERzin0GLRz7IxsTcfAgnGsqrRpedyXVQpMq2Wn5K68dmLWLpU-p9msRbQ-usuHEsGorZK0BbwxWzxvCdrhDK2vL23UMCRTM0RlStVxPK5vpsXMaWvj3Vee90i5Wq9s6r0VbRTWalC__-smtXKyu8MERNOwJSZkCACeAFgzkC6xGkYAhOLqkJEiBXJkJAuCUBUTMIA4KEgAEWzchbT5DXHfxwc7LiqNnPmRwdHgBa8NO6jDQ9uvW6wa7DHYetfP3DxO4s7JhybLa1YcerUV-Yae7aDww_P2_zjvH9euWqOOW3yjBWtO2__bKzLvfnR_enEpmaBMtFuy4qEsgrlqRMpPm86mql6apjI4V63V_bFgl5WT-USpQhDowVK2amiFmM8tEUsiRJcCeowHM9AvVQreLGE83LMkcBBI99dAE0ANZNtAGFewJjwkilBJPG8TphoKEQhmihKpsgBXuIVhXEGRRQpRGAmhzkRqIpOaQNo4afuoTWTj60cfjw5fQQoxy98ZPGmF3YsWLJo8voJ587Y9-X38geyLBq_nHXZIy9fPun5m1pebqVzv7J-NuaM08XEzYl3v3kmtfuCr2Yc-VAa3vtxS7NAnpKSKsBEzVaMphWdFB0kgNSEgHpwiPe7a0qaxYtVG_SVq_7RAhkey-asYmfUZ1XKMbGSr-Vo1UZ60rZyNV6Uy8l8MY_iES1ic0abAGom2wCCEqcLVFRNpEKBaghAaCBJUYgKOVNVGMQCUyXRQBqFTJWwKQkIIoZFQW8AvZV64bRDez_6flYbbmu8oDa_euu2yJB9w1vX3uO-YrZvobj2yK07V7-1YebjrRcgbsvS4fiyJ9_gpx4KvT5n_A7PTz-cqF_asuuaoTFnn2N-bbU2CaSHi0bJlrEi3kyh6NFrgqOu59VKOJ2t9Xj4sllT4wXqJdFQfzI2aiBvIGSG_Zls2IsDSdKblRV3QlQz2X7Z2evudVBk16QIM1yiCZsBaiLbAOIEnROQppu8AjgKiKqIhgQVwDQCTCKJkqCIBIqGpoicCilvcoxDWJewcPLE3TD0fHzb0y_BrtRd2gjQN3fd--36ju_CYzuV8adcsOeA95UvVv-09N1t05_bN7DllouWTN-18r4nTm-9Y8uZ2L02fj1XLizHlzyfrZ-x65Pt503b0uyJs0oDhpWuSh4vwFpJjlcDciKRCbtQTzmvxkWrKIWCUQm51FomO1ogKyMLyXSPx0xHxKrixwFT7a9XgzUUTuedXgDi8XKmgGJmJOwMNQHUTLYBhERGkE6YCTXEYYAYxxuE5xGnCbopAkWjAtCgbhCMKAUQmQqkvA4wJaQBpNyhPfDyK9Ijs-yOv40ArTv_hoMtSbB12NjtzD1Ef35ow47k_D3frn41f-2LX6YmdTxzzba3t180_7XFV5m-fz304tj2Td-t7Nx_oblszv7ctI2Hmz1xhayl1myOkhEOxUt2S_S4shFvMI7dIbseyPa4AlalJ8q6g2KhGBktUJGVfQEaSsXtsK7EnXnC6zIP84ZHrYUyGR9vcxXj9lwuaIvG7E0ANZM9-QcBHkNCNFMFEKhYFTRiSBoUOV0RTE3DKmWMCJKhijzgeJWakgoIwyLUuAZQSlq-Y177mBOSHt49AjRF-PBAe9d1H3ffcvyZczrHLAhP9X3jC5y797YFa-mNxYl_evDqo4fu_8NZh3d88un1R1f_5cfBf8_76p3tv_psyooll_k-nj2mWaC6b8BCmVrCytYjIFZKF1wytmUFKdijVuoCLIdrBV8dk3AxbIwWqJRAkpyRMnkY9ReLcipX8PR3u6RcrheykFINGCCc7-lJ25LMpjUB1Ez25IljDHFYpCYUiUYR4yVkUMKYIEiSiTDBSFcRgAajPAOU6ibjmShBToOwAXRf-6V31zceuXHdjHV9I0D7Jn7wgXzr4TVX_-PYw_mpLatO69_qW7R1Q-m8o9OcsGP2rM_f3lM_3n0_-aL18zWHV-eHzkik9r4_LriMt16a98Tm889sdiTASDqRwFLW3evj6_mU6i96cqInGEY4Ea0F1brlsPUXJSlCrcpogfRCzGll1YKRsqXrWEuVajq21wK5buKv5FxOV7wQy4Wz5Wwg3NRIaCbbABKopoi6BkymSSrTRCpSQ8UqEEQBmkTTBQp0VcSGBgAvYaiZCgQMUiz89wXdVtm7cNtjqvLmb5ZtHAHq2vfX9Uem2q-ZXTzw3oub0ruemj52f29hwuWeZ2_6_qylidcO5d9p3310-_wptTV3Dv1WXZzbuLsNDU2-dzDy7Sf84MxxTQKZAxWi5wO0Agci0WTOnStHklUpbPaHSi7JcsV9UbvH6_IHzHTVNeqRwLtlj6rV_T3ZehxGex2KLA3EXX4XhNmaGZdj-VjCRgy37HA4mhkJTWQbQBImhOkcbyIeQA1oiOcNBVBEdBWYlOc0IgBAREPnBYoABSavUAI0RhFuAM22T_nzs8NTTyz94YFVI0Bze_9-4utz3dvfW5B7fXL7uJWvr-1rO-uG5evusY_jlk-ynvzheOfE5IFIt4ieunAfO3bK-CWuX2w-0HbQP_fDi-8Y2NPsitNYLCfr2YLGEhrwg7LNoL3ZWKLSAzwDxNvNV_OA1L0oFVdToz5xuURswBupyoo7Ggua8Vg8kIjV5TLvcyCUDJsV7PXKuglTehn5mwBqJnvyBSEd6qpKTV5HCEOeU4mBCJSwpImmIkBd4BRGoUEUHUlMQ6amQVVVJB2CBpBwxRsH-9-cvnYnOfvd_wQAAP__hMJ751MQAAA
|
||||||
BIN
pkg/container/testvectors/Bytes
Normal file
BIN
pkg/container/testvectors/Bytes
Normal file
Binary file not shown.
BIN
pkg/container/testvectors/BytesGzipped
Normal file
BIN
pkg/container/testvectors/BytesGzipped
Normal file
Binary file not shown.
122
pkg/container/writer.go
Normal file
122
pkg/container/writer.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/cbor"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
||||||
|
type Writer map[string]struct{}
|
||||||
|
|
||||||
|
func NewWriter() Writer {
|
||||||
|
return make(Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
|
||||||
|
func (ctn Writer) AddSealed(data []byte) {
|
||||||
|
ctn[string(data)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBytes encode the container into raw bytes.
|
||||||
|
func (ctn Writer) ToBytes() ([]byte, error) {
|
||||||
|
return ctn.toBytes(headerRawBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBytesWriter is the same as ToBytes, but with an io.Writer.
|
||||||
|
func (ctn Writer) ToBytesWriter(w io.Writer) error {
|
||||||
|
return ctn.toWriter(headerRawBytes, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBytesGzipped encode the container into gzipped bytes.
|
||||||
|
func (ctn Writer) ToBytesGzipped() ([]byte, error) {
|
||||||
|
return ctn.toBytes(headerRawBytesGzip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBytesGzippedWriter is the same as ToBytesGzipped, but with an io.Writer.
|
||||||
|
func (ctn Writer) ToBytesGzippedWriter(w io.Writer) error {
|
||||||
|
return ctn.toWriter(headerRawBytesGzip, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64StdPadding encode the container into a base64 string, with standard encoding and padding.
|
||||||
|
func (ctn Writer) ToBase64StdPadding() (string, error) {
|
||||||
|
return ctn.toString(headerBase64StdPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64StdPaddingWriter is the same as ToBase64StdPadding, but with an io.Writer.
|
||||||
|
func (ctn Writer) ToBase64StdPaddingWriter(w io.Writer) error {
|
||||||
|
return ctn.toWriter(headerBase64StdPadding, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64StdPaddingGzipped encode the container into a pre-gzipped base64 string, with standard encoding and padding.
|
||||||
|
func (ctn Writer) ToBase64StdPaddingGzipped() (string, error) {
|
||||||
|
return ctn.toString(headerBase64StdPaddingGzip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64StdPaddingGzippedWriter is the same as ToBase64StdPaddingGzipped, but with an io.Writer.
|
||||||
|
func (ctn Writer) ToBase64StdPaddingGzippedWriter(w io.Writer) error {
|
||||||
|
return ctn.toWriter(headerBase64StdPaddingGzip, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64URL encode the container into base64 string, with URL-safe encoding and no padding.
|
||||||
|
func (ctn Writer) ToBase64URL() (string, error) {
|
||||||
|
return ctn.toString(headerBase64URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
|
||||||
|
func (ctn Writer) ToBase64URLWriter(w io.Writer) error {
|
||||||
|
return ctn.toWriter(headerBase64URL, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64URL encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding.
|
||||||
|
func (ctn Writer) ToBase64URLGzip() (string, error) {
|
||||||
|
return ctn.toString(headerBase64URLGzip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
|
||||||
|
func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error {
|
||||||
|
return ctn.toWriter(headerBase64URLGzip, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctn Writer) toBytes(header header) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := ctn.toWriter(header, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctn Writer) toString(header header) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := ctn.toWriter(header, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctn Writer) toWriter(header header, w io.Writer) (err error) {
|
||||||
|
encoder := header.encoder(w)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = encoder.Close()
|
||||||
|
}()
|
||||||
|
node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
|
||||||
|
qp.MapEntry(ma, containerVersionTag, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
|
||||||
|
for data, _ := range ctn {
|
||||||
|
qp.ListEntry(la, qp.Bytes([]byte(data)))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipld.EncodeStreaming(encoder, node, cbor.Encode)
|
||||||
|
}
|
||||||
18
pkg/container/writer_test.go
Normal file
18
pkg/container/writer_test.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriterDedup(t *testing.T) {
|
||||||
|
ctn := NewWriter()
|
||||||
|
|
||||||
|
_, _, sealed := randToken()
|
||||||
|
ctn.AddSealed(sealed)
|
||||||
|
require.Len(t, ctn, 1)
|
||||||
|
|
||||||
|
ctn.AddSealed(sealed)
|
||||||
|
require.Len(t, ctn, 1)
|
||||||
|
}
|
||||||
265
pkg/meta/meta.go
Normal file
265
pkg/meta/meta.go
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/secretbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("key not found in meta")
|
||||||
|
|
||||||
|
var ErrNotEncryptable = errors.New("value of this type cannot be encrypted")
|
||||||
|
|
||||||
|
// Meta is a container for meta key-value pairs in a UCAN token.
|
||||||
|
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||||
|
// and transformations, while hiding the IPLD complexity from the caller.
|
||||||
|
type Meta struct {
|
||||||
|
// This type must be compatible with the IPLD type represented by the IPLD
|
||||||
|
// schema { String : Any }.
|
||||||
|
|
||||||
|
Keys []string
|
||||||
|
Values map[string]ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMeta constructs a new Meta.
|
||||||
|
func NewMeta() *Meta {
|
||||||
|
return &Meta{Values: map[string]ipld.Node{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool retrieves a value as a bool.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
|
func (m *Meta) GetBool(key string) (bool, error) {
|
||||||
|
v, ok := m.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return false, ErrNotFound
|
||||||
|
}
|
||||||
|
return v.AsBool()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString retrieves a value as a string.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
|
func (m *Meta) GetString(key string) (string, error) {
|
||||||
|
v, ok := m.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return "", ErrNotFound
|
||||||
|
}
|
||||||
|
return v.AsString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptedString decorates GetString and decrypt its output with the given symmetric encryption key.
|
||||||
|
func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||||
|
v, err := m.GetBytes(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(decrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt64 retrieves a value as an int64.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
|
func (m *Meta) GetInt64(key string) (int64, error) {
|
||||||
|
v, ok := m.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
return v.AsInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFloat64 retrieves a value as a float64.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
|
func (m *Meta) GetFloat64(key string) (float64, error) {
|
||||||
|
v, ok := m.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return 0, ErrNotFound
|
||||||
|
}
|
||||||
|
return v.AsFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBytes retrieves a value as a []byte.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
|
func (m *Meta) GetBytes(key string) ([]byte, error) {
|
||||||
|
v, ok := m.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return v.AsBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptedBytes decorates GetBytes and decrypt its output with the given symmetric encryption key.
|
||||||
|
func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||||
|
v, err := m.GetBytes(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNode retrieves a value as a raw IPLD node.
|
||||||
|
// Returns ErrNotFound if the given key is missing.
|
||||||
|
func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
||||||
|
v, ok := m.Values[key]
|
||||||
|
if !ok {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a key/value pair in the meta set.
|
||||||
|
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||||
|
func (m *Meta) Add(key string, val any) error {
|
||||||
|
if _, ok := m.Values[key]; ok {
|
||||||
|
return fmt.Errorf("duplicate key %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := literal.Any(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Keys = append(m.Keys, key)
|
||||||
|
m.Values[key] = node
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEncrypted adds a key/value pair in the meta set.
|
||||||
|
// The value is encrypted with the given encryptionKey.
|
||||||
|
// Accepted types for the value are: string, []byte.
|
||||||
|
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||||
|
func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
||||||
|
var encrypted []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case string:
|
||||||
|
encrypted, err = secretbox.EncryptWithKey([]byte(val), encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
encrypted, err = secretbox.EncryptWithKey(val, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ErrNotEncryptable
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Add(key, encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Iterator interface {
|
||||||
|
Iter() iter.Seq2[string, ipld.Node]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include merges the provided meta into the existing one.
|
||||||
|
//
|
||||||
|
// If duplicate keys are encountered, the new value is silently dropped
|
||||||
|
// without causing an error.
|
||||||
|
func (m *Meta) Include(other Iterator) {
|
||||||
|
for key, value := range other.Iter() {
|
||||||
|
if _, ok := m.Values[key]; ok {
|
||||||
|
// don't overwrite
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.Values[key] = value
|
||||||
|
m.Keys = append(m.Keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of key/values.
|
||||||
|
func (m *Meta) Len() int {
|
||||||
|
return len(m.Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iter iterates over the meta key/values
|
||||||
|
func (m *Meta) Iter() iter.Seq2[string, ipld.Node] {
|
||||||
|
return func(yield func(string, ipld.Node) bool) {
|
||||||
|
for _, key := range m.Keys {
|
||||||
|
if !yield(key, m.Values[key]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
sort.Strings(m.Keys)
|
||||||
|
|
||||||
|
buf := strings.Builder{}
|
||||||
|
buf.WriteString("{")
|
||||||
|
|
||||||
|
for key, node := range m.Values {
|
||||||
|
buf.WriteString("\n\t")
|
||||||
|
buf.WriteString(key)
|
||||||
|
buf.WriteString(": ")
|
||||||
|
buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t"))
|
||||||
|
buf.WriteString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Values) > 0 {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
buf.WriteString("}")
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOnly returns a read-only version of Meta.
|
||||||
|
func (m *Meta) ReadOnly() ReadOnly {
|
||||||
|
return ReadOnly{meta: m}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone makes a deep copy.
|
||||||
|
func (m *Meta) Clone() *Meta {
|
||||||
|
res := &Meta{
|
||||||
|
Keys: make([]string, len(m.Keys)),
|
||||||
|
Values: make(map[string]ipld.Node, len(m.Values)),
|
||||||
|
}
|
||||||
|
copy(res.Keys, m.Keys)
|
||||||
|
for k, v := range m.Values {
|
||||||
|
res.Values[k] = v
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
130
pkg/meta/meta_test.go
Normal file
130
pkg/meta/meta_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package meta_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"maps"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMeta_Add(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type Unsupported struct{}
|
||||||
|
|
||||||
|
t.Run("error if not primitive or Node", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("encrypted meta", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := meta.NewMeta()
|
||||||
|
|
||||||
|
// string encryption
|
||||||
|
err = m.AddEncrypted("secret", "hello world", key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = m.GetString("secret")
|
||||||
|
require.Error(t, err) // the ciphertext is saved as []byte instead of string
|
||||||
|
|
||||||
|
decrypted, err := m.GetEncryptedString("secret", key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "hello world", decrypted)
|
||||||
|
|
||||||
|
// bytes encryption
|
||||||
|
originalBytes := make([]byte, 128)
|
||||||
|
_, err = rand.Read(originalBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = m.AddEncrypted("secret-bytes", originalBytes, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encryptedBytes, err := m.GetBytes("secret-bytes")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, originalBytes, encryptedBytes)
|
||||||
|
|
||||||
|
decryptedBytes, err := m.GetEncryptedBytes("secret-bytes", key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, originalBytes, decryptedBytes)
|
||||||
|
|
||||||
|
// error cases
|
||||||
|
t.Run("error on unsupported type", func(t *testing.T) {
|
||||||
|
err := m.AddEncrypted("invalid", 123, key)
|
||||||
|
require.ErrorIs(t, err, meta.ErrNotEncryptable)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on invalid key size", func(t *testing.T) {
|
||||||
|
err := m.AddEncrypted("invalid", "test", []byte("short-key"))
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "invalid key size")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on nil key", func(t *testing.T) {
|
||||||
|
err := m.AddEncrypted("invalid", "test", nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "encryption key is required")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIterCloneEquals(t *testing.T) {
|
||||||
|
m := meta.NewMeta()
|
||||||
|
|
||||||
|
require.NoError(t, m.Add("foo", "bar"))
|
||||||
|
require.NoError(t, m.Add("baz", 1234))
|
||||||
|
|
||||||
|
expected := map[string]ipld.Node{
|
||||||
|
"foo": basicnode.NewString("bar"),
|
||||||
|
"baz": basicnode.NewInt(1234),
|
||||||
|
}
|
||||||
|
|
||||||
|
// meta -> iter
|
||||||
|
require.Equal(t, expected, maps.Collect(m.Iter()))
|
||||||
|
|
||||||
|
// readonly -> iter
|
||||||
|
ro := m.ReadOnly()
|
||||||
|
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||||
|
|
||||||
|
// meta -> clone -> iter
|
||||||
|
clone := m.Clone()
|
||||||
|
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
||||||
|
|
||||||
|
// readonly -> WriteableClone -> iter
|
||||||
|
wclone := ro.WriteableClone()
|
||||||
|
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
||||||
|
|
||||||
|
require.True(t, m.Equals(wclone))
|
||||||
|
require.True(t, ro.Equals(wclone.ReadOnly()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInclude(t *testing.T) {
|
||||||
|
m1 := meta.NewMeta()
|
||||||
|
|
||||||
|
require.NoError(t, m1.Add("samekey", "bar"))
|
||||||
|
require.NoError(t, m1.Add("baz", 1234))
|
||||||
|
|
||||||
|
m2 := meta.NewMeta()
|
||||||
|
|
||||||
|
require.NoError(t, m2.Add("samekey", "othervalue")) // check no overwrite
|
||||||
|
require.NoError(t, m2.Add("otherkey", 1234))
|
||||||
|
|
||||||
|
m1.Include(m2)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]ipld.Node{
|
||||||
|
"samekey": basicnode.NewString("bar"),
|
||||||
|
"baz": basicnode.NewInt(1234),
|
||||||
|
"otherkey": basicnode.NewInt(1234),
|
||||||
|
}, maps.Collect(m1.Iter()))
|
||||||
|
}
|
||||||
64
pkg/meta/readonly.go
Normal file
64
pkg/meta/readonly.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package meta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"iter"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadOnly wraps a Meta into a read-only facade.
|
||||||
|
type ReadOnly struct {
|
||||||
|
meta *Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetBool(key string) (bool, error) {
|
||||||
|
return r.meta.GetBool(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetString(key string) (string, error) {
|
||||||
|
return r.meta.GetString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||||
|
return r.meta.GetEncryptedString(key, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||||
|
return r.meta.GetInt64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetFloat64(key string) (float64, error) {
|
||||||
|
return r.meta.GetFloat64(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
||||||
|
return r.meta.GetBytes(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||||
|
return r.meta.GetEncryptedBytes(key, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||||
|
return r.meta.GetNode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Len() int {
|
||||||
|
return r.meta.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||||
|
return r.meta.Iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||||
|
return r.meta.Equals(other.meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) String() string {
|
||||||
|
return r.meta.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) WriteableClone() *Meta {
|
||||||
|
return r.meta.Clone()
|
||||||
|
}
|
||||||
79
pkg/policy/glob.go
Normal file
79
pkg/policy/glob.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type glob string
|
||||||
|
|
||||||
|
// parseGlob ensures that the pattern conforms to the spec: only '*' and escaped '\*' are allowed.
|
||||||
|
func parseGlob(pattern string) (glob, error) {
|
||||||
|
for i := 0; i < len(pattern); i++ {
|
||||||
|
if pattern[i] == '*' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == '*' {
|
||||||
|
i++ // skip the escaped '*'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pattern[i] == '\\' && i+1 < len(pattern) {
|
||||||
|
i++ // skip the escaped character
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if pattern[i] == '\\' {
|
||||||
|
return "", fmt.Errorf("invalid escape sequence")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return glob(pattern), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParseGlob(pattern string) glob {
|
||||||
|
g, err := parseGlob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match matches a string against the glob pattern with * wildcards, handling escaped '\*' literals.
|
||||||
|
func (pattern glob) Match(str string) bool {
|
||||||
|
// i is the index for the pattern
|
||||||
|
// j is the index for the string
|
||||||
|
var i, j int
|
||||||
|
|
||||||
|
// starIdx keeps track of the position of the last * in the pattern.
|
||||||
|
// matchIdx keeps track of the position in the string where the last * matched.
|
||||||
|
var starIdx, matchIdx int = -1, -1
|
||||||
|
|
||||||
|
for j < len(str) {
|
||||||
|
if i < len(pattern) && (pattern[i] == str[j] || pattern[i] == '\\' && i+1 < len(pattern) && pattern[i+1] == str[j]) {
|
||||||
|
// characters match or if there's an escaped character that matches
|
||||||
|
if pattern[i] == '\\' {
|
||||||
|
// skip the escape character
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
j++
|
||||||
|
} else if i < len(pattern) && pattern[i] == '*' {
|
||||||
|
// there's a * wildcard in the pattern
|
||||||
|
starIdx = i
|
||||||
|
matchIdx = j
|
||||||
|
i++
|
||||||
|
} else if starIdx != -1 {
|
||||||
|
// there's a previous * wildcard, backtrack
|
||||||
|
i = starIdx + 1
|
||||||
|
matchIdx++
|
||||||
|
j = matchIdx
|
||||||
|
} else {
|
||||||
|
// no match found
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for remaining characters in the pattern
|
||||||
|
for i < len(pattern) && pattern[i] == '*' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// the entire pattern is processed, it's a match
|
||||||
|
return i == len(pattern)
|
||||||
|
}
|
||||||
73
pkg/policy/glob_test.go
Normal file
73
pkg/policy/glob_test.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSimpleGlobMatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
pattern string
|
||||||
|
str string
|
||||||
|
matches bool
|
||||||
|
}{
|
||||||
|
// Basic matching
|
||||||
|
{"*", "anything", true},
|
||||||
|
{"a*", "abc", true},
|
||||||
|
{"*c", "abc", true},
|
||||||
|
{"a*c", "abc", true},
|
||||||
|
{"a*c", "abxc", true},
|
||||||
|
{"a*c", "ac", true},
|
||||||
|
{"a*c", "a", false},
|
||||||
|
{"a*c", "ab", false},
|
||||||
|
|
||||||
|
// Escaped characters
|
||||||
|
{"a\\*c", "a*c", true},
|
||||||
|
{"a\\*c", "abc", false},
|
||||||
|
|
||||||
|
// Mixed wildcards and literals
|
||||||
|
{"a*b*c", "abc", true},
|
||||||
|
{"a*b*c", "aXbYc", true},
|
||||||
|
{"a*b*c", "aXbY", false},
|
||||||
|
{"a*b*c", "abYc", true},
|
||||||
|
{"a*b*c", "aXbc", true},
|
||||||
|
{"a*b*c", "aXbYcZ", false},
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
{"", "", true},
|
||||||
|
{"", "a", false},
|
||||||
|
{"*", "", true},
|
||||||
|
{"*", "a", true},
|
||||||
|
{"\\*", "*", true},
|
||||||
|
{"\\*", "a", false},
|
||||||
|
|
||||||
|
// Specified test cases
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol.", true},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Dan, Erin, Carol.", true},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice*, Bob , Carol.", true},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol.", true},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice*, Bob, Carol", false},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice*, Bob*, Carol!", false},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice, Bob, Carol.", false},
|
||||||
|
{"Alice\\*, Bob*, Carol.", "Alice Cooper, Bob, Carol.", false},
|
||||||
|
{"Alice\\*, Bob*, Carol.", " Alice*, Bob, Carol. ", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.pattern+"_"+tt.str, func(t *testing.T) {
|
||||||
|
g, err := parseGlob(tt.pattern)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.matches, g.Match(tt.str))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGlob(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
g := mustParseGlob("Alice\\*, Bob*, Carol.")
|
||||||
|
g.Match("Alice*, Bob*, Carol!")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,15 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||||
|
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
||||||
|
return nil, fmt.Errorf("policy contains integer values outside safe bounds: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return statementsFromIPLD("/", node)
|
return statementsFromIPLD("/", node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,14 +45,14 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
}
|
}
|
||||||
op := must.String(opNode)
|
op := must.String(opNode)
|
||||||
|
|
||||||
arg2AsSelector := func() (selector.Selector, error) {
|
arg2AsSelector := func(op string) (selector.Selector, error) {
|
||||||
nd, _ := node.LookupByIndex(1)
|
nd, _ := node.LookupByIndex(1)
|
||||||
if nd.Kind() != datamodel.Kind_String {
|
if nd.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(path + "1/")
|
return nil, ErrNotAString(combinePath(path, op, 1))
|
||||||
}
|
}
|
||||||
sel, err := selector.Parse(must.String(nd))
|
sel, err := selector.Parse(must.String(nd))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidSelector(path+"1/", err)
|
return nil, ErrInvalidSelector(combinePath(path, op, 1), err)
|
||||||
}
|
}
|
||||||
return sel, nil
|
return sel, nil
|
||||||
}
|
}
|
||||||
@@ -57,15 +62,15 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
switch op {
|
switch op {
|
||||||
case KindNot:
|
case KindNot:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
statement, err := statementFromIPLD(path+"1/", arg2)
|
statement, err := statementFromIPLD(combinePath(path, op, 1), arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return Not(statement), nil
|
return negation{statement: statement}, nil
|
||||||
|
|
||||||
case KindAnd, KindOr:
|
case KindAnd, KindOr:
|
||||||
arg2, _ := node.LookupByIndex(1)
|
arg2, _ := node.LookupByIndex(1)
|
||||||
statement, err := statementsFromIPLD(path+"1/", arg2)
|
statement, err := statementsFromIPLD(combinePath(path, op, 1), arg2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -77,7 +82,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
case 3:
|
case 3:
|
||||||
switch op {
|
switch op {
|
||||||
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
||||||
sel, err := arg2AsSelector()
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -85,28 +90,31 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
|||||||
return equality{kind: op, selector: sel, value: arg3}, nil
|
return equality{kind: op, selector: sel, value: arg3}, nil
|
||||||
|
|
||||||
case KindLike:
|
case KindLike:
|
||||||
sel, err := arg2AsSelector()
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pattern, _ := node.LookupByIndex(2)
|
pattern, _ := node.LookupByIndex(2)
|
||||||
if pattern.Kind() != datamodel.Kind_String {
|
if pattern.Kind() != datamodel.Kind_String {
|
||||||
return nil, ErrNotAString(path + "2/")
|
return nil, ErrNotAString(combinePath(path, op, 2))
|
||||||
}
|
}
|
||||||
res, err := Like(sel, must.String(pattern))
|
g, err := parseGlob(must.String(pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, ErrInvalidPattern(path+"2/", err)
|
return nil, ErrInvalidPattern(combinePath(path, op, 2), err)
|
||||||
}
|
}
|
||||||
return res, nil
|
return wildcard{selector: sel, pattern: g}, nil
|
||||||
|
|
||||||
case KindAll, KindAny:
|
case KindAll, KindAny:
|
||||||
sel, err := arg2AsSelector()
|
sel, err := arg2AsSelector(op)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
statementsNodes, _ := node.LookupByIndex(2)
|
statementsNode, _ := node.LookupByIndex(2)
|
||||||
statements, err := statementsFromIPLD(path+"1/", statementsNodes)
|
statement, err := statementFromIPLD(combinePath(path, op, 1), statementsNode)
|
||||||
return quantifier{kind: op, selector: sel, statements: statements}, nil
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return quantifier{kind: op, selector: sel, statement: statement}, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, ErrUnrecognizedOperator(path, op)
|
return nil, ErrUnrecognizedOperator(path, op)
|
||||||
@@ -123,7 +131,7 @@ func statementsFromIPLD(path string, node datamodel.Node) ([]Statement, error) {
|
|||||||
return nil, ErrNotATuple(path)
|
return nil, ErrNotATuple(path)
|
||||||
}
|
}
|
||||||
if node.Length() == 0 {
|
if node.Length() == 0 {
|
||||||
return nil, ErrEmptyList(path)
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
res := make([]Statement, node.Length())
|
res := make([]Statement, node.Length())
|
||||||
@@ -229,7 +237,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = listBuilder.AssembleValue().AssignString(statement.pattern)
|
err = listBuilder.AssembleValue().AssignString(string(statement.pattern))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -243,7 +251,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
args, err := statementsToIPLD(statement.statements)
|
args, err := statementToIPLD(statement.statement)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -260,3 +268,7 @@ func statementToIPLD(statement Statement) (datamodel.Node, error) {
|
|||||||
|
|
||||||
return list.Build(), nil
|
return list.Build(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func combinePath(prev string, operator string, index int) string {
|
||||||
|
return fmt.Sprintf("%s%d-%s/", prev, index, operator)
|
||||||
|
}
|
||||||
@@ -35,10 +35,6 @@ func ErrNotATuple(path string) error {
|
|||||||
return errWithPath{path: path, msg: "not a tuple"}
|
return errWithPath{path: path, msg: "not a tuple"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ErrEmptyList(path string) error {
|
|
||||||
return errWithPath{path: path, msg: "empty list"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func safeStr(str string) string {
|
func safeStr(str string) string {
|
||||||
if len(str) > 10 {
|
if len(str) > 10 {
|
||||||
return str[:10]
|
return str[:10]
|
||||||
@@ -11,16 +11,14 @@ import (
|
|||||||
func TestIpldRoundTrip(t *testing.T) {
|
func TestIpldRoundTrip(t *testing.T) {
|
||||||
const illustrativeExample = `
|
const illustrativeExample = `
|
||||||
[
|
[
|
||||||
["==", ".status", "draft"],
|
["==", ".status", "draft"],
|
||||||
["all", ".reviewer", [
|
["all", ".reviewer", ["like", ".email", "*@example.com"]],
|
||||||
["like", ".email", "*@example.com"]]
|
["any", ".tags",
|
||||||
],
|
["or", [
|
||||||
["any", ".tags", [
|
["==", ".", "news"],
|
||||||
["or", [
|
["==", ".", "press"]
|
||||||
["==", ".", "news"],
|
]]
|
||||||
["==", ".", "press"]]
|
]
|
||||||
]]
|
|
||||||
]
|
|
||||||
]`
|
]`
|
||||||
|
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
49
pkg/policy/limits/int.go
Normal file
49
pkg/policy/limits/int.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package limits
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MaxInt53 represents the maximum safe integer in JavaScript (2^53 - 1)
|
||||||
|
MaxInt53 = 9007199254740991
|
||||||
|
// MinInt53 represents the minimum safe integer in JavaScript (-2^53 + 1)
|
||||||
|
MinInt53 = -9007199254740991
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateIntegerBoundsIPLD(node ipld.Node) error {
|
||||||
|
switch node.Kind() {
|
||||||
|
case ipld.Kind_Int:
|
||||||
|
val := must.Int(node)
|
||||||
|
if val > MaxInt53 || val < MinInt53 {
|
||||||
|
return fmt.Errorf("integer value %d exceeds safe bounds", val)
|
||||||
|
}
|
||||||
|
case ipld.Kind_List:
|
||||||
|
it := node.ListIterator()
|
||||||
|
for !it.Done() {
|
||||||
|
_, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ipld.Kind_Map:
|
||||||
|
it := node.MapIterator()
|
||||||
|
for !it.Done() {
|
||||||
|
_, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
82
pkg/policy/limits/int_test.go
Normal file
82
pkg/policy/limits/int_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package limits
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateIntegerBoundsIPLD(t *testing.T) {
|
||||||
|
buildMap := func() datamodel.Node {
|
||||||
|
nb := basicnode.Prototype.Any.NewBuilder()
|
||||||
|
qp.Map(1, func(ma datamodel.MapAssembler) {
|
||||||
|
qp.MapEntry(ma, "foo", qp.Int(MaxInt53+1))
|
||||||
|
})(nb)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildList := func() datamodel.Node {
|
||||||
|
nb := basicnode.Prototype.Any.NewBuilder()
|
||||||
|
qp.List(1, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.Int(MinInt53-1))
|
||||||
|
})(nb)
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input datamodel.Node
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid int",
|
||||||
|
input: basicnode.NewInt(42),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max safe int",
|
||||||
|
input: basicnode.NewInt(MaxInt53),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min safe int",
|
||||||
|
input: basicnode.NewInt(MinInt53),
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "above MaxInt53",
|
||||||
|
input: basicnode.NewInt(MaxInt53 + 1),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "below MinInt53",
|
||||||
|
input: basicnode.NewInt(MinInt53 - 1),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested map with invalid int",
|
||||||
|
input: buildMap(),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested list with invalid int",
|
||||||
|
input: buildList(),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := ValidateIntegerBoundsIPLD(tt.input)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "exceeds safe bounds")
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
205
pkg/policy/literal/literal.go
Normal file
205
pkg/policy/literal/literal.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// Package literal holds a collection of functions to create IPLD types to use in policies, selector and args.
|
||||||
|
package literal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"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"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Bool = basicnode.NewBool
|
||||||
|
var Int = basicnode.NewInt
|
||||||
|
var Float = basicnode.NewFloat
|
||||||
|
var String = basicnode.NewString
|
||||||
|
var Bytes = basicnode.NewBytes
|
||||||
|
var Link = basicnode.NewLink
|
||||||
|
|
||||||
|
func LinkCid(cid cid.Cid) ipld.Node {
|
||||||
|
return Link(cidlink.Link{Cid: cid})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Null() ipld.Node {
|
||||||
|
nb := basicnode.Prototype.Any.NewBuilder()
|
||||||
|
nb.AssignNull()
|
||||||
|
return nb.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map creates an IPLD node from a map[string]any
|
||||||
|
func Map[T any](m map[string]T) (ipld.Node, error) {
|
||||||
|
return qp.BuildMap(basicnode.Prototype.Any, int64(len(m)), func(ma datamodel.MapAssembler) {
|
||||||
|
// deterministic iteration
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for key := range m {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, key := range keys {
|
||||||
|
qp.MapEntry(ma, key, anyAssemble(m[key]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// List creates an IPLD node from a []any
|
||||||
|
func List[T any](l []T) (ipld.Node, error) {
|
||||||
|
return qp.BuildList(basicnode.Prototype.Any, int64(len(l)), func(la datamodel.ListAssembler) {
|
||||||
|
for _, val := range l {
|
||||||
|
qp.ListEntry(la, anyAssemble(val))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any creates an IPLD node from any value
|
||||||
|
// If possible, use another dedicated function for your type for performance.
|
||||||
|
func Any(v any) (res ipld.Node, err error) {
|
||||||
|
// some fast path
|
||||||
|
switch val := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return basicnode.NewBool(val), nil
|
||||||
|
case string:
|
||||||
|
return basicnode.NewString(val), nil
|
||||||
|
case int:
|
||||||
|
i := int64(val)
|
||||||
|
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||||
|
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", i)
|
||||||
|
}
|
||||||
|
return basicnode.NewInt(i), nil
|
||||||
|
case int8:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int16:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int32:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int64:
|
||||||
|
if val > limits.MaxInt53 || val < limits.MinInt53 {
|
||||||
|
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", val)
|
||||||
|
}
|
||||||
|
return basicnode.NewInt(val), nil
|
||||||
|
case uint:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint8:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint16:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint32:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint64:
|
||||||
|
if val > uint64(limits.MaxInt53) {
|
||||||
|
return nil, fmt.Errorf("unsigned integer value %d exceeds safe integer bounds", val)
|
||||||
|
}
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case float32:
|
||||||
|
return basicnode.NewFloat(float64(val)), nil
|
||||||
|
case float64:
|
||||||
|
return basicnode.NewFloat(val), nil
|
||||||
|
case []byte:
|
||||||
|
return basicnode.NewBytes(val), nil
|
||||||
|
case datamodel.Node:
|
||||||
|
return val, nil
|
||||||
|
case cid.Cid:
|
||||||
|
return LinkCid(val), nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := basicnode.Prototype__Any{}.NewBuilder()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
res = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
anyAssemble(v)(builder)
|
||||||
|
|
||||||
|
return builder.Build(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyAssemble(val any) qp.Assemble {
|
||||||
|
var rt reflect.Type
|
||||||
|
var rv reflect.Value
|
||||||
|
|
||||||
|
// support for recursive calls, staying in reflection land
|
||||||
|
if cast, ok := val.(reflect.Value); ok {
|
||||||
|
rt = cast.Type()
|
||||||
|
rv = cast
|
||||||
|
} else {
|
||||||
|
rt = reflect.TypeOf(val)
|
||||||
|
rv = reflect.ValueOf(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to dereference in some cases, to get the real value type
|
||||||
|
if rt.Kind() == reflect.Ptr || rt.Kind() == reflect.Interface {
|
||||||
|
rv = rv.Elem()
|
||||||
|
rt = rv.Type()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rt.Kind() {
|
||||||
|
case reflect.Array:
|
||||||
|
if rt.Elem().Kind() == reflect.Uint8 {
|
||||||
|
panic("bytes array are not supported yet")
|
||||||
|
}
|
||||||
|
return qp.List(int64(rv.Len()), func(la datamodel.ListAssembler) {
|
||||||
|
for i := range rv.Len() {
|
||||||
|
qp.ListEntry(la, anyAssemble(rv.Index(i)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case reflect.Slice:
|
||||||
|
if rt.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return qp.Bytes(val.([]byte))
|
||||||
|
}
|
||||||
|
return qp.List(int64(rv.Len()), func(la datamodel.ListAssembler) {
|
||||||
|
for i := range rv.Len() {
|
||||||
|
qp.ListEntry(la, anyAssemble(rv.Index(i)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case reflect.Map:
|
||||||
|
if rt.Key().Kind() != reflect.String {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// deterministic iteration
|
||||||
|
keys := rv.MapKeys()
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
return keys[i].String() < keys[j].String()
|
||||||
|
})
|
||||||
|
return qp.Map(int64(rv.Len()), func(ma datamodel.MapAssembler) {
|
||||||
|
for _, key := range keys {
|
||||||
|
qp.MapEntry(ma, key.String(), anyAssemble(rv.MapIndex(key)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case reflect.Bool:
|
||||||
|
return qp.Bool(rv.Bool())
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
i := rv.Int()
|
||||||
|
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||||
|
panic(fmt.Sprintf("integer %d exceeds safe bounds", i))
|
||||||
|
}
|
||||||
|
return qp.Int(i)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
u := rv.Uint()
|
||||||
|
if u > limits.MaxInt53 {
|
||||||
|
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
|
||||||
|
}
|
||||||
|
return qp.Int(int64(u))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return qp.Float(rv.Float())
|
||||||
|
case reflect.String:
|
||||||
|
return qp.String(rv.String())
|
||||||
|
case reflect.Struct:
|
||||||
|
if rt == reflect.TypeOf(cid.Cid{}) {
|
||||||
|
c := rv.Interface().(cid.Cid)
|
||||||
|
return qp.Link(cidlink.Link{Cid: c})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(fmt.Sprintf("unsupported type %T", val))
|
||||||
|
}
|
||||||
314
pkg/policy/literal/literal_test.go
Normal file
314
pkg/policy/literal/literal_test.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package literal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
n, err := List([]int{1, 2, 3})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, n.Kind())
|
||||||
|
require.Equal(t, int64(3), n.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
2: int{3}
|
||||||
|
}`, printer.Sprint(n))
|
||||||
|
|
||||||
|
n, err = List([][]int{{1, 2, 3}, {4, 5, 6}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, n.Kind())
|
||||||
|
require.Equal(t, int64(2), n.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
2: int{3}
|
||||||
|
}
|
||||||
|
1: list{
|
||||||
|
0: int{4}
|
||||||
|
1: int{5}
|
||||||
|
2: int{6}
|
||||||
|
}
|
||||||
|
}`, printer.Sprint(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMap(t *testing.T) {
|
||||||
|
n, err := Map(map[string]any{
|
||||||
|
"bool": true,
|
||||||
|
"string": "foobar",
|
||||||
|
"bytes": []byte{1, 2, 3, 4},
|
||||||
|
"int": 1234,
|
||||||
|
"uint": uint(12345),
|
||||||
|
"float": 1.45,
|
||||||
|
"slice": []int{1, 2, 3},
|
||||||
|
"array": [2]int{1, 2},
|
||||||
|
"map": map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"foofoo": map[string]string{
|
||||||
|
"barbar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
v, err := n.LookupByString("bool")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
||||||
|
require.Equal(t, true, must(v.AsBool()))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("string")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_String, v.Kind())
|
||||||
|
require.Equal(t, "foobar", must(v.AsString()))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("bytes")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
||||||
|
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("int")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||||
|
require.Equal(t, int64(1234), must(v.AsInt()))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("uint")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||||
|
require.Equal(t, int64(12345), must(v.AsInt()))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("float")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
||||||
|
require.Equal(t, 1.45, must(v.AsFloat()))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("slice")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||||
|
require.Equal(t, int64(3), v.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
2: int{3}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("array")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||||
|
require.Equal(t, int64(2), v.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("map")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
||||||
|
require.Equal(t, int64(2), v.Length())
|
||||||
|
require.Equal(t, `map{
|
||||||
|
string{"foo"}: string{"bar"}
|
||||||
|
string{"foofoo"}: map{
|
||||||
|
string{"barbar"}: string{"foo"}
|
||||||
|
}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("link")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
||||||
|
asLink, err := v.AsLink()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAny(t *testing.T) {
|
||||||
|
data := map[string]any{
|
||||||
|
"bool": true,
|
||||||
|
"string": "foobar",
|
||||||
|
"bytes": []byte{1, 2, 3, 4},
|
||||||
|
"int": 1234,
|
||||||
|
"uint": uint(12345),
|
||||||
|
"float": 1.45,
|
||||||
|
"slice": []int{1, 2, 3},
|
||||||
|
"array": [2]int{1, 2},
|
||||||
|
"map": map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"foofoo": map[string]string{
|
||||||
|
"barbar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
||||||
|
"func": func() {},
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := Any(data["bool"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
||||||
|
require.Equal(t, true, must(v.AsBool()))
|
||||||
|
|
||||||
|
v, err = Any(data["string"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_String, v.Kind())
|
||||||
|
require.Equal(t, "foobar", must(v.AsString()))
|
||||||
|
|
||||||
|
v, err = Any(data["bytes"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
||||||
|
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
||||||
|
|
||||||
|
v, err = Any(data["int"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||||
|
require.Equal(t, int64(1234), must(v.AsInt()))
|
||||||
|
|
||||||
|
v, err = Any(data["uint"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||||
|
require.Equal(t, int64(12345), must(v.AsInt()))
|
||||||
|
|
||||||
|
v, err = Any(data["float"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
||||||
|
require.Equal(t, 1.45, must(v.AsFloat()))
|
||||||
|
|
||||||
|
v, err = Any(data["slice"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||||
|
require.Equal(t, int64(3), v.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
2: int{3}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = Any(data["array"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||||
|
require.Equal(t, int64(2), v.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = Any(data["map"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
||||||
|
require.Equal(t, int64(2), v.Length())
|
||||||
|
require.Equal(t, `map{
|
||||||
|
string{"foo"}: string{"bar"}
|
||||||
|
string{"foofoo"}: map{
|
||||||
|
string{"barbar"}: string{"foo"}
|
||||||
|
}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = Any(data["link"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
||||||
|
asLink, err := v.AsLink()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||||
|
|
||||||
|
_, err = Any(data["func"])
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAny(b *testing.B) {
|
||||||
|
b.Run("bool", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("string", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any("foobar")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("bytes", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any([]byte{1, 2, 3, 4})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("map", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any(map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"foofoo": map[string]string{
|
||||||
|
"barbar": "foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnyAssembleIntegerOverflow(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input interface{}
|
||||||
|
shouldErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid int",
|
||||||
|
input: 42,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max safe int",
|
||||||
|
input: limits.MaxInt53,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "min safe int",
|
||||||
|
input: limits.MinInt53,
|
||||||
|
shouldErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overflow int",
|
||||||
|
input: int64(limits.MaxInt53 + 1),
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "underflow int",
|
||||||
|
input: int64(limits.MinInt53 - 1),
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overflow uint",
|
||||||
|
input: uint64(limits.MaxInt53 + 1),
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := Any(tt.input)
|
||||||
|
if tt.shouldErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func must[T any](t T, err error) T {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
295
pkg/policy/match.go
Normal file
295
pkg/policy/match.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match determines if the IPLD node satisfies the policy.
|
||||||
|
// The first Statement failing to match is returned as well.
|
||||||
|
func (p Policy) Match(node datamodel.Node) (bool, Statement) {
|
||||||
|
for _, stmt := range p {
|
||||||
|
res, leaf := matchStatement(stmt, node)
|
||||||
|
switch res {
|
||||||
|
case matchResultNoData, matchResultFalse:
|
||||||
|
return false, leaf
|
||||||
|
case matchResultOptionalNoData, matchResultTrue:
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
|
||||||
|
// If the data is missing or the non-optional Statement is matching, true is returned.
|
||||||
|
//
|
||||||
|
// This allows performing the policy checking in multiple steps, and find immediately if a Statement already failed.
|
||||||
|
// A final call to Match is necessary to make sure that the policy is fully matched, with no missing data
|
||||||
|
// (apart from optional values).
|
||||||
|
//
|
||||||
|
// The first Statement failing to match is returned as well.
|
||||||
|
func (p Policy) PartialMatch(node datamodel.Node) (bool, Statement) {
|
||||||
|
for _, stmt := range p {
|
||||||
|
res, leaf := matchStatement(stmt, node)
|
||||||
|
switch res {
|
||||||
|
case matchResultFalse:
|
||||||
|
return false, leaf
|
||||||
|
case matchResultNoData, matchResultOptionalNoData, matchResultTrue:
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type matchResult int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
matchResultTrue matchResult = iota // statement has data and resolve to true
|
||||||
|
matchResultFalse // statement has data and resolve to false
|
||||||
|
matchResultNoData // statement has no data
|
||||||
|
matchResultOptionalNoData // statement has no data and is optional
|
||||||
|
)
|
||||||
|
|
||||||
|
// matchStatement evaluate the policy against the given ipld.Node and returns:
|
||||||
|
// - matchResultTrue: if the selector matched and the statement evaluated to true.
|
||||||
|
// - matchResultFalse: if the selector matched and the statement evaluated to false.
|
||||||
|
// - matchResultNoData: if the selector didn't match the expected data.
|
||||||
|
// For matchResultTrue and matchResultNoData, the leaf-most (innermost) statement failing to be true is returned,
|
||||||
|
// as well as the corresponding root-most encompassing statement.
|
||||||
|
func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Statement) {
|
||||||
|
var boolToRes = func(v bool) (matchResult, Statement) {
|
||||||
|
if v {
|
||||||
|
return matchResultTrue, nil
|
||||||
|
} else {
|
||||||
|
return matchResultFalse, cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cur.Kind() {
|
||||||
|
case KindEqual:
|
||||||
|
if s, ok := cur.(equality); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil { // optional selector didn't match
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
return boolToRes(datamodel.DeepEqual(s.value, res))
|
||||||
|
}
|
||||||
|
case KindGreaterThan:
|
||||||
|
if s, ok := cur.(equality); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil { // optional selector didn't match
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
return boolToRes(isOrdered(s.value, res, gt))
|
||||||
|
}
|
||||||
|
case KindGreaterThanOrEqual:
|
||||||
|
if s, ok := cur.(equality); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil { // optional selector didn't match
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
return boolToRes(isOrdered(s.value, res, gte))
|
||||||
|
}
|
||||||
|
case KindLessThan:
|
||||||
|
if s, ok := cur.(equality); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil { // optional selector didn't match
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
return boolToRes(isOrdered(s.value, res, lt))
|
||||||
|
}
|
||||||
|
case KindLessThanOrEqual:
|
||||||
|
if s, ok := cur.(equality); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil { // optional selector didn't match
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
return boolToRes(isOrdered(s.value, res, lte))
|
||||||
|
}
|
||||||
|
case KindNot:
|
||||||
|
if s, ok := cur.(negation); ok {
|
||||||
|
res, leaf := matchStatement(s.statement, node)
|
||||||
|
switch res {
|
||||||
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
|
return res, leaf
|
||||||
|
case matchResultTrue:
|
||||||
|
return matchResultFalse, cur
|
||||||
|
case matchResultFalse:
|
||||||
|
return matchResultTrue, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case KindAnd:
|
||||||
|
if s, ok := cur.(connective); ok {
|
||||||
|
for _, cs := range s.statements {
|
||||||
|
res, leaf := matchStatement(cs, node)
|
||||||
|
switch res {
|
||||||
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
|
return res, leaf
|
||||||
|
case matchResultTrue:
|
||||||
|
// continue
|
||||||
|
case matchResultFalse:
|
||||||
|
return matchResultFalse, leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchResultTrue, nil
|
||||||
|
}
|
||||||
|
case KindOr:
|
||||||
|
if s, ok := cur.(connective); ok {
|
||||||
|
if len(s.statements) == 0 {
|
||||||
|
return matchResultTrue, nil
|
||||||
|
}
|
||||||
|
for _, cs := range s.statements {
|
||||||
|
res, leaf := matchStatement(cs, node)
|
||||||
|
switch res {
|
||||||
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
|
return res, leaf
|
||||||
|
case matchResultTrue:
|
||||||
|
return matchResultTrue, leaf
|
||||||
|
case matchResultFalse:
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchResultFalse, cur
|
||||||
|
}
|
||||||
|
case KindLike:
|
||||||
|
if s, ok := cur.(wildcard); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil { // optional selector didn't match
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
v, err := res.AsString()
|
||||||
|
if err != nil {
|
||||||
|
return matchResultFalse, cur // not a string
|
||||||
|
}
|
||||||
|
return boolToRes(s.pattern.Match(v))
|
||||||
|
}
|
||||||
|
case KindAll:
|
||||||
|
if s, ok := cur.(quantifier); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
it := res.ListIterator()
|
||||||
|
if it == nil {
|
||||||
|
return matchResultFalse, cur // not a list
|
||||||
|
}
|
||||||
|
for !it.Done() {
|
||||||
|
_, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
panic("should never happen")
|
||||||
|
}
|
||||||
|
matchRes, leaf := matchStatement(s.statement, v)
|
||||||
|
switch matchRes {
|
||||||
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
|
return matchRes, leaf
|
||||||
|
case matchResultTrue:
|
||||||
|
// continue
|
||||||
|
case matchResultFalse:
|
||||||
|
return matchResultFalse, leaf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchResultTrue, nil
|
||||||
|
}
|
||||||
|
case KindAny:
|
||||||
|
if s, ok := cur.(quantifier); ok {
|
||||||
|
res, err := s.selector.Select(node)
|
||||||
|
if err != nil {
|
||||||
|
return matchResultNoData, cur
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return matchResultOptionalNoData, nil
|
||||||
|
}
|
||||||
|
it := res.ListIterator()
|
||||||
|
if it == nil {
|
||||||
|
return matchResultFalse, cur // not a list
|
||||||
|
}
|
||||||
|
for !it.Done() {
|
||||||
|
_, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
panic("should never happen")
|
||||||
|
}
|
||||||
|
matchRes, leaf := matchStatement(s.statement, v)
|
||||||
|
switch matchRes {
|
||||||
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
|
return matchRes, leaf
|
||||||
|
case matchResultTrue:
|
||||||
|
return matchResultTrue, nil
|
||||||
|
case matchResultFalse:
|
||||||
|
// continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchResultFalse, cur
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// isOrdered compares two IPLD nodes and returns true if they satisfy the given ordering function.
|
||||||
|
// It supports comparison of integers and floats, returning false for:
|
||||||
|
// - Nodes of different or unsupported kinds
|
||||||
|
// - Integer values outside JavaScript's safe integer bounds (±2^53-1)
|
||||||
|
// - Non-finite floating point values (NaN or ±Inf)
|
||||||
|
//
|
||||||
|
// The satisfies parameter is a function that interprets the comparison result:
|
||||||
|
// - For ">" it returns true when order is 1
|
||||||
|
// - For ">=" it returns true when order is 0 or 1
|
||||||
|
// - For "<" it returns true when order is -1
|
||||||
|
// - For "<=" it returns true when order is -1 or 0
|
||||||
|
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||||
|
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||||
|
a := must.Int(actual)
|
||||||
|
b := must.Int(expected)
|
||||||
|
|
||||||
|
return satisfies(cmp.Compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected.Kind() == ipld.Kind_Float && actual.Kind() == ipld.Kind_Float {
|
||||||
|
a, err := actual.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("extracting node float: %w", err))
|
||||||
|
}
|
||||||
|
b, err := expected.AsFloat()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("extracting selector float: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if math.IsInf(a, 0) || math.IsNaN(a) || math.IsInf(b, 0) || math.IsNaN(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return satisfies(cmp.Compare(a, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func gt(order int) bool { return order == 1 }
|
||||||
|
func gte(order int) bool { return order == 0 || order == 1 }
|
||||||
|
func lt(order int) bool { return order == -1 }
|
||||||
|
func lte(order int) bool { return order == 0 || order == -1 }
|
||||||
903
pkg/policy/match_test.go
Normal file
903
pkg/policy/match_test.go
Normal file
@@ -0,0 +1,903 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
t.Run("equality", func(t *testing.T) {
|
||||||
|
t.Run("string", func(t *testing.T) {
|
||||||
|
nd := literal.String("test")
|
||||||
|
|
||||||
|
pol := MustConstruct(Equal(".", literal.String("test")))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.String("test2")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.Int(138)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(Equal(".", literal.Int(138)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("float", func(t *testing.T) {
|
||||||
|
nd := literal.Float(1.138)
|
||||||
|
|
||||||
|
pol := MustConstruct(Equal(".", literal.Float(1.138)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("IPLD Link", func(t *testing.T) {
|
||||||
|
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
||||||
|
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
||||||
|
|
||||||
|
nd := literal.Link(l0)
|
||||||
|
|
||||||
|
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string in map", func(t *testing.T) {
|
||||||
|
nd, _ := literal.Map(map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
})
|
||||||
|
|
||||||
|
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string in list", func(t *testing.T) {
|
||||||
|
nd, _ := literal.List([]any{"foo"})
|
||||||
|
|
||||||
|
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("inequality", func(t *testing.T) {
|
||||||
|
t.Run("gt int", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThan(".", literal.Int(138)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThan(".", literal.Int(140)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gte int", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(140)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gt float", func(t *testing.T) {
|
||||||
|
nd := literal.Float(1.38)
|
||||||
|
|
||||||
|
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThan(".", literal.Float(2)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gte float", func(t *testing.T) {
|
||||||
|
nd := literal.Float(1.38)
|
||||||
|
|
||||||
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(2)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("lt int", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(LessThan(".", literal.Int(138)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(LessThan(".", literal.Int(100)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("lte int", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(LessThanOrEqual(".", literal.Int(100)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negation", func(t *testing.T) {
|
||||||
|
nd := literal.Bool(false)
|
||||||
|
|
||||||
|
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("conjunction", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(
|
||||||
|
And(
|
||||||
|
GreaterThan(".", literal.Int(1)),
|
||||||
|
LessThan(".", literal.Int(1138)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(
|
||||||
|
And(
|
||||||
|
GreaterThan(".", literal.Int(1)),
|
||||||
|
Equal(".", literal.Int(1138)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, MustConstruct(Equal(".", literal.Int(1138)))[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(And())
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("disjunction", func(t *testing.T) {
|
||||||
|
nd := literal.Int(138)
|
||||||
|
|
||||||
|
pol := MustConstruct(
|
||||||
|
Or(
|
||||||
|
GreaterThan(".", literal.Int(138)),
|
||||||
|
LessThan(".", literal.Int(1138)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(
|
||||||
|
Or(
|
||||||
|
GreaterThan(".", literal.Int(138)),
|
||||||
|
Equal(".", literal.Int(1138)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Or())
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wildcard", func(t *testing.T) {
|
||||||
|
pattern := `Alice\*, Bob*, Carol.`
|
||||||
|
|
||||||
|
for _, s := range []string{
|
||||||
|
"Alice*, Bob, Carol.",
|
||||||
|
"Alice*, Bob, Dan, Erin, Carol.",
|
||||||
|
"Alice*, Bob , Carol.",
|
||||||
|
"Alice*, Bob*, Carol.",
|
||||||
|
} {
|
||||||
|
func(s string) {
|
||||||
|
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
||||||
|
nd := literal.String(s)
|
||||||
|
|
||||||
|
pol := MustConstruct(Like(".", pattern))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
})
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range []string{
|
||||||
|
"Alice*, Bob, Carol",
|
||||||
|
"Alice*, Bob*, Carol!",
|
||||||
|
"Alice Cooper, Bob, Carol.",
|
||||||
|
"Alice, Bob, Carol.",
|
||||||
|
" Alice*, Bob, Carol. ",
|
||||||
|
} {
|
||||||
|
func(s string) {
|
||||||
|
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
||||||
|
nd := literal.String(s)
|
||||||
|
|
||||||
|
pol := MustConstruct(Like(".", pattern))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
}(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("quantification", func(t *testing.T) {
|
||||||
|
t.Run("all", func(t *testing.T) {
|
||||||
|
nd, _ := literal.List([]any{
|
||||||
|
map[string]int{"value": 5},
|
||||||
|
map[string]int{"value": 10},
|
||||||
|
map[string]int{"value": 20},
|
||||||
|
map[string]int{"value": 50},
|
||||||
|
map[string]int{"value": 100},
|
||||||
|
})
|
||||||
|
|
||||||
|
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, MustConstruct(GreaterThan(".value", literal.Int(20)))[0], leaf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("any", func(t *testing.T) {
|
||||||
|
nd, _ := literal.List([]any{
|
||||||
|
map[string]int{"value": 5},
|
||||||
|
map[string]int{"value": 10},
|
||||||
|
map[string]int{"value": 20},
|
||||||
|
map[string]int{"value": 50},
|
||||||
|
map[string]int{"value": 100},
|
||||||
|
})
|
||||||
|
|
||||||
|
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
||||||
|
ok, leaf := pol.Match(nd)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicyExamples(t *testing.T) {
|
||||||
|
makeNode := func(data string) ipld.Node {
|
||||||
|
nd, err := ipld.Decode([]byte(data), dagjson.Decode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return nd
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluate := func(statement string, data ipld.Node) bool {
|
||||||
|
// we need to wrap statement with [] to make them a policy
|
||||||
|
policy := fmt.Sprintf("[%s]", statement)
|
||||||
|
|
||||||
|
pol, err := FromDagJson(policy)
|
||||||
|
require.NoError(t, err)
|
||||||
|
res, _ := pol.Match(data)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("And", func(t *testing.T) {
|
||||||
|
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`["and", []]`, data))
|
||||||
|
require.True(t, evaluate(`
|
||||||
|
["and", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
[">=", ".age", 21]
|
||||||
|
]]`, data))
|
||||||
|
require.False(t, evaluate(`
|
||||||
|
["and", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
[">=", ".age", 21],
|
||||||
|
["==", ".nationalities", ["American"]]
|
||||||
|
]]`, data))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Or", func(t *testing.T) {
|
||||||
|
data := makeNode(`{ "name": "Katie", "age": 35, "nationalities": ["Canadian", "South African"] }`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`["or", []]`, data))
|
||||||
|
require.True(t, evaluate(`
|
||||||
|
["or", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
[">", ".age", 45]
|
||||||
|
]]
|
||||||
|
`, data))
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Not", func(t *testing.T) {
|
||||||
|
data := makeNode(`{ "name": "Katie", "nationalities": ["Canadian", "South African"] }`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`
|
||||||
|
["not",
|
||||||
|
["and", [
|
||||||
|
["==", ".name", "Katie"],
|
||||||
|
["==", ".nationalities", ["American"]]
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
`, data))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("All", func(t *testing.T) {
|
||||||
|
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
||||||
|
|
||||||
|
require.False(t, evaluate(`["all", ".a", [">", ".b", 0]]`, data))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Any", func(t *testing.T) {
|
||||||
|
data := makeNode(`{"a": [{"b": 1}, {"b": 2}, {"z": [7, 8, 9]}]}`)
|
||||||
|
|
||||||
|
require.True(t, evaluate(`["any", ".a", ["==", ".b", 2]]`, data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzMatch(f *testing.F) {
|
||||||
|
// Policy + Data examples
|
||||||
|
f.Add([]byte(`[["==", ".status", "draft"]]`), []byte(`{"status": "draft"}`))
|
||||||
|
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
|
||||||
|
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
|
||||||
|
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
|
||||||
|
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
|
||||||
|
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
|
||||||
|
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))
|
||||||
|
f.Add([]byte(`[["and", [["==", ".role", "admin"], [">=", ".experience", 5]]]]`), []byte(`{"role": "admin", "experience": 6}`))
|
||||||
|
f.Add([]byte(`[["or", [["==", ".department", "HR"], ["==", ".department", "Finance"]]]]`), []byte(`{"department": "HR"}`))
|
||||||
|
f.Add([]byte(`[["like", ".email", "*@company.com"]]`), []byte(`{"email": "user@company.com"}`))
|
||||||
|
f.Add([]byte(`[["all", ".projects", [">", ".budget", 10000]]]`), []byte(`{"projects": [{"budget": 15000}, {"budget": 8000}]}`))
|
||||||
|
f.Add([]byte(`[["any", ".skills", ["==", ".", "Go"]]]`), []byte(`{"skills": ["Go", "Python", "JavaScript"]}`))
|
||||||
|
f.Add(
|
||||||
|
[]byte(`[["and", [
|
||||||
|
["==", ".name", "Bob"],
|
||||||
|
["or", [[">", ".age", 25],["==", ".status", "active"]]],
|
||||||
|
["all", ".tasks", ["==", ".completed", true]]
|
||||||
|
]]]`),
|
||||||
|
[]byte(`{
|
||||||
|
"name": "Bob",
|
||||||
|
"age": 26,
|
||||||
|
"status": "active",
|
||||||
|
"tasks": [{"completed": true}, {"completed": true}, {"completed": false}]
|
||||||
|
}`),
|
||||||
|
)
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, policyBytes []byte, dataBytes []byte) {
|
||||||
|
policyNode, err := ipld.Decode(policyBytes, dagjson.Decode)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
dataNode, err := ipld.Decode(dataBytes, dagjson.Decode)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
// policy node -> policy object
|
||||||
|
policy, err := FromIPLD(policyNode)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = policy.Match(dataNode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionalSelectors(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
policy Policy
|
||||||
|
data map[string]any
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing optional field returns true",
|
||||||
|
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
||||||
|
data: map[string]any{},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "present optional field with matching value returns true",
|
||||||
|
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
||||||
|
data: map[string]any{"field": "value"},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "present optional field with non-matching value returns false",
|
||||||
|
policy: MustConstruct(Equal(".field?", literal.String("value"))),
|
||||||
|
data: map[string]any{"field": "other"},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing non-optional field returns false",
|
||||||
|
policy: MustConstruct(Equal(".field", literal.String("value"))),
|
||||||
|
data: map[string]any{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested missing non-optional field returns false",
|
||||||
|
policy: MustConstruct(Equal(".outer?.inner", literal.String("value"))),
|
||||||
|
data: map[string]any{"outer": map[string]any{}},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "completely missing nested optional path returns true",
|
||||||
|
policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))),
|
||||||
|
data: map[string]any{},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "partially present nested optional path with missing end returns true",
|
||||||
|
policy: MustConstruct(Equal(".outer?.inner?", literal.String("value"))),
|
||||||
|
data: map[string]any{"outer": map[string]any{}},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "optional array index returns true when array is empty",
|
||||||
|
policy: MustConstruct(Equal(".array[0]?", literal.String("value"))),
|
||||||
|
data: map[string]any{"array": []any{}},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-optional array index returns false when array is empty",
|
||||||
|
policy: MustConstruct(Equal(".array[0]", literal.String("value"))),
|
||||||
|
data: map[string]any{"array": []any{}},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
nb := basicnode.Prototype.Map.NewBuilder()
|
||||||
|
n, err := literal.Map(tt.data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = nb.AssignNode(n)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result, _ := tt.policy.Match(nb.Build())
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The unique behaviour of PartialMatch is that it should return true for missing non-optional data (unlike Match).
|
||||||
|
func TestPartialMatch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
policy Policy
|
||||||
|
data map[string]any
|
||||||
|
expectedMatch bool
|
||||||
|
expectedStmt Statement
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "returns true for missing non-optional field",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Equal(".field", literal.String("value")),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true when present data matches",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Equal(".foo", literal.String("correct")),
|
||||||
|
Equal(".missing", literal.String("whatever")),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"foo": "correct",
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns false with failing statement for present but non-matching value",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Equal(".foo", literal.String("value1")),
|
||||||
|
Equal(".bar", literal.String("value2")),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"foo": "wrong",
|
||||||
|
"bar": "value2",
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedStmt: MustConstruct(
|
||||||
|
Equal(".foo", literal.String("value1")),
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "continues past missing data until finding actual mismatch",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Equal(".missing", literal.String("value")),
|
||||||
|
Equal(".present", literal.String("wrong")),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"present": "actual",
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedStmt: MustConstruct(
|
||||||
|
Equal(".present", literal.String("wrong")),
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optional fields
|
||||||
|
{
|
||||||
|
name: "returns false when optional field present but wrong",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Equal(".field?", literal.String("value")),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"field": "wrong",
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedStmt: MustConstruct(
|
||||||
|
Equal(".field?", literal.String("value")),
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Like pattern matching
|
||||||
|
{
|
||||||
|
name: "returns true for matching like pattern",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Like(".pattern", "test*"),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"pattern": "testing123",
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns false for non-matching like pattern",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Like(".pattern", "test*"),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"pattern": "wrong123",
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedStmt: MustConstruct(
|
||||||
|
Like(".pattern", "test*"),
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Array quantifiers
|
||||||
|
{
|
||||||
|
name: "all matches when every element satisfies condition",
|
||||||
|
policy: MustConstruct(
|
||||||
|
All(".numbers", Equal(".", literal.Int(1))),
|
||||||
|
),
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"numbers": []interface{}{1, 1, 1},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all fails when any element doesn't satisfy",
|
||||||
|
policy: MustConstruct(
|
||||||
|
All(".numbers", Equal(".", literal.Int(1))),
|
||||||
|
),
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"numbers": []interface{}{1, 2, 1},
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedStmt: MustConstruct(
|
||||||
|
Equal(".", literal.Int(1)),
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any succeeds when one element matches",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Any(".numbers", Equal(".", literal.Int(2))),
|
||||||
|
),
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"numbers": []interface{}{1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "any fails when no elements match",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Any(".numbers", Equal(".", literal.Int(4))),
|
||||||
|
),
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"numbers": []interface{}{1, 2, 3},
|
||||||
|
},
|
||||||
|
expectedMatch: false,
|
||||||
|
expectedStmt: MustConstruct(
|
||||||
|
Any(".numbers", Equal(".", literal.Int(4))),
|
||||||
|
)[0],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Complex nested case
|
||||||
|
{
|
||||||
|
name: "complex nested policy",
|
||||||
|
policy: MustConstruct(
|
||||||
|
And(
|
||||||
|
Equal(".required", literal.String("present")),
|
||||||
|
Equal(".optional?", literal.String("value")),
|
||||||
|
Any(".items",
|
||||||
|
And(
|
||||||
|
Equal(".name", literal.String("test")),
|
||||||
|
Like(".id", "ID*"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"required": "present",
|
||||||
|
"items": []any{
|
||||||
|
map[string]any{
|
||||||
|
"name": "wrong",
|
||||||
|
"id": "ID123",
|
||||||
|
},
|
||||||
|
map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"id": "ID456",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
// missing optional values for all the operators
|
||||||
|
{
|
||||||
|
name: "returns true for missing optional equal",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Equal(".field?", literal.String("value")),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for missing optional like pattern",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Like(".pattern?", "test*"),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for missing optional greater than",
|
||||||
|
policy: MustConstruct(
|
||||||
|
GreaterThan(".number?", literal.Int(5)),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for missing optional less than",
|
||||||
|
policy: MustConstruct(
|
||||||
|
LessThan(".number?", literal.Int(5)),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for missing optional array with all",
|
||||||
|
policy: MustConstruct(
|
||||||
|
All(".numbers?", Equal(".", literal.Int(1))),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for missing optional array with any",
|
||||||
|
policy: MustConstruct(
|
||||||
|
Any(".numbers?", Equal(".", literal.Int(1))),
|
||||||
|
),
|
||||||
|
data: map[string]any{},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for complex nested optional paths",
|
||||||
|
policy: MustConstruct(
|
||||||
|
And(
|
||||||
|
Equal(".required", literal.String("present")),
|
||||||
|
Any(".optional_array?",
|
||||||
|
And(
|
||||||
|
Equal(".name?", literal.String("test")),
|
||||||
|
Like(".id?", "ID*"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"required": "present",
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns true for partially present nested optional paths",
|
||||||
|
policy: MustConstruct(
|
||||||
|
And(
|
||||||
|
Equal(".required", literal.String("present")),
|
||||||
|
Any(".items",
|
||||||
|
And(
|
||||||
|
Equal(".name", literal.String("test")),
|
||||||
|
Like(".optional_id?", "ID*"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: map[string]any{
|
||||||
|
"required": "present",
|
||||||
|
"items": []any{
|
||||||
|
map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
// optional_id is missing
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedMatch: true,
|
||||||
|
expectedStmt: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
node, err := literal.Map(tt.data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
match, stmt := tt.policy.PartialMatch(node)
|
||||||
|
require.Equal(t, tt.expectedMatch, match)
|
||||||
|
if tt.expectedStmt == nil {
|
||||||
|
require.Nil(t, stmt)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, tt.expectedStmt, stmt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
246
pkg/policy/policy.go
Normal file
246
pkg/policy/policy.go
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
|
||||||
|
selpkg "github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KindEqual = "==" // implemented by equality
|
||||||
|
KindGreaterThan = ">" // implemented by equality
|
||||||
|
KindGreaterThanOrEqual = ">=" // implemented by equality
|
||||||
|
KindLessThan = "<" // implemented by equality
|
||||||
|
KindLessThanOrEqual = "<=" // implemented by equality
|
||||||
|
KindNot = "not" // implemented by negation
|
||||||
|
KindAnd = "and" // implemented by connective
|
||||||
|
KindOr = "or" // implemented by connective
|
||||||
|
KindLike = "like" // implemented by wildcard
|
||||||
|
KindAll = "all" // implemented by quantifier
|
||||||
|
KindAny = "any" // implemented by quantifier
|
||||||
|
)
|
||||||
|
|
||||||
|
type Policy []Statement
|
||||||
|
|
||||||
|
type Constructor func() (Statement, error)
|
||||||
|
|
||||||
|
func Construct(cstors ...Constructor) (Policy, error) {
|
||||||
|
stmts, err := assemble(cstors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return stmts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustConstruct(cstors ...Constructor) Policy {
|
||||||
|
pol, err := Construct(cstors...)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return pol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Policy) String() string {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
|
childs := make([]string, len(p))
|
||||||
|
for i, statement := range p {
|
||||||
|
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[\n %s\n]", strings.Join(childs, ",\n "))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Statement interface {
|
||||||
|
Kind() string
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type equality struct {
|
||||||
|
kind string
|
||||||
|
selector selpkg.Selector
|
||||||
|
value ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e equality) Kind() string {
|
||||||
|
return e.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e equality) String() string {
|
||||||
|
child, err := ipld.Encode(e.value, dagjson.Encode)
|
||||||
|
if err != nil {
|
||||||
|
return "ERROR: INVALID VALUE"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`["%s", "%s", %s]`, e.kind, e.selector, strings.ReplaceAll(string(child), "\n", "\n "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Equal(selector string, value ipld.Node) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return equality{kind: KindEqual, selector: sel, value: value}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThan(selector string, value ipld.Node) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return equality{kind: KindGreaterThan, selector: sel, value: value}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GreaterThanOrEqual(selector string, value ipld.Node) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return equality{kind: KindGreaterThanOrEqual, selector: sel, value: value}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThan(selector string, value ipld.Node) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return equality{kind: KindLessThan, selector: sel, value: value}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LessThanOrEqual(selector string, value ipld.Node) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return equality{kind: KindLessThanOrEqual, selector: sel, value: value}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type negation struct {
|
||||||
|
statement Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n negation) Kind() string {
|
||||||
|
return KindNot
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n negation) String() string {
|
||||||
|
child := n.statement.String()
|
||||||
|
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Not(cstor Constructor) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
stmt, err := cstor()
|
||||||
|
return negation{statement: stmt}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type connective struct {
|
||||||
|
kind string
|
||||||
|
statements []Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c connective) Kind() string {
|
||||||
|
return c.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c connective) String() string {
|
||||||
|
childs := make([]string, len(c.statements))
|
||||||
|
for i, statement := range c.statements {
|
||||||
|
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func And(cstors ...Constructor) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
stmts, err := assemble(cstors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return connective{kind: KindAnd, statements: stmts}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Or(cstors ...Constructor) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
stmts, err := assemble(cstors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return connective{kind: KindOr, statements: stmts}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type wildcard struct {
|
||||||
|
selector selpkg.Selector
|
||||||
|
pattern glob
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n wildcard) Kind() string {
|
||||||
|
return KindLike
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n wildcard) String() string {
|
||||||
|
return fmt.Sprintf(`["%s", "%s", "%s"]`, n.Kind(), n.selector, n.pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Like(selector string, pattern string) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
g, err := parseGlob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return wildcard{selector: sel, pattern: g}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type quantifier struct {
|
||||||
|
kind string
|
||||||
|
selector selpkg.Selector
|
||||||
|
statement Statement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n quantifier) Kind() string {
|
||||||
|
return n.kind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n quantifier) String() string {
|
||||||
|
child := n.statement.String()
|
||||||
|
return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func All(selector string, cstor Constructor) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
stmt, err := cstor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return quantifier{kind: KindAll, selector: sel, statement: stmt}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Any(selector string, cstor Constructor) Constructor {
|
||||||
|
return func() (Statement, error) {
|
||||||
|
stmt, err := cstor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sel, err := selpkg.Parse(selector)
|
||||||
|
return quantifier{kind: KindAny, selector: sel, statement: stmt}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assemble(cstors []Constructor) ([]Statement, error) {
|
||||||
|
stmts := make([]Statement, 0, len(cstors))
|
||||||
|
for _, cstor := range cstors {
|
||||||
|
stmt, err := cstor()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stmts = append(stmts, stmt)
|
||||||
|
}
|
||||||
|
return stmts, nil
|
||||||
|
}
|
||||||
91
pkg/policy/policy_test.go
Normal file
91
pkg/policy/policy_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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 ExamplePolicy_accumulate() {
|
||||||
|
var statements []policy.Constructor
|
||||||
|
|
||||||
|
statements = append(statements, policy.Equal(".status", literal.String("draft")))
|
||||||
|
|
||||||
|
statements = append(statements, policy.All(".reviewer",
|
||||||
|
policy.Like(".email", "*@example.com"),
|
||||||
|
))
|
||||||
|
|
||||||
|
statements = append(statements, policy.Any(".tags", policy.Or(
|
||||||
|
policy.Equal(".", literal.String("news")),
|
||||||
|
policy.Equal(".", literal.String("press")),
|
||||||
|
)))
|
||||||
|
|
||||||
|
pol := policy.MustConstruct(statements...)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
67
pkg/policy/policytest/spec.go
Normal file
67
pkg/policy/policytest/spec.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package policytest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmptyPolicy provides a Policy with no statements.
|
||||||
|
var EmptyPolicy = policy.Policy{}
|
||||||
|
|
||||||
|
// ExampleValidationPolicy provides a instantiated SpecPolicy containing the
|
||||||
|
// statements that are included in the second code block of the [Validation]
|
||||||
|
// section of the delegation specification.
|
||||||
|
//
|
||||||
|
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||||
|
var SpecPolicy = policy.MustConstruct(
|
||||||
|
policy.Equal(".from", literal.String("alice@example.com")),
|
||||||
|
policy.Any(".to", policy.Like(".", "*@example.com")),
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: Replace the URL for [Validation] above when the delegation
|
||||||
|
// specification has been finished/merged.
|
||||||
|
|
||||||
|
// SpecValidArguments provides valid, instantiated Arguments containing
|
||||||
|
// the key/value pairs that are included in portion of the the second code
|
||||||
|
// block of the [Validation] section of the delegation specification.
|
||||||
|
//
|
||||||
|
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||||
|
var SpecValidArguments = args.NewBuilder().
|
||||||
|
Add("from", "alice@example.com").
|
||||||
|
Add("to", []string{
|
||||||
|
"bob@example.com",
|
||||||
|
"carol@not.example.com",
|
||||||
|
}).
|
||||||
|
Add("title", "Coffee").
|
||||||
|
Add("body", "Still on for coffee").
|
||||||
|
MustBuild()
|
||||||
|
|
||||||
|
var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
|
||||||
|
|
||||||
|
// SpecInvalidArguments provides invalid, instantiated Arguments containing
|
||||||
|
// the key/value pairs that are included in portion of the the second code
|
||||||
|
// block of the [Validation] section of the delegation specification.
|
||||||
|
//
|
||||||
|
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||||
|
var SpecInvalidArguments = args.NewBuilder().
|
||||||
|
Add("from", "alice@example.com").
|
||||||
|
Add("to", []string{
|
||||||
|
"bob@null.com",
|
||||||
|
"carol@elsewhere.example.com",
|
||||||
|
}).
|
||||||
|
Add("title", "Coffee").
|
||||||
|
Add("body", "Still on for coffee").
|
||||||
|
MustBuild()
|
||||||
|
|
||||||
|
var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments)
|
||||||
|
|
||||||
|
func mustIPLD(args *args.Args) ipld.Node {
|
||||||
|
node, err := args.ToIPLD()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
32
pkg/policy/policytest/spec_test.go
Normal file
32
pkg/policy/policytest/spec_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package policytest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestInvocationValidation applies the example policy to the second
|
||||||
|
// example arguments as defined in the [Validation] section of the
|
||||||
|
// invocation specification.
|
||||||
|
//
|
||||||
|
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||||
|
func TestInvocationValidationSpecExamples(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("with passing args", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD)
|
||||||
|
assert.True(t, exec)
|
||||||
|
assert.Nil(t, stmt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD)
|
||||||
|
assert.False(t, exec)
|
||||||
|
assert.NotNil(t, stmt)
|
||||||
|
})
|
||||||
|
}
|
||||||
209
pkg/policy/selector/parsing.go
Normal file
209
pkg/policy/selector/parsing.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
||||||
|
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
||||||
|
|
||||||
|
// Field name requirements:
|
||||||
|
// - Must start with ASCII letter, Unicode letter, or underscore
|
||||||
|
// - Can contain:
|
||||||
|
// - ASCII letters (a-z, A-Z)
|
||||||
|
// - ASCII digits (0-9)
|
||||||
|
// - Unicode letters (\p{L})
|
||||||
|
// - Dollar sign ($)
|
||||||
|
// - Underscore (_)
|
||||||
|
// - Hyphen (-)
|
||||||
|
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_\p{L}][a-zA-Z0-9$_\p{L}\-]*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func Parse(str string) (Selector, error) {
|
||||||
|
if len(str) == 0 {
|
||||||
|
return nil, newParseError("empty selector", str, 0, "")
|
||||||
|
}
|
||||||
|
if string(str[0]) != "." {
|
||||||
|
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
|
||||||
|
}
|
||||||
|
if str == "." {
|
||||||
|
return Selector{segment{str: ".", identity: true}}, nil
|
||||||
|
}
|
||||||
|
if str == ".?" {
|
||||||
|
return Selector{segment{str: ".?", identity: true, optional: true}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
col := 0
|
||||||
|
var sel Selector
|
||||||
|
for _, tok := range tokenize(str) {
|
||||||
|
seg := tok
|
||||||
|
opt := strings.HasSuffix(tok, "?")
|
||||||
|
if opt {
|
||||||
|
seg = strings.TrimRight(tok, "?")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case seg == ".":
|
||||||
|
if len(sel) > 0 && sel[len(sel)-1].Identity() {
|
||||||
|
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
|
||||||
|
}
|
||||||
|
sel = append(sel, segment{str: ".", identity: true})
|
||||||
|
|
||||||
|
case seg == "[]":
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
|
||||||
|
|
||||||
|
case strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]"):
|
||||||
|
lookup := seg[1 : len(seg)-1]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// index, [123]
|
||||||
|
case indexRegex.MatchString(lookup):
|
||||||
|
idx, err := strconv.Atoi(lookup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseError("invalid index", str, col, tok)
|
||||||
|
}
|
||||||
|
if idx > limits.MaxInt53 || idx < limits.MinInt53 {
|
||||||
|
return nil, newParseError(fmt.Sprintf("index %d exceeds safe integer bounds", idx), str, col, tok)
|
||||||
|
}
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, index: idx})
|
||||||
|
|
||||||
|
// explicit field, ["abcd"]
|
||||||
|
case strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\""):
|
||||||
|
fieldName := lookup[1 : len(lookup)-1]
|
||||||
|
if strings.Contains(fieldName, ":") {
|
||||||
|
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
||||||
|
}
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, field: fieldName})
|
||||||
|
|
||||||
|
// slice [3:5] or [:5] or [3:], also negative numbers
|
||||||
|
case sliceRegex.MatchString(lookup):
|
||||||
|
var rng [2]int64
|
||||||
|
splt := strings.Split(lookup, ":")
|
||||||
|
if splt[0] == "" {
|
||||||
|
rng[0] = math.MinInt
|
||||||
|
} else {
|
||||||
|
i, err := strconv.ParseInt(splt[0], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseError("invalid slice index", str, col, tok)
|
||||||
|
}
|
||||||
|
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||||
|
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||||
|
}
|
||||||
|
rng[0] = i
|
||||||
|
}
|
||||||
|
if splt[1] == "" {
|
||||||
|
rng[1] = math.MaxInt
|
||||||
|
} else {
|
||||||
|
i, err := strconv.ParseInt(splt[1], 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, newParseError("invalid slice index", str, col, tok)
|
||||||
|
}
|
||||||
|
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||||
|
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||||
|
}
|
||||||
|
rng[1] = i
|
||||||
|
}
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
case fieldRegex.MatchString(seg):
|
||||||
|
sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]})
|
||||||
|
default:
|
||||||
|
return nil, newParseError(fmt.Sprintf("invalid segment: %s", seg), str, col, tok)
|
||||||
|
}
|
||||||
|
col += len(tok)
|
||||||
|
}
|
||||||
|
return sel, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustParse(sel string) Selector {
|
||||||
|
s, err := Parse(sel)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenize(str string) []string {
|
||||||
|
var toks []string
|
||||||
|
col := 0
|
||||||
|
ofs := 0
|
||||||
|
ctx := ""
|
||||||
|
|
||||||
|
for col < len(str) {
|
||||||
|
char := string(str[col])
|
||||||
|
|
||||||
|
if char == "\"" && string(str[col-1]) != "\\" {
|
||||||
|
col++
|
||||||
|
if ctx == "\"" {
|
||||||
|
ctx = ""
|
||||||
|
} else {
|
||||||
|
ctx = "\""
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx == "\"" {
|
||||||
|
col++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == "." || char == "[" {
|
||||||
|
if ofs < col {
|
||||||
|
toks = append(toks, str[ofs:col])
|
||||||
|
}
|
||||||
|
ofs = col
|
||||||
|
}
|
||||||
|
col++
|
||||||
|
}
|
||||||
|
|
||||||
|
if ofs < col && ctx != "\"" {
|
||||||
|
toks = append(toks, str[ofs:col])
|
||||||
|
}
|
||||||
|
|
||||||
|
return toks
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseerr struct {
|
||||||
|
msg string
|
||||||
|
src string
|
||||||
|
col int
|
||||||
|
tok string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Name() string {
|
||||||
|
return "ParseError"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Message() string {
|
||||||
|
return p.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Column() int {
|
||||||
|
return p.col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Error() string {
|
||||||
|
return p.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Source() string {
|
||||||
|
return p.src
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p parseerr) Token() string {
|
||||||
|
return p.tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParseError(message string, source string, column int, token string) error {
|
||||||
|
return parseerr{message, source, column, token}
|
||||||
|
}
|
||||||
641
pkg/policy/selector/parsing_test.go
Normal file
641
pkg/policy/selector/parsing_test.go
Normal file
@@ -0,0 +1,641 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
t.Run("identity", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dotted field name", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".foo_bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo_bar")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".foo-bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo-bar")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".foo*bar")
|
||||||
|
require.ErrorContains(t, err, "invalid segment")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit field", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.["foo"]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Equal(t, sel[1].Field(), "foo")
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("iterator, collection value", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.True(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[138]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), 138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("negative index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[-138]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), -138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("List slice", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[7:11]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{7, 11})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".[2:]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{2, math.MaxInt})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".[:42]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{math.MinInt, 42})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".[0:-2]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{0, -2})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional identity", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.True(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional dotted field name", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".foo?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.True(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional explicit field", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.["foo"]?`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Equal(t, sel[1].Field(), "foo")
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional iterator", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.True(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[138]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), 138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional negative index", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[-138]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), -138)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional list slice", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[7:11]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{7, 11})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".[2:]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{2, math.MaxInt})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".[:42]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{math.MinInt, 42})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
|
||||||
|
sel, err = Parse(".[0:-2]?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.True(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{0, -2})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("idempotent optional", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".foo???")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.True(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deny multi dot", func(t *testing.T) {
|
||||||
|
_, err := Parse("..")
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nesting", func(t *testing.T) {
|
||||||
|
str := `.foo.["bar"].[138]?.baz[1:]`
|
||||||
|
sel, err := Parse(str)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, str, sel.String())
|
||||||
|
require.Equal(t, 7, len(sel))
|
||||||
|
require.False(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Equal(t, sel[0].Field(), "foo")
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.True(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
require.False(t, sel[2].Identity())
|
||||||
|
require.False(t, sel[2].Optional())
|
||||||
|
require.False(t, sel[2].Iterator())
|
||||||
|
require.Empty(t, sel[2].Slice())
|
||||||
|
require.Equal(t, sel[2].Field(), "bar")
|
||||||
|
require.Empty(t, sel[2].Index())
|
||||||
|
require.True(t, sel[3].Identity())
|
||||||
|
require.False(t, sel[3].Optional())
|
||||||
|
require.False(t, sel[3].Iterator())
|
||||||
|
require.Empty(t, sel[3].Slice())
|
||||||
|
require.Empty(t, sel[3].Field())
|
||||||
|
require.Empty(t, sel[3].Index())
|
||||||
|
require.False(t, sel[4].Identity())
|
||||||
|
require.True(t, sel[4].Optional())
|
||||||
|
require.False(t, sel[4].Iterator())
|
||||||
|
require.Empty(t, sel[4].Slice())
|
||||||
|
require.Empty(t, sel[4].Field())
|
||||||
|
require.Equal(t, sel[4].Index(), 138)
|
||||||
|
require.False(t, sel[5].Identity())
|
||||||
|
require.False(t, sel[5].Optional())
|
||||||
|
require.False(t, sel[5].Iterator())
|
||||||
|
require.Empty(t, sel[5].Slice())
|
||||||
|
require.Equal(t, sel[5].Field(), "baz")
|
||||||
|
require.Empty(t, sel[5].Index())
|
||||||
|
require.False(t, sel[6].Identity())
|
||||||
|
require.False(t, sel[6].Optional())
|
||||||
|
require.False(t, sel[6].Iterator())
|
||||||
|
require.Equal(t, sel[6].Slice(), []int64{1, math.MaxInt})
|
||||||
|
require.Empty(t, sel[6].Field())
|
||||||
|
require.Empty(t, sel[6].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non dotted", func(t *testing.T) {
|
||||||
|
_, err := Parse("foo")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non quoted", func(t *testing.T) {
|
||||||
|
_, err := Parse(".[foo]")
|
||||||
|
require.NotNil(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice with negative start and positive end", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[0:-2]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{0, -2})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice with start greater than end", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".[5:2]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{5, 2})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice on string", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.["foo"].[1:3]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 4, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Equal(t, sel[1].Field(), "foo")
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
require.True(t, sel[2].Identity())
|
||||||
|
require.False(t, sel[2].Optional())
|
||||||
|
require.False(t, sel[2].Iterator())
|
||||||
|
require.Empty(t, sel[2].Slice())
|
||||||
|
require.Empty(t, sel[2].Field())
|
||||||
|
require.Empty(t, sel[2].Index())
|
||||||
|
require.False(t, sel[3].Identity())
|
||||||
|
require.False(t, sel[3].Optional())
|
||||||
|
require.False(t, sel[3].Iterator())
|
||||||
|
require.Equal(t, sel[3].Slice(), []int64{1, 3})
|
||||||
|
require.Empty(t, sel[3].Field())
|
||||||
|
require.Empty(t, sel[3].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice on array", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[1:3]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Equal(t, sel[1].Slice(), []int64{1, 3})
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index on array", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[1]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Empty(t, sel[1].Field())
|
||||||
|
require.Equal(t, sel[1].Index(), 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid slice on object", func(t *testing.T) {
|
||||||
|
_, err := Parse(`.["foo":"bar"]`)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "invalid segment")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index on object", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.["foo"]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(sel))
|
||||||
|
require.True(t, sel[0].Identity())
|
||||||
|
require.False(t, sel[0].Optional())
|
||||||
|
require.False(t, sel[0].Iterator())
|
||||||
|
require.Empty(t, sel[0].Slice())
|
||||||
|
require.Empty(t, sel[0].Field())
|
||||||
|
require.Empty(t, sel[0].Index())
|
||||||
|
require.False(t, sel[1].Identity())
|
||||||
|
require.False(t, sel[1].Optional())
|
||||||
|
require.False(t, sel[1].Iterator())
|
||||||
|
require.Empty(t, sel[1].Slice())
|
||||||
|
require.Equal(t, sel[1].Field(), "foo")
|
||||||
|
require.Empty(t, sel[1].Index())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice with non-integer start", func(t *testing.T) {
|
||||||
|
_, err := Parse(".[foo:3]")
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice with non-integer end", func(t *testing.T) {
|
||||||
|
_, err := Parse(".[1:bar]")
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index with non-integer", func(t *testing.T) {
|
||||||
|
_, err := Parse(".[foo]")
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("extended field names", func(t *testing.T) {
|
||||||
|
validFields := []string{
|
||||||
|
".basic",
|
||||||
|
".user_name",
|
||||||
|
".user-name",
|
||||||
|
".userName$special",
|
||||||
|
".αβγ", // Greek letters
|
||||||
|
".użytkownik", // Polish characters
|
||||||
|
".用户", // Chinese characters
|
||||||
|
".사용자", // Korean characters
|
||||||
|
"._private",
|
||||||
|
".number123",
|
||||||
|
".camelCase",
|
||||||
|
".snake_case",
|
||||||
|
".kebab-case",
|
||||||
|
".mixed_kebab-case",
|
||||||
|
".with$dollar",
|
||||||
|
".MIXED_Case_123",
|
||||||
|
".unicodeø",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range validFields {
|
||||||
|
sel, err := Parse(field)
|
||||||
|
require.NoError(t, err, "field: %s", field)
|
||||||
|
require.NotNil(t, sel)
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidFields := []string{
|
||||||
|
".123number", // Can't start with digit
|
||||||
|
".@special", // @ not allowed
|
||||||
|
".space name", // No spaces
|
||||||
|
".#hashtag", // No #
|
||||||
|
".name!", // No !
|
||||||
|
".{brackets}", // No brackets
|
||||||
|
".name/with/slashes", // No slashes
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, field := range invalidFields {
|
||||||
|
sel, err := Parse(field)
|
||||||
|
require.Error(t, err, "field: %s", field)
|
||||||
|
require.Nil(t, sel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("integer overflow", func(t *testing.T) {
|
||||||
|
sel, err := Parse(fmt.Sprintf(".[%d]", limits.MaxInt53+1))
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, sel)
|
||||||
|
|
||||||
|
sel, err = Parse(fmt.Sprintf(".[%d]", limits.MinInt53-1))
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, sel)
|
||||||
|
|
||||||
|
// Test slice overflow
|
||||||
|
sel, err = Parse(fmt.Sprintf(".[%d:42]", limits.MaxInt53+1))
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, sel)
|
||||||
|
|
||||||
|
sel, err = Parse(fmt.Sprintf(".[1:%d]", limits.MaxInt53+1))
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, sel)
|
||||||
|
})
|
||||||
|
}
|
||||||
342
pkg/policy/selector/selector.go
Normal file
342
pkg/policy/selector/selector.go
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Selector describes a UCAN policy selector, as specified here:
|
||||||
|
// https://github.com/ucan-wg/delegation/blob/4094d5878b58f5d35055a3b93fccda0b8329ebae/README.md#selectors
|
||||||
|
type Selector []segment
|
||||||
|
|
||||||
|
// Select perform the selection described by the selector on the input IPLD DAG.
|
||||||
|
// Select can return:
|
||||||
|
// - exactly one matched IPLD node
|
||||||
|
// - a resolutionerr error if not being able to resolve to a node
|
||||||
|
// - nil and no errors, if the selector couldn't match on an optional segment (with ?).
|
||||||
|
func (s Selector) Select(subject ipld.Node) (ipld.Node, error) {
|
||||||
|
return resolve(s, subject, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Selector) String() string {
|
||||||
|
var res strings.Builder
|
||||||
|
for _, seg := range s {
|
||||||
|
res.WriteString(seg.String())
|
||||||
|
}
|
||||||
|
return res.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type segment struct {
|
||||||
|
str string
|
||||||
|
identity bool
|
||||||
|
optional bool
|
||||||
|
iterator bool
|
||||||
|
slice []int64 // either 0-length or 2-length
|
||||||
|
field string
|
||||||
|
index int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the segment's string representation.
|
||||||
|
func (s segment) String() string {
|
||||||
|
return s.str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity flags that this selector is the identity selector.
|
||||||
|
func (s segment) Identity() bool {
|
||||||
|
return s.identity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional flags that this selector is optional.
|
||||||
|
func (s segment) Optional() bool {
|
||||||
|
return s.optional
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterator flags that this selector is an iterator segment.
|
||||||
|
func (s segment) Iterator() bool {
|
||||||
|
return s.iterator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice flags that this segment targets a range of a slice.
|
||||||
|
func (s segment) Slice() []int64 {
|
||||||
|
return s.slice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field is the name of a field in a struct/map.
|
||||||
|
func (s segment) Field() string {
|
||||||
|
return s.field
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index is an index of a slice.
|
||||||
|
func (s segment) Index() int {
|
||||||
|
return s.index
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, error) {
|
||||||
|
errIfNotOptional := func(s segment, err error) error {
|
||||||
|
if !s.Optional() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cur := subject
|
||||||
|
for _, seg := range sel {
|
||||||
|
// 1st level: handle the different segment types (iterator, field, slice, index)
|
||||||
|
// 2nd level: handle different node kinds (list, map, string, bytes)
|
||||||
|
switch {
|
||||||
|
case seg.Identity():
|
||||||
|
continue
|
||||||
|
|
||||||
|
case seg.Iterator():
|
||||||
|
switch {
|
||||||
|
case cur == nil || cur.Kind() == datamodel.Kind_Null:
|
||||||
|
if seg.Optional() {
|
||||||
|
// build an empty list
|
||||||
|
n, _ := qp.BuildList(basicnode.Prototype.Any, 0, func(_ datamodel.ListAssembler) {})
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
||||||
|
|
||||||
|
case cur.Kind() == datamodel.Kind_List:
|
||||||
|
// iterators are no-op on list
|
||||||
|
continue
|
||||||
|
|
||||||
|
case cur.Kind() == datamodel.Kind_Map:
|
||||||
|
// iterators on maps collect the values
|
||||||
|
nd, err := qp.BuildList(basicnode.Prototype.Any, cur.Length(), func(l datamodel.ListAssembler) {
|
||||||
|
it := cur.MapIterator()
|
||||||
|
for !it.Done() {
|
||||||
|
_, v, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
// recovered by BuildList
|
||||||
|
// Error is bubbled up, but should never occur as we already checked the type,
|
||||||
|
// and are using the iterator correctly.
|
||||||
|
// This is verified with fuzzing.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
qp.ListEntry(l, qp.Node(v))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic("should never happen")
|
||||||
|
}
|
||||||
|
return nd, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
|
||||||
|
}
|
||||||
|
|
||||||
|
case seg.Field() != "":
|
||||||
|
at = append(at, seg.Field())
|
||||||
|
switch {
|
||||||
|
case cur == nil:
|
||||||
|
err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
||||||
|
return nil, errIfNotOptional(seg, err)
|
||||||
|
|
||||||
|
case cur.Kind() == datamodel.Kind_Map:
|
||||||
|
n, err := cur.LookupByString(seg.Field())
|
||||||
|
if err != nil {
|
||||||
|
// the only possible error is missing field as we already check the type
|
||||||
|
if seg.Optional() {
|
||||||
|
cur = nil
|
||||||
|
} else {
|
||||||
|
return nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cur = n
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
|
||||||
|
return nil, errIfNotOptional(seg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case len(seg.Slice()) > 0:
|
||||||
|
if cur == nil {
|
||||||
|
err := newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
||||||
|
return nil, errIfNotOptional(seg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slice := seg.Slice()
|
||||||
|
|
||||||
|
switch cur.Kind() {
|
||||||
|
case datamodel.Kind_List:
|
||||||
|
start, end := resolveSliceIndices(slice, cur.Length())
|
||||||
|
sliced, err := qp.BuildList(basicnode.Prototype.Any, end-start, func(l datamodel.ListAssembler) {
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
item, err := cur.LookupByIndex(i)
|
||||||
|
if err != nil {
|
||||||
|
// recovered by BuildList
|
||||||
|
// Error is bubbled up, but should never occur as we already checked the type and boundaries
|
||||||
|
// This is verified with fuzzing.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
qp.ListEntry(l, qp.Node(item))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic("should never happen")
|
||||||
|
}
|
||||||
|
cur = sliced
|
||||||
|
|
||||||
|
case datamodel.Kind_Bytes:
|
||||||
|
b, _ := cur.AsBytes()
|
||||||
|
start, end := resolveSliceIndices(slice, int64(len(b)))
|
||||||
|
cur = basicnode.NewBytes(b[start:end])
|
||||||
|
|
||||||
|
case datamodel.Kind_String:
|
||||||
|
str, _ := cur.AsString()
|
||||||
|
runes := []rune(str)
|
||||||
|
start, end := resolveSliceIndices(slice, int64(len(runes)))
|
||||||
|
cur = basicnode.NewString(string(runes[start:end]))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
|
||||||
|
}
|
||||||
|
|
||||||
|
default: // Index()
|
||||||
|
at = append(at, strconv.Itoa(seg.Index()))
|
||||||
|
|
||||||
|
if cur == nil {
|
||||||
|
err := newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
|
||||||
|
return nil, errIfNotOptional(seg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := seg.Index()
|
||||||
|
switch cur.Kind() {
|
||||||
|
case datamodel.Kind_List:
|
||||||
|
if idx < 0 {
|
||||||
|
idx = int(cur.Length()) + idx
|
||||||
|
}
|
||||||
|
if idx < 0 || idx >= int(cur.Length()) {
|
||||||
|
err := newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
|
||||||
|
return nil, errIfNotOptional(seg, err)
|
||||||
|
}
|
||||||
|
cur, _ = cur.LookupByIndex(int64(idx))
|
||||||
|
|
||||||
|
case datamodel.Kind_Bytes:
|
||||||
|
b, _ := cur.AsBytes()
|
||||||
|
if idx < 0 {
|
||||||
|
idx = len(b) + idx
|
||||||
|
}
|
||||||
|
if idx < 0 || idx >= len(b) {
|
||||||
|
err := newResolutionError(fmt.Sprintf("index %d out of bounds for bytes of length %d", seg.Index(), len(b)), at)
|
||||||
|
return nil, errIfNotOptional(seg, err)
|
||||||
|
}
|
||||||
|
cur = basicnode.NewInt(int64(b[idx]))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// segment exhausted, we return where we are
|
||||||
|
return cur, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveSliceIndices resolves the start and end indices for slicing a list or byte array.
|
||||||
|
//
|
||||||
|
// It takes the slice indices from the selector segment and the length of the list or byte array,
|
||||||
|
// and returns the resolved start and end indices. Negative indices are supported.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - slice: The slice indices from the selector segment.
|
||||||
|
// - length: The length of the list or byte array being sliced.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - start: The resolved start index for slicing.
|
||||||
|
// - end: The resolved **excluded** end index for slicing.
|
||||||
|
func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||||
|
if len(slice) != 2 {
|
||||||
|
panic("should always be 2-length")
|
||||||
|
}
|
||||||
|
|
||||||
|
start, end = slice[0], slice[1]
|
||||||
|
|
||||||
|
// adjust boundaries
|
||||||
|
switch {
|
||||||
|
case slice[0] == math.MinInt:
|
||||||
|
start = 0
|
||||||
|
case slice[0] < 0:
|
||||||
|
// Check for potential overflow before adding
|
||||||
|
if -slice[0] > length {
|
||||||
|
start = 0
|
||||||
|
} else {
|
||||||
|
start = length + slice[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case slice[1] == math.MaxInt:
|
||||||
|
end = length
|
||||||
|
case slice[1] < 0:
|
||||||
|
// Check for potential overflow before adding
|
||||||
|
if -slice[1] > length {
|
||||||
|
end = 0
|
||||||
|
} else {
|
||||||
|
end = length + slice[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// backward iteration is not allowed, shortcut to an empty result
|
||||||
|
if start >= end {
|
||||||
|
start, end = 0, 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// clamp out of bound
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if start > length {
|
||||||
|
start = length
|
||||||
|
}
|
||||||
|
if end < 0 {
|
||||||
|
end = 0
|
||||||
|
}
|
||||||
|
if end > length {
|
||||||
|
end = length
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func kindString(n datamodel.Node) string {
|
||||||
|
if n == nil {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
return n.Kind().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolutionerr struct {
|
||||||
|
msg string
|
||||||
|
at []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) Name() string {
|
||||||
|
return "ResolutionError"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) Message() string {
|
||||||
|
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) At() []string {
|
||||||
|
return r.at
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resolutionerr) Error() string {
|
||||||
|
return r.Message()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResolutionError(message string, at []string) error {
|
||||||
|
return resolutionerr{message, at}
|
||||||
|
}
|
||||||
413
pkg/policy/selector/selector_test.go
Normal file
413
pkg/policy/selector/selector_test.go
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
package selector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSelect(t *testing.T) {
|
||||||
|
type name struct {
|
||||||
|
First string
|
||||||
|
Middle *string
|
||||||
|
Last string
|
||||||
|
}
|
||||||
|
type interest struct {
|
||||||
|
Name string
|
||||||
|
Outdoor bool
|
||||||
|
Experience int
|
||||||
|
}
|
||||||
|
type user struct {
|
||||||
|
Name name
|
||||||
|
Age int
|
||||||
|
Nationalities []string
|
||||||
|
Interests []interest
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := ipld.LoadSchemaBytes([]byte(`
|
||||||
|
type User struct {
|
||||||
|
name Name
|
||||||
|
age Int
|
||||||
|
nationalities [String]
|
||||||
|
interests [Interest]
|
||||||
|
}
|
||||||
|
type Name struct {
|
||||||
|
first String
|
||||||
|
middle optional String
|
||||||
|
last String
|
||||||
|
}
|
||||||
|
type Interest struct {
|
||||||
|
name String
|
||||||
|
outdoor Bool
|
||||||
|
experience Int
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
typ := ts.TypeByName("User")
|
||||||
|
|
||||||
|
am := "Joan"
|
||||||
|
alice := user{
|
||||||
|
Name: name{First: "Alice", Middle: &am, Last: "Wonderland"},
|
||||||
|
Age: 24,
|
||||||
|
Nationalities: []string{"British"},
|
||||||
|
Interests: []interest{
|
||||||
|
{Name: "Cycling", Outdoor: true, Experience: 4},
|
||||||
|
{Name: "Chess", Outdoor: false, Experience: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bob := user{
|
||||||
|
Name: name{First: "Bob", Last: "Builder"},
|
||||||
|
Age: 35,
|
||||||
|
Nationalities: []string{"Canadian", "South African"},
|
||||||
|
Interests: []interest{
|
||||||
|
{Name: "Snowboarding", Outdoor: true, Experience: 8},
|
||||||
|
{Name: "Reading", Outdoor: false, Experience: 25},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
anode := bindnode.Wrap(&alice, typ)
|
||||||
|
bnode := bindnode.Wrap(&bob, typ)
|
||||||
|
|
||||||
|
t.Run("identity", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
age := must.Int(must.Node(res.LookupByString("age")))
|
||||||
|
require.Equal(t, int64(alice.Age), age)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nested property", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.first")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
name := must.String(res)
|
||||||
|
require.Equal(t, alice.Name.First, name)
|
||||||
|
|
||||||
|
res, err = sel.Select(bnode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
name = must.String(res)
|
||||||
|
require.Equal(t, bob.Name.First, name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional nested property", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.middle?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
name := must.String(res)
|
||||||
|
require.Equal(t, *alice.Name.Middle, name)
|
||||||
|
|
||||||
|
res, err = sel.Select(bnode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, res)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not exists", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.foo")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(anode)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Empty(t, res)
|
||||||
|
|
||||||
|
require.ErrorAs(t, err, &resolutionerr{}, "error should be a resolution error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("optional not exists", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".name.foo?")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
one, err := sel.Select(anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, one)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("iterator", func(t *testing.T) {
|
||||||
|
sel, err := Parse(".interests[]")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(anode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
|
||||||
|
require.Equal(t, alice.Interests[0].Name, iname)
|
||||||
|
|
||||||
|
iname = must.String(must.Node(must.Node(res.LookupByIndex(1)).LookupByString("name")))
|
||||||
|
require.Equal(t, alice.Interests[1].Name, iname)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice on string", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[1:3]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node := basicnode.NewString("hello")
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
str, err := res.AsString()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "el", str) // assert sliced substring
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("out of bounds slicing", func(t *testing.T) {
|
||||||
|
node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.Int(1))
|
||||||
|
qp.ListEntry(la, qp.Int(2))
|
||||||
|
qp.ListEntry(la, qp.Int(3))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sel, err := Parse(`.[10:20]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
require.Equal(t, int64(0), res.Length())
|
||||||
|
|
||||||
|
_, err = res.LookupByIndex(0)
|
||||||
|
require.ErrorIs(t, err, datamodel.ErrNotExists{}) // assert empty result for out of bounds slice
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("backward slicing", func(t *testing.T) {
|
||||||
|
node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.Int(1))
|
||||||
|
qp.ListEntry(la, qp.Int(2))
|
||||||
|
qp.ListEntry(la, qp.Int(3))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sel, err := Parse(`.[5:2]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
require.Equal(t, int64(0), res.Length())
|
||||||
|
|
||||||
|
_, err = res.LookupByIndex(0)
|
||||||
|
require.ErrorIs(t, err, datamodel.ErrNotExists{}) // assert empty result for backward slice
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice with negative index", func(t *testing.T) {
|
||||||
|
node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.Int(1))
|
||||||
|
qp.ListEntry(la, qp.Int(2))
|
||||||
|
qp.ListEntry(la, qp.Int(3))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sel, err := Parse(`.[0:-1]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
val, err := res.LookupByIndex(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, int(must.Int(val))) // Assert sliced value at index 1
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("slice on bytes", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[1:3]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
bytes, err := res.AsBytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []byte{0x02, 0x03}, bytes) // assert sliced bytes
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("index on bytes", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[2]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
|
val, err := res.AsInt()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(0x03), val) // assert indexed byte value
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("out of bounds slicing on bytes", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[10:20]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03})
|
||||||
|
res, err := sel.Select(node)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, res)
|
||||||
|
|
||||||
|
bytes, err := res.AsBytes()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, bytes) // assert empty result for out of bounds slice
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("out of bounds indexing on bytes", func(t *testing.T) {
|
||||||
|
sel, err := Parse(`.[10]`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
node := basicnode.NewBytes([]byte{0x01, 0x02, 0x03})
|
||||||
|
_, err = sel.Select(node)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "can not resolve path: .10") // assert error for out of bounds index
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzParse(f *testing.F) {
|
||||||
|
selectorCorpus := []string{
|
||||||
|
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
||||||
|
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
||||||
|
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
||||||
|
`.length`, `.[4]`,
|
||||||
|
}
|
||||||
|
for _, selector := range selectorCorpus {
|
||||||
|
f.Add(selector)
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, selector string) {
|
||||||
|
// only look for panic()
|
||||||
|
_, _ = Parse(selector)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzParseAndSelect(f *testing.F) {
|
||||||
|
selectorCorpus := []string{
|
||||||
|
`.`, `.[]`, `.[]?`, `.[][]?`, `.x`, `.["x"]`, `.[0]`, `.[-1]`, `.[0]`,
|
||||||
|
`.[0]`, `.[0:2]`, `.[1:]`, `.[:2]`, `.[0:2]`, `.[1:]`, `.x?`, `.x?`,
|
||||||
|
`.x?`, `.["x"]?`, `.length?`, `.[4]?`, `.[]`, `.[][]`, `.x`, `.x`, `.x`,
|
||||||
|
`.length`, `.[4]`,
|
||||||
|
}
|
||||||
|
subjectCorpus := []string{
|
||||||
|
`{"x":1}`, `[1, 2]`, `null`, `[[1], 2, [3]]`, `{"x": 1 }`, `{"x": 1}`,
|
||||||
|
`[1, 2]`, `[1, 2]`, `"Hi"`, `{"/":{"bytes":"AAE"}`, `[0, 1, 2]`,
|
||||||
|
`[0, 1, 2]`, `[0, 1, 2]`, `"hello"`, `{"/":{"bytes":"AAEC"}}`, `{}`,
|
||||||
|
`null`, `[]`, `{}`, `[1, 2]`, `[0, 1]`, `null`, `[[1], 2, [3]]`, `{}`,
|
||||||
|
`null`, `[]`, `[1, 2]`, `[0, 1]`,
|
||||||
|
}
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
switch {
|
||||||
|
case i < len(selectorCorpus) && i < len(subjectCorpus):
|
||||||
|
f.Add(selectorCorpus[i], subjectCorpus[i])
|
||||||
|
continue
|
||||||
|
case i > len(selectorCorpus):
|
||||||
|
f.Add("", subjectCorpus[i])
|
||||||
|
continue
|
||||||
|
case i > len(subjectCorpus):
|
||||||
|
f.Add(selectorCorpus[i], "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, selector, subject string) {
|
||||||
|
sel, err := Parse(selector)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
np := basicnode.Prototype.Any
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
err = dagjson.Decode(nb, strings.NewReader(subject))
|
||||||
|
if err != nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
node := nb.Build()
|
||||||
|
if node == nil {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for panic()
|
||||||
|
_, err = sel.Select(node)
|
||||||
|
if err != nil && !errors.As(err, &resolutionerr{}) {
|
||||||
|
// not normal, we should only have resolution errors
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveSliceIndices(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
slice []int64
|
||||||
|
length int64
|
||||||
|
wantStart int64
|
||||||
|
wantEnd int64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "normal case",
|
||||||
|
slice: []int64{1, 3},
|
||||||
|
length: 5,
|
||||||
|
wantStart: 1,
|
||||||
|
wantEnd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative indices",
|
||||||
|
slice: []int64{-2, -1},
|
||||||
|
length: 5,
|
||||||
|
wantStart: 3,
|
||||||
|
wantEnd: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overflow protection negative start",
|
||||||
|
slice: []int64{math.MinInt64, 3},
|
||||||
|
length: 5,
|
||||||
|
wantStart: 0,
|
||||||
|
wantEnd: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overflow protection negative end",
|
||||||
|
slice: []int64{0, math.MinInt64},
|
||||||
|
length: 5,
|
||||||
|
wantStart: 0,
|
||||||
|
wantEnd: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max bounds",
|
||||||
|
slice: []int64{0, math.MaxInt64},
|
||||||
|
length: 5,
|
||||||
|
wantStart: 0,
|
||||||
|
wantEnd: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
start, end := resolveSliceIndices(tt.slice, tt.length)
|
||||||
|
require.Equal(t, tt.wantStart, start)
|
||||||
|
require.Equal(t, tt.wantEnd, end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
package selector_test
|
package selector_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/v1/capability/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestSupported Forms runs tests against the Selector according to the
|
// TestSupported Forms runs tests against the Selector according to the
|
||||||
@@ -26,18 +23,16 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
Output string
|
Output string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass
|
// Pass and return a node
|
||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
{Name: "Identity", Selector: `.`, Input: `{"x":1}`, Output: `{"x":1}`},
|
||||||
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
{Name: "Iterator", Selector: `.[]`, Input: `[1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `()`},
|
{Name: "Optional Null Iterator", Selector: `.[]?`, Input: `null`, Output: `[]`},
|
||||||
{Name: "Optional Iterator", Selector: `.[][]?`, Input: `[[1], 2, [3]]`, Output: `[1, 3]`},
|
|
||||||
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
{Name: "Object Key", Selector: `.x`, Input: `{"x": 1 }`, Output: `1`},
|
||||||
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
{Name: "Quoted Key", Selector: `.["x"]`, Input: `{"x": 1}`, Output: `1`},
|
||||||
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
{Name: "Index", Selector: `.[0]`, Input: `[1, 2]`, Output: `1`},
|
||||||
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
{Name: "Negative Index", Selector: `.[-1]`, Input: `[1, 2]`, Output: `2`},
|
||||||
{Name: "String Index", Selector: `.[0]`, Input: `"Hi"`, Output: `"H"`},
|
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}}`, Output: `0`},
|
||||||
{Name: "Bytes Index", Selector: `.[0]`, Input: `{"/":{"bytes":"AAE"}`, Output: `0`},
|
|
||||||
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[0:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
{Name: "Array Slice", Selector: `.[1:]`, Input: `[0, 1, 2]`, Output: `[1, 2]`},
|
||||||
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
{Name: "Array Slice", Selector: `.[:2]`, Input: `[0, 1, 2]`, Output: `[0, 1]`},
|
||||||
@@ -52,35 +47,16 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
res, err := sel.Select(makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEqual(t, node != nil, len(nodes) > 0) // XOR (only one of node or nodes should be set)
|
require.NotNil(t, res)
|
||||||
|
|
||||||
// make an IPLD List node from a []datamodel.Node
|
|
||||||
if node == nil {
|
|
||||||
nb := basicnode.Prototype.List.NewBuilder()
|
|
||||||
la, err := nb.BeginList(int64(len(nodes)))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
for _, n := range nodes {
|
|
||||||
// TODO: This code is probably not needed if the Select operation properly prunes nil values - e.g.: Optional Iterator
|
|
||||||
if n == nil {
|
|
||||||
n = datamodel.Null
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, la.AssembleValue().AssignNode(n))
|
|
||||||
}
|
|
||||||
require.NoError(t, la.Finish())
|
|
||||||
|
|
||||||
node = nb.Build()
|
|
||||||
}
|
|
||||||
|
|
||||||
exp := makeNode(t, tc.Output)
|
exp := makeNode(t, tc.Output)
|
||||||
equalIPLD(t, exp, node)
|
require.True(t, ipld.DeepEqual(exp, res))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// null
|
// No error and return null, as optional
|
||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Optional Missing Key", Selector: `.x?`, Input: `{}`},
|
{Name: "Optional Missing Key", Selector: `.x?`, Input: `{}`},
|
||||||
{Name: "Optional Null Key", Selector: `.x?`, Input: `null`},
|
{Name: "Optional Null Key", Selector: `.x?`, Input: `null`},
|
||||||
@@ -97,19 +73,15 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
res, err := sel.Select(makeNode(t, tc.Input))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// TODO: should Select return a single node which is sometimes a list or null?
|
require.Nil(t, res)
|
||||||
// require.Equal(t, datamodel.Null, node)
|
|
||||||
assert.Nil(t, node)
|
|
||||||
assert.Empty(t, nodes)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// error
|
// fail to select and return an error
|
||||||
for _, testcase := range []Testcase{
|
for _, testcase := range []Testcase{
|
||||||
{Name: "Null Iterator", Selector: `.[]`, Input: `null`},
|
{Name: "Null Iterator", Selector: `.[]`, Input: `null`},
|
||||||
{Name: "Nested Iterator", Selector: `.[][]`, Input: `[[1], 2, [3]]`},
|
|
||||||
{Name: "Missing Key", Selector: `.x`, Input: `{}`},
|
{Name: "Missing Key", Selector: `.x`, Input: `{}`},
|
||||||
{Name: "Null Key", Selector: `.x`, Input: `null`},
|
{Name: "Null Key", Selector: `.x`, Input: `null`},
|
||||||
{Name: "Array Key", Selector: `.x`, Input: `[]`},
|
{Name: "Array Key", Selector: `.x`, Input: `[]`},
|
||||||
@@ -124,31 +96,13 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// attempt to select
|
// attempt to select
|
||||||
node, nodes, err := selector.Select(sel, makeNode(t, tc.Input))
|
res, err := sel.Select(makeNode(t, tc.Input))
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Nil(t, node)
|
require.Nil(t, res)
|
||||||
assert.Empty(t, nodes)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func equalIPLD(t *testing.T, expected datamodel.Node, actual datamodel.Node) bool {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
exp, act := &bytes.Buffer{}, &bytes.Buffer{}
|
|
||||||
if err := dagjson.Encode(expected, exp); err != nil {
|
|
||||||
return assert.Fail(t, "Failed to encode json for expected IPLD node")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dagjson.Encode(actual, act); err != nil {
|
|
||||||
return assert.Fail(t, "Failed to encode JSON for actual IPLD node")
|
|
||||||
}
|
|
||||||
|
|
||||||
require.JSONEq(t, exp.String(), act.String())
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeNode(t *testing.T, dagJsonInput string) ipld.Node {
|
func makeNode(t *testing.T, dagJsonInput string) ipld.Node {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
90
pkg/secretbox/secretbox.go
Normal file
90
pkg/secretbox/secretbox.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package secretbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/nacl/secretbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
const keySize = 32 // secretbox allows only 32-byte keys
|
||||||
|
|
||||||
|
var ErrShortCipherText = errors.New("ciphertext too short")
|
||||||
|
var ErrNoEncryptionKey = errors.New("encryption key is required")
|
||||||
|
var ErrInvalidKeySize = errors.New("invalid key size: must be 32 bytes")
|
||||||
|
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
|
||||||
|
|
||||||
|
// GenerateKey generates a random 32-byte key to be used by EncryptWithKey and DecryptWithKey
|
||||||
|
func GenerateKey() ([]byte, error) {
|
||||||
|
key := make([]byte, keySize)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate key: %w", err)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptWithKey encrypts data using NaCl's secretbox with the provided key.
|
||||||
|
// 40 bytes of overhead (24-byte nonce + 16-byte MAC) are added to the plaintext size.
|
||||||
|
func EncryptWithKey(data, key []byte) ([]byte, error) {
|
||||||
|
if err := validateKey(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey [keySize]byte
|
||||||
|
copy(secretKey[:], key)
|
||||||
|
|
||||||
|
// Generate 24 bytes of random data as nonce
|
||||||
|
var nonce [24]byte
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt and authenticate data
|
||||||
|
encrypted := secretbox.Seal(nonce[:], data, &nonce, &secretKey)
|
||||||
|
return encrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptStringWithKey decrypts data using secretbox with the provided key
|
||||||
|
func DecryptStringWithKey(data, key []byte) ([]byte, error) {
|
||||||
|
if err := validateKey(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < 24 {
|
||||||
|
return nil, ErrShortCipherText
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretKey [keySize]byte
|
||||||
|
copy(secretKey[:], key)
|
||||||
|
|
||||||
|
var nonce [24]byte
|
||||||
|
copy(nonce[:], data[:24])
|
||||||
|
|
||||||
|
decrypted, ok := secretbox.Open(nil, data[24:], &nonce, &secretKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("decryption failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateKey(key []byte) error {
|
||||||
|
if key == nil {
|
||||||
|
return ErrNoEncryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) != keySize {
|
||||||
|
return ErrInvalidKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if key is all zeros
|
||||||
|
for _, b := range key {
|
||||||
|
if b != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrZeroKey
|
||||||
|
}
|
||||||
144
pkg/secretbox/secretbox_test.go
Normal file
144
pkg/secretbox/secretbox_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package secretbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSecretBoxEncryption(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, keySize) // generate random 32-byte key
|
||||||
|
_, errKey := rand.Read(key)
|
||||||
|
require.NoError(t, errKey)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
key []byte
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid encryption/decryption",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil key returns error",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: nil,
|
||||||
|
wantErr: ErrNoEncryptionKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty data",
|
||||||
|
data: []byte{},
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key size",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: make([]byte, 16), // Only 32 bytes allowed now
|
||||||
|
wantErr: ErrInvalidKeySize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero key returns error",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: make([]byte, keySize),
|
||||||
|
wantErr: ErrZeroKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encrypted, err := EncryptWithKey(tt.data, tt.key)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify encrypted data is different and includes nonce
|
||||||
|
require.Greater(t, len(encrypted), 24) // At least nonce size
|
||||||
|
if len(tt.data) > 0 {
|
||||||
|
require.NotEqual(t, tt.data, encrypted[24:]) // Ignore nonce prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := DecryptStringWithKey(encrypted, tt.key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, bytes.Equal(tt.data, decrypted))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptionErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, keySize)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create valid encrypted data for tampering tests
|
||||||
|
validData := []byte("test message")
|
||||||
|
encrypted, err := EncryptWithKey(validData, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
key []byte
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "short ciphertext",
|
||||||
|
data: make([]byte, 23), // Less than nonce size
|
||||||
|
key: key,
|
||||||
|
errMsg: "ciphertext too short",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ciphertext",
|
||||||
|
data: make([]byte, 24), // Just nonce size
|
||||||
|
key: key,
|
||||||
|
errMsg: "decryption failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tampered ciphertext",
|
||||||
|
data: tamperWithBytes(encrypted),
|
||||||
|
key: key,
|
||||||
|
errMsg: "decryption failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing key",
|
||||||
|
data: encrypted,
|
||||||
|
key: nil,
|
||||||
|
errMsg: "encryption key is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := DecryptStringWithKey(tt.data, tt.key)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tamperWithBytes modifies a byte in the encrypted data to simulate tampering
|
||||||
|
func tamperWithBytes(data []byte) []byte {
|
||||||
|
if len(data) < 25 { // Need at least nonce + 1 byte
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
tampered := make([]byte, len(data))
|
||||||
|
copy(tampered, data)
|
||||||
|
tampered[24] ^= 0x01 // Modify first byte after nonce
|
||||||
|
return tampered
|
||||||
|
}
|
||||||
274
token/delegation/delegation.go
Normal file
274
token/delegation/delegation.go
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
// Package delegation implements the UCAN [delegation] specification with
|
||||||
|
// an immutable Token type as well as methods to convert the Token to and
|
||||||
|
// from the [envelope]-enclosed, signed and DAG-CBOR-encoded form that
|
||||||
|
// should most commonly be used for transport and storage.
|
||||||
|
//
|
||||||
|
// [delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
||||||
|
// [envelope]: https://github.com/ucan-wg/spec#envelope
|
||||||
|
package delegation
|
||||||
|
|
||||||
|
// TODO: change the "delegation" link above when the specification is merged
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/nonce"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Token is an immutable type that holds the fields of a UCAN delegation.
|
||||||
|
type Token struct {
|
||||||
|
// Issuer DID (sender)
|
||||||
|
issuer did.DID
|
||||||
|
// Audience DID (receiver)
|
||||||
|
audience did.DID
|
||||||
|
// Principal that the chain is about (the Subject)
|
||||||
|
subject did.DID
|
||||||
|
// The Command to eventually invoke
|
||||||
|
command command.Command
|
||||||
|
// The delegation policy
|
||||||
|
policy policy.Policy
|
||||||
|
// A unique, random nonce
|
||||||
|
nonce []byte
|
||||||
|
// Arbitrary Metadata
|
||||||
|
meta *meta.Meta
|
||||||
|
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
|
notBefore *time.Time
|
||||||
|
// The timestamp at which the Invocation becomes invalid
|
||||||
|
expiration *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a validated delegation Token from the provided parameters and options.
|
||||||
|
// This is typically used to delegate a given power to another agent.
|
||||||
|
//
|
||||||
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
||||||
|
func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) {
|
||||||
|
tkn := &Token{
|
||||||
|
issuer: iss,
|
||||||
|
audience: aud,
|
||||||
|
subject: sub,
|
||||||
|
command: cmd,
|
||||||
|
policy: pol,
|
||||||
|
meta: meta.NewMeta(),
|
||||||
|
nonce: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err := opt(tkn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if len(tkn.nonce) == 0 {
|
||||||
|
tkn.nonce, err = nonce.Generate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tkn.validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Root creates a validated UCAN delegation Token from the provided parameters and options.
|
||||||
|
// This is typically used to create and give a power to an agent.
|
||||||
|
//
|
||||||
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
|
||||||
|
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
|
return New(iss, aud, cmd, pol, iss, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
|
||||||
|
//
|
||||||
|
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
|
||||||
|
// This is a very powerful pattern, use it only if you understand it.
|
||||||
|
// Powerline delegations MUST NOT be used as the root delegation to a resource
|
||||||
|
//
|
||||||
|
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
|
||||||
|
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
|
||||||
|
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
|
||||||
|
//
|
||||||
|
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
|
||||||
|
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
|
return New(iss, aud, cmd, pol, did.Undef, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issuer returns the did.DID representing the Token's issuer.
|
||||||
|
func (t *Token) Issuer() did.DID {
|
||||||
|
return t.issuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audience returns the did.DID representing the Token's audience.
|
||||||
|
func (t *Token) Audience() did.DID {
|
||||||
|
return t.audience
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject returns the did.DID representing the Token's subject.
|
||||||
|
//
|
||||||
|
// This field may be did.Undef for delegations that are [Powerlined] but
|
||||||
|
// must be equal to the value returned by the Issuer method for root
|
||||||
|
// tokens.
|
||||||
|
func (t *Token) Subject() did.DID {
|
||||||
|
return t.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command returns the capability's command.Command.
|
||||||
|
func (t *Token) Command() command.Command {
|
||||||
|
return t.command
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy returns the capability's policy.Policy.
|
||||||
|
func (t *Token) Policy() policy.Policy {
|
||||||
|
return t.policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nonce returns the random Nonce encapsulated in this Token.
|
||||||
|
func (t *Token) Nonce() []byte {
|
||||||
|
return t.nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Meta returns the Token's metadata.
|
||||||
|
func (t *Token) Meta() meta.ReadOnly {
|
||||||
|
return t.meta.ReadOnly()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotBefore returns the time at which the Token becomes "active".
|
||||||
|
func (t *Token) NotBefore() *time.Time {
|
||||||
|
return t.notBefore
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expiration returns the time at which the Token expires.
|
||||||
|
func (t *Token) Expiration() *time.Time {
|
||||||
|
return t.expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
func (t *Token) IsValidNow() bool {
|
||||||
|
return t.IsValidAt(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
func (t *Token) IsValidAt(ti time.Time) bool {
|
||||||
|
if t.expiration != nil && ti.After(*t.expiration) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if t.notBefore != nil && ti.Before(*t.notBefore) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) String() string {
|
||||||
|
var res strings.Builder
|
||||||
|
|
||||||
|
var kind string
|
||||||
|
switch {
|
||||||
|
case t.issuer == t.subject:
|
||||||
|
kind = " (root delegation)"
|
||||||
|
case t.subject == did.Undef:
|
||||||
|
kind = " (powerline delegation)"
|
||||||
|
default:
|
||||||
|
kind = " (normal delegation)"
|
||||||
|
}
|
||||||
|
|
||||||
|
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
||||||
|
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
||||||
|
res.WriteString(fmt.Sprintf("Subject: %s%s\n", t.Subject(), kind))
|
||||||
|
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
||||||
|
res.WriteString(fmt.Sprintf("Policy: %s\n", t.Policy()))
|
||||||
|
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
|
||||||
|
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
|
||||||
|
res.WriteString(fmt.Sprintf("NotBefore: %v\n", t.NotBefore()))
|
||||||
|
res.WriteString(fmt.Sprintf("Expiration: %v", t.Expiration()))
|
||||||
|
|
||||||
|
return res.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) validate() error {
|
||||||
|
var errs error
|
||||||
|
|
||||||
|
requiredDID := func(id did.DID, fieldname string) {
|
||||||
|
if !id.Defined() {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf(`a valid did is required for %s: %s`, fieldname, id.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredDID(t.issuer, "Issuer")
|
||||||
|
requiredDID(t.audience, "Audience")
|
||||||
|
|
||||||
|
if len(t.nonce) < 12 {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenFromModel build a decoded view of the raw IPLD data.
|
||||||
|
// This function also serves as validation.
|
||||||
|
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||||
|
var (
|
||||||
|
tkn Token
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
tkn.issuer, err = did.Parse(m.Iss)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse iss: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.audience, err = did.Parse(m.Aud); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse audience: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse policy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Nonce) == 0 {
|
||||||
|
return nil, fmt.Errorf("nonce is required")
|
||||||
|
}
|
||||||
|
tkn.nonce = m.Nonce
|
||||||
|
|
||||||
|
tkn.meta = m.Meta
|
||||||
|
if tkn.meta == nil {
|
||||||
|
tkn.meta = meta.NewMeta()
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn.notBefore, err = parse.OptionalTimestamp(m.Nbf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse notBefore: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse expiration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tkn.validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tkn, nil
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ type Payload struct {
|
|||||||
nonce Bytes
|
nonce Bytes
|
||||||
|
|
||||||
# Arbitrary Metadata
|
# Arbitrary Metadata
|
||||||
meta {String : Any}
|
meta optional {String : Any}
|
||||||
|
|
||||||
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
# "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
|
||||||
nbf optional Int
|
nbf optional Int
|
||||||
222
token/delegation/delegation_test.go
Normal file
222
token/delegation/delegation_test.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package delegation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||||
|
|
||||||
|
subJectCmd = "/foo/bar"
|
||||||
|
subjectPol = `
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
".status",
|
||||||
|
"draft"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"all",
|
||||||
|
".reviewer",
|
||||||
|
[
|
||||||
|
"like",
|
||||||
|
".email",
|
||||||
|
"*@example.com"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"any",
|
||||||
|
".tags",
|
||||||
|
[
|
||||||
|
"or",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
".",
|
||||||
|
"news"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"==",
|
||||||
|
".",
|
||||||
|
"press"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConstructors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cmd, err := command.Parse(subJectCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pol, err := policy.FromDagJson(subjectPol)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
exp, err := time.Parse(time.RFC3339, "2200-01-01T00:00:00Z")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("New", func(t *testing.T) {
|
||||||
|
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, didtest.PersonaCarol.DID(),
|
||||||
|
delegation.WithNonce([]byte(nonce)),
|
||||||
|
delegation.WithExpiration(exp),
|
||||||
|
delegation.WithMeta("foo", "fooo"),
|
||||||
|
delegation.WithMeta("bar", "barr"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
golden.Assert(t, string(data), "new.dagjson")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Root", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
|
delegation.WithNonce([]byte(nonce)),
|
||||||
|
delegation.WithExpiration(exp),
|
||||||
|
delegation.WithMeta("foo", "fooo"),
|
||||||
|
delegation.WithMeta("bar", "barr"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
golden.Assert(t, string(data), "root.dagjson")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Powerline", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
|
delegation.WithNonce([]byte(nonce)),
|
||||||
|
delegation.WithExpiration(exp),
|
||||||
|
delegation.WithMeta("foo", "fooo"),
|
||||||
|
delegation.WithMeta("bar", "barr"),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
golden.Assert(t, string(data), "powerline.dagjson")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptedMeta(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cmd, err := command.Parse(subJectCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
pol, err := policy.FromDagJson(subjectPol)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encryptionKey, err := base64.StdEncoding.DecodeString(aesKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, encryptionKey, 32)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple string",
|
||||||
|
key: "secret1",
|
||||||
|
value: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
key: "secret2",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special characters",
|
||||||
|
key: "secret3",
|
||||||
|
value: "!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode characters",
|
||||||
|
key: "secret4",
|
||||||
|
value: "Hello, 世界! 👋",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
|
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedTkn, _, err := delegation.FromSealed(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = decodedTkn.Meta().GetString(tt.key)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
decrypted, err := decodedTkn.Meta().GetEncryptedString(tt.key, encryptionKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Verify the decrypted value is equal to the original
|
||||||
|
require.Equal(t, tt.value, decrypted)
|
||||||
|
|
||||||
|
// Try to decrypt with wrong key
|
||||||
|
wrongKey := make([]byte, 32)
|
||||||
|
_, err = decodedTkn.Meta().GetEncryptedString(tt.key, wrongKey)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("multiple encrypted values in the same token", func(t *testing.T) {
|
||||||
|
values := map[string]string{
|
||||||
|
"secret1": "value1",
|
||||||
|
"secret2": "value2",
|
||||||
|
"secret3": "value3",
|
||||||
|
}
|
||||||
|
var opts []delegation.Option
|
||||||
|
for k, v := range values {
|
||||||
|
opts = append(opts, delegation.WithEncryptedMetaString(k, v, encryptionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token with multiple encrypted values
|
||||||
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedTkn, _, err := delegation.FromSealed(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
decrypted, err := decodedTkn.Meta().GetEncryptedString(k, encryptionKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, v, decrypted)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
5
token/delegation/delegationtest/README.md
Normal file
5
token/delegation/delegationtest/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# delegationtest
|
||||||
|
|
||||||
|
See the package documentation for instructions on how to use the generated
|
||||||
|
tokens as well as information on how to regenerate the code if changes have
|
||||||
|
been made.
|
||||||
BIN
token/delegation/delegationtest/data/TokenAliceBob.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenAliceBob.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenBobCarol.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenBobCarol.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenCarolDan.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenCarolDan.dagcbor
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user