139 Commits

Author SHA1 Message Date
Michael Muré
dddb67a2b7 Merge pull request #73 from ucan-wg/dlg-constructor
delegation: WIP harmonisation of the constructors, issuer verification
2024-11-26 11:41:59 +01:00
Steve Moyer
60bdc8873b feat(delegation): make Token constructors resemble invocation.New() signature 2024-11-25 15:12:29 -05:00
Michael Muré
d90715d1fe delegation: WIP harmonisation of the constructors, issuer verification 2024-11-20 15:59:13 +01:00
Michael Muré
5f8536e480 Merge pull request #60 from ucan-wg/wip
feat(invocation): add token validation prior to execution
2024-11-20 15:48:43 +01:00
Michael Muré
c19e38356d delegationtest: make the generator a main() 2024-11-20 15:35:33 +01:00
Michael Muré
aea1880386 tests: lots of small asjustement 2024-11-20 14:55:48 +01:00
Michael Muré
2fb5a3dc01 Merge pull request #65 from ucan-wg/minor_improv
various sanding everywhere towards building the tookit
2024-11-20 13:44:26 +01:00
Michael Muré
e980d6c0b9 various sanding everywhere towards building the tookit 2024-11-20 12:34:24 +01:00
Steve Moyer
1098e76cba test(invocation): add command.Covers and subject consistency tests
Also improve the maintainability of the tests by a) providing a set of fixed Personas
and then generating a slew of valid delegation tokens, invalid delegation tokens
and proof-chains thereof.
2024-11-19 14:35:46 -05:00
Michael Muré
bb36d61d93 invocation: rework the validation doc, fix missing invocation time check 2024-11-14 16:45:54 +01:00
Michael Muré
417ef78570 fix(invocation): cleanup proof verification algo 2024-11-14 15:03:49 +01:00
Steve Moyer
00d2380f14 fix(invocation): change verifyProof to chain the Issuer field 2024-11-14 07:21:27 -05:00
Michael Muré
8ca088bf27 Merge pull request #61 from ucan-wg/v1-token-container-versioning
feat(container): versioning
2024-11-14 12:40:07 +01:00
Fabio Bozzo
25ca34923f feat(container): versioning for the CBOR container 2024-11-14 12:38:26 +01:00
Steve Moyer
fc4c8f2de1 fix: issues discovered by invocation validation tests 2024-11-13 12:40:25 -05:00
Michael Muré
64b989452f invocation: split out the delegation chain control 2024-11-13 16:58:48 +01:00
Michael Muré
92065ca0d3 invocation: fix proof documentation 2024-11-13 14:50:59 +01:00
Michael Muré
814cec1495 wip API exploration 2024-11-13 12:47:42 +01:00
Steve Moyer
0f70557309 feat(invocation): validate delegation proof chain 2024-11-13 12:43:30 +01:00
Michael Muré
89e4d5d419 wip API exploration 2024-11-13 12:43:29 +01:00
Michael Muré
9057cbcba6 Merge pull request #59 from ucan-wg/command-covers
command: add Covers() for attenuation test, fix incorrect Segments()
2024-11-13 12:38:24 +01:00
Michael Muré
98d9cadcbd command: fast-path for Covers() 2024-11-12 19:01:02 +01:00
Michael Muré
e938d64220 command: add Covers() for attenuation test, fix incorrect Segments() 2024-11-12 18:42:59 +01:00
Michael Muré
c577d73f3e improve readme 2024-11-12 18:15:45 +01:00
Michael Muré
be185a8496 Merge pull request #58 from ucan-wg/readme
add OK readme
2024-11-12 17:19:49 +01:00
Michael Muré
17a57c622a add OK readme 2024-11-12 16:58:17 +01:00
Fabio Bozzo
6298fa28bd Merge pull request #51 from ucan-wg/v1-meta-encryption
feat(meta): values symmetric encryption
2024-11-12 16:48:04 +01:00
Fabio Bozzo
d3e97aaa08 AddEncrypted adds ciphertext always as bytes 2024-11-12 16:37:53 +01:00
Fabio Bozzo
fdff79d23a split WithEncryptedMeta in options.go by type 2024-11-12 16:07:39 +01:00
Fabio Bozzo
a26d836025 validate non-zero aes key and other refactoring 2024-11-12 16:04:33 +01:00
Fabio Bozzo
9f47418bdf fix merge conflict 2024-11-12 15:31:21 +01:00
Fabio Bozzo
81c7a0f80d Merge branch 'v1' into v1-meta-encryption
# Conflicts:
#	pkg/meta/meta.go
2024-11-12 15:30:54 +01:00
Fabio Bozzo
3987e8649c refactor meta/internal/crypto and add key generation method 2024-11-12 15:29:48 +01:00
Michael Muré
17a1d54b6f Merge pull request #57 from ucan-wg/simplify-args
args: simplify API + code
2024-11-12 15:08:14 +01:00
Fabio Bozzo
7cb0f97b30 Merge branch 'v1' into v1-meta-encryption
Signed-off-by: Fabio Bozzo <fabio.bozzo@gmail.com>
2024-11-12 15:07:19 +01:00
Michael Muré
c4a53f42b6 args,meta: harmonize supported types, with fast paths 2024-11-12 13:09:07 +01:00
Michael Muré
522181b16a args: simplify API + code 2024-11-12 12:14:58 +01:00
Michael Muré
633b3d210a token: move nonce generation to a shared space 2024-11-12 10:38:25 +01:00
Steve Moyer
3c705ca150 Merge pull request #49 from ucan-wg/feat/complete-invocation-stub
feat: complete invocation stub
2024-11-07 13:51:47 -05:00
Steve Moyer
1fa2b5e6fc feat(invocation): use dedicated type for invocation.Token.Arguments 2024-11-07 13:50:20 -05:00
Steve Moyer
11bc085c60 test(policy): update to handel statement returned from Match 2024-11-07 13:17:22 -05:00
Steve Moyer
a4a8634eb8 Merge branch 'v1' into feat/complete-invocation-stub 2024-11-07 13:14:03 -05:00
Steve Moyer
d353dfe652 feat(args): create a specialized type to manage invocation Arguments 2024-11-07 12:58:53 -05:00
Michael Muré
1e5ecdc205 Merge pull request #56 from ucan-wg/match-return
policy: make Match also return the failing statement
2024-11-07 15:40:57 +01:00
Michael Muré
f9065d39d8 policy: make Match also return the failing statement
It's a powerful capability, so let's expose it.

I also found a bug in the process.
2024-11-07 15:33:21 +01:00
Michael Muré
cddade4670 Merge pull request #55 from ucan-wg/flacky-test
literal: fix flacky test
2024-11-07 12:07:43 +01:00
Michael Muré
948087744d literal: fix flacky test
also: make tests less noisy everywhere
2024-11-07 12:01:29 +01:00
Steve Moyer
bfb93d6988 Merge branch 'feat/complete-invocation-stub' of github.com:ucan-wg/go-ucan into feat/complete-invocation-stub 2024-11-06 12:57:13 -05:00
Michael Muré
cfcb199818 meta: prevent overwrite of values 2024-11-06 12:57:04 -05:00
Steve Moyer
85557ab6b5 test(policy): refactor matching test from delegation spec 2024-11-06 12:57:01 -05:00
Steve Moyer
adc2b8d0da test(policy): adds example test case from invocation specification 2024-11-06 12:55:49 -05:00
Michael Muré
bcdaf0cca3 invocation: round of cleanups/fixes 2024-11-06 12:55:45 -05:00
Steve Moyer
d754c5837b test(invocation): adds schema round-trip test 2024-11-06 12:51:24 -05:00
Steve Moyer
d89fb395e3 feat(invocation): ipld unseal to invocation 2024-11-06 12:48:41 -05:00
Steve Moyer
4932e32052 feat(invocation): produce example output similar to spec 2024-11-06 12:44:48 -05:00
Steve Moyer
a52b48cf47 feat(invocation): provide New constructor and encoding to wire-format 2024-11-06 12:41:36 -05:00
Steve Moyer
e6e4d85381 Merge branch 'feat/complete-invocation-stub' of github.com:ucan-wg/go-ucan into feat/complete-invocation-stub 2024-11-06 12:22:19 -05:00
Steve Moyer
962e897ff5 Otest(policy): refactor matching test from delegation spec 2024-11-06 12:18:15 -05:00
Steve Moyer
58bb5cdb8f test(policy): adds example test case from invocation specification 2024-11-06 12:16:49 -05:00
Steve Moyer
ce7f653ab0 chore(invocation): clean up left-over (and unneeded) conversion of prf 2024-11-06 12:16:49 -05:00
Michael Muré
7d4f973171 invocation: round of cleanups/fixes 2024-11-06 12:16:48 -05:00
Steve Moyer
3dc0011628 docs(invocation): edit (and finish) Go docs for exported types 2024-11-06 12:16:47 -05:00
Steve Moyer
08f821f23d test(invocation): adds schema round-trip test 2024-11-06 12:16:46 -05:00
Steve Moyer
1b61f2e4db docs(invocation): fix truncated WithEmptyNonce description 2024-11-06 12:16:45 -05:00
Steve Moyer
187e7a869c feat(invocation): ipld unseal to invocation 2024-11-06 12:16:44 -05:00
Steve Moyer
a98653b769 feat(invocation): produce example output similar to spec 2024-11-06 12:16:43 -05:00
Steve Moyer
31d16ac468 feat(invocation): provide New constructor and encoding to wire-format 2024-11-06 12:16:42 -05:00
Michael Muré
d2b004c405 meta: prevent overwrite of values 2024-11-06 18:06:46 +01:00
Michael Muré
884d63a689 Merge pull request #53 from ucan-wg/test-literal
literal: add test suite
2024-11-06 16:45:19 +01:00
Michael Muré
c9f3a6033a delegation: minor fix around meta 2024-11-06 16:43:57 +01:00
Michael Muré
8447499c5a literal: add test suite 2024-11-06 16:42:45 +01:00
Steve Moyer
b4e222f8a0 test(policy): refactor matching test from delegation spec 2024-11-06 10:17:18 -05:00
Michael Muré
41b8600fbc Merge pull request #52 from ucan-wg/meta-readonly
meta: make a read-only version to enforce token immutability
2024-11-06 15:28:19 +01:00
Steve Moyer
824c8fe523 test(policy): adds example test case from invocation specification 2024-11-06 09:25:51 -05:00
Michael Muré
6aeb6a8b70 meta: make a read-only version to enforce token immutability 2024-11-06 15:17:35 +01:00
Steve Moyer
a1aaf47d7c chore(invocation): clean up left-over (and unneeded) conversion of prf 2024-11-05 15:28:31 -05:00
Michael Muré
728696f169 invocation: round of cleanups/fixes 2024-11-05 17:39:39 +01:00
Michael Muré
cfb4446a05 Merge pull request #48 from ucan-wg/pol-partial
policy: implement partial matching, to evaluate in multiple steps with fail early
2024-11-05 16:31:52 +01:00
Michael Muré
06a72868a5 container: add a delegation iterator 2024-11-05 16:26:53 +01:00
Michael Muré
6f9a6fa5c1 literal: make Map and List generic, to avoid requiring conversions 2024-11-05 16:26:14 +01:00
Steve Moyer
f2b4c3ac20 docs(invocation): edit (and finish) Go docs for exported types 2024-11-05 09:35:55 -05:00
Steve Moyer
7a7db684c3 test(invocation): adds schema round-trip test 2024-11-05 08:39:43 -05:00
Steve Moyer
d7454156d2 docs(invocation): fix truncated WithEmptyNonce description 2024-11-05 07:39:51 -05:00
Steve Moyer
d3ad6715d9 feat(invocation): ipld unseal to invocation 2024-11-04 16:07:11 -05:00
Fabio Bozzo
602bdf9c7a fix broken meta_test.gotest 2024-11-04 19:15:34 +01:00
Fabio Bozzo
d21c17c4ca address pr remarks 2024-11-04 19:11:25 +01:00
Michael Muré
72f4ef7b5e policy: fix incorrect test for PartialMatch 2024-11-04 19:07:36 +01:00
Fabio Bozzo
02be4010d6 add array quantifiers tests and tiny fix 2024-11-04 18:50:30 +01:00
Michael Muré
61e031529f policy: use "any" 2024-11-04 18:41:18 +01:00
Michael Muré
19721027e4 literal: rewrite Map() to cover more types 2024-11-04 18:34:31 +01:00
Fabio Bozzo
bc847ee027 fix literal.Map to handle list values too 2024-11-04 17:10:57 +01:00
Fabio Bozzo
5bfe430934 add test cases for missing optional values for all operators 2024-11-04 17:07:32 +01:00
Fabio Bozzo
10b5e1e603 add test cases for optional, like pattern, nested policy 2024-11-04 13:04:54 +01:00
Michael Muré
3cf1de6b67 policy: fix distrinction between "no data" and "optional not data" 2024-11-04 11:15:32 +01:00
Michael Muré
400f689a85 literal: some better typing 2024-11-04 11:15:12 +01:00
Fabio Bozzo
6717a3a89c refactor: simplify optional selector handling
Let Select() handle optional selectors by checking its nil return value, rather than explicitly checking IsOptional()
Applied this pattern consistently across all statement kinds (Equal, Like, All, Any, etc)
2024-11-04 10:56:06 +01:00
Fabio Bozzo
9e9c632ded revert typo 2024-11-01 17:47:47 +01:00
Fabio Bozzo
b210c69173 tests for partial match 2024-11-01 17:43:55 +01:00
Fabio Bozzo
6d85b2ba3c additional tests for optional selectors 2024-11-01 13:07:46 +01:00
Fabio Bozzo
76c015e78b feat(meta): values symmetric encryption 2024-10-31 18:24:54 +01:00
Steve Moyer
fcb527cc52 Merge pull request #50 from ucan-wg/fix/meta-optional-in-delegation
fix: change meta optional in `delegation` `Token`, model and schema
2024-10-24 18:10:30 -04:00
Steve Moyer
89f648a94e docs(did): re-fix typo RE coercion of secp256k1 public keys 2024-10-24 13:44:32 -04:00
Steve Moyer
e1d771333c fix(delegation): meta is optional 2024-10-24 13:43:52 -04:00
Steve Moyer
f44cf8af78 feat(invocation): produce example output similar to spec 2024-10-24 12:59:38 -04:00
Steve Moyer
2b2fc4a13f Merge branch 'v1' into feat/complete-invocation-stub 2024-10-24 11:11:21 -04:00
Michael Muré
6b72799818 Merge pull request #41 from ucan-wg/streamline-did
did: simplify public API, add missing required algorithms
2024-10-24 17:09:10 +02:00
Steve Moyer
ccc85d4697 docs(did): correct typos 2024-10-24 10:59:27 -04:00
Steve Moyer
0d63e90b67 docs(did): add comment explaining why some ECDSA public keys are "coerced" to Secp256k1 public keys 2024-10-24 10:54:43 -04:00
Steve Moyer
d784c92c29 feat(invocation): provide New constructor and encoding to wire-format 2024-10-24 10:44:38 -04:00
Michael Muré
7662fe34db policy: implement partial matching, to evaluate in multiple steps with fail early 2024-10-24 16:21:57 +02:00
Michael Muré
6d0fbd4d5a minor cleanups 2024-10-24 14:46:01 +02:00
Michael Muré
e76354fb0a Merge pull request #47 from ucan-wg/simplify-literal
literal: simplify package with built-in functions
2024-10-24 14:33:33 +02:00
Michael Muré
deaf9c4fe9 Merge pull request #46 from ucan-wg/rework-policies
selector: rework to match the spec, cleanup lots of edge cases
2024-10-24 13:01:10 +02:00
Michael Muré
a1c2c5c067 literal: simplify package with built-in functions 2024-10-24 12:55:06 +02:00
Michael Muré
2c58fedfd5 did: last cleanups 2024-10-24 12:51:21 +02:00
Fabio Bozzo
2ea9f8c93b clamp start idx to length for out of bound 2024-10-24 12:44:25 +02:00
Fabio Bozzo
00ff88ef23 add slicing/indexing for bytes kind 2024-10-24 12:27:01 +02:00
Michael Muré
2ffdf004ac test(did): split the test vector reading code in a separate file/package
tests are much cleaner and explicit now
2024-10-24 12:03:23 +02:00
Michael Muré
a8780f750c policy: remove remnant of policy matching, that concept doesn't really work with complex policies
Maybe it will be revived later.
2024-10-24 11:17:38 +02:00
Michael Muré
c70f68b886 selector: remove incorrect tests
https://github.com/ucan-wg/delegation/issues/5#issuecomment-2434713564

My understanding is that a few of those example are now incorrect/obsolete:

String Index: as per @expede, indexing on strings is now disallowed, to match jq
Optional Iterator, .[][]?: the spec says that iterator on arrays are no-op. It makes sense imho, as jq output multiple values with that, which we can't.
Nested Iterator, .[][]: same reason
2024-10-24 11:11:25 +02:00
Michael Muré
a27eb258e5 selector: remove remnant of policy matching, that concept doesn't really work with complex policies
Maybe it will be revived later.
2024-10-24 11:07:00 +02:00
Fabio Bozzo
866683347f disable support for string indexing 2024-10-23 18:25:16 +02:00
Fabio Bozzo
2fafbe7bf3 enable string indexing/slicing and new selector tests 2024-10-23 17:01:00 +02:00
Fabio Bozzo
1728bf29b8 add more parsing tests 2024-10-23 16:37:35 +02:00
Michael Muré
8fac97b7e7 fix some edge cases:
- slicing is start:end, not start:length, but importantly start to *excluded* end
- backward slicing is illegal
- slicing on string is allowed
- slicing on strings operates on runes, not bytes
2024-10-23 12:18:03 +02:00
Michael Muré
7ad940844c selector: disallow backward slicing 2024-10-23 11:31:41 +02:00
Fabio Bozzo
52ae2eaf60 fix inconsistent test expectations 2024-10-23 11:25:07 +02:00
Michael Muré
570bcdcb6c policy: comment out "filtering" of policies, concept that doesn't really work 2024-10-23 11:25:07 +02:00
Michael Muré
5abb870462 policy: follow the changes in selector, operate on a single returned node 2024-10-23 11:25:07 +02:00
Fabio Bozzo
4ec675861d propose fix for inconsistent test expectations 2024-10-22 17:33:28 +02:00
Fabio Bozzo
b941b507e0 fix optional identity parsing (no idempotent) 2024-10-22 14:13:12 +02:00
Michael Muré
e66beb662e selector: rework to match the spec, cleanup lots of edge cases
- removed field selection against a list
- a selector return exactly one node, or nil
- implemented the full set of slicing, including negative indexes
- plenty of other cleanup and simplification
2024-10-22 13:47:53 +02:00
Steve Moyer
87e25090bb test(did): verifies that ECDSA keys with the secp256k1 curve are "coerced" into crypto.Secp256k1 keys 2024-10-21 15:29:51 -04:00
Steve Moyer
6011f0740a fix(did): finish the GenerateECDSAWithCurve function 2024-10-21 11:03:43 -04:00
Steve Moyer
2bd177ce4d fix(did): correct function names for key/DID generators 2024-10-21 10:59:27 -04:00
Steve Moyer
abda49061d test(did): add test vectors from did:key specification 2024-10-17 15:19:49 -04:00
Steve Moyer
fb978ee574 feat(did): strengthen DID crypto 2024-10-17 15:18:31 -04:00
Steve Moyer
da1310b78a feat(did): strengthens crypto for public key handliing 2024-10-17 07:42:40 -04:00
Michael Muré
f8b5fa3a32 did: simplify public API, add missing required algorithm 2024-10-15 12:20:35 +02:00
98 changed files with 7457 additions and 1764 deletions

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

31
did/README.md Normal file
View 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",
```

View File

@@ -1,43 +1,171 @@
package did package did
import ( import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"errors" "errors"
"fmt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
crypto "github.com/libp2p/go-libp2p/core/crypto" crypto "github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/crypto/pb" "github.com/libp2p/go-libp2p/core/crypto/pb"
"github.com/multiformats/go-multicodec" "github.com/multiformats/go-multicodec"
"github.com/multiformats/go-varint" "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) { func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
return FromPubKey(privKey.GetPublic()) return FromPubKey(privKey.GetPublic())
} }
// FromPubKey returns a did:key constructed from the provided public key.
func FromPubKey(pubKey crypto.PubKey) (DID, error) { func FromPubKey(pubKey crypto.PubKey) (DID, error) {
code, ok := map[pb.KeyType]multicodec.Code{ var code multicodec.Code
pb.KeyType_Ed25519: multicodec.Ed25519Pub,
pb.KeyType_RSA: multicodec.RsaPub, switch pubKey.Type() {
pb.KeyType_Secp256k1: multicodec.Secp256k1Pub, case pb.KeyType_Ed25519:
pb.KeyType_ECDSA: multicodec.Es256, code = multicodec.Ed25519Pub
}[pubKey.Type()] case pb.KeyType_RSA:
if !ok { code = RSA
return Undef, errors.New("Blah") 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")
} }
buf := varint.ToUvarint(uint64(code)) if pubKey.Type() == pb.KeyType_ECDSA && code == Secp256k1 {
var err error
pubBytes, err := pubKey.Raw() pubKey, err = coerceECDSAToSecp256k1(pubKey)
if err != nil { if err != nil {
return Undef, err 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{ return DID{
str: string(append(buf, pubBytes...)), code: code,
code: uint64(code), bytes: string(append(varint.ToUvarint(uint64(code)), bytes...)),
key: true,
}, nil }, nil
} }
// ToPubKey returns the crypto.PubKey encapsulated in the DID formed by
// parsing the provided string.
func ToPubKey(s string) (crypto.PubKey, error) { func ToPubKey(s string) (crypto.PubKey, error) {
id, err := Parse(s) id, err := Parse(s)
if err != nil { if err != nil {
@@ -46,3 +174,58 @@ func ToPubKey(s string) (crypto.PubKey, error) {
return id.PubKey() 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
}

View File

@@ -1,10 +1,17 @@
package did_test package did_test
import ( import (
"crypto/elliptic"
"crypto/rand"
"testing" "testing"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/libp2p/go-libp2p/core/crypto" "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/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/did"
) )
@@ -16,9 +23,59 @@ const (
func TestFromPubKey(t *testing.T) { func TestFromPubKey(t *testing.T) {
t.Parallel() t.Parallel()
id, err := did.FromPubKey(examplePubKey(t)) _, ecdsaP256, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P256(), rand.Reader)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, exampleDID(t), id) _, 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) { func TestToPubKey(t *testing.T) {

View File

@@ -1,6 +1,9 @@
package did package did
import ( import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/x509"
"fmt" "fmt"
"strings" "strings"
@@ -10,113 +13,61 @@ import (
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:"
const DIDCore = 0x0d1d
const Ed25519 = 0xed
const RSA = uint64(multicodec.RsaPub)
var MethodOffset = varint.UvarintSize(uint64(DIDCore))
// //
// [did:key format]: https://w3c-ccg.github.io/did-method-key/ // [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
type DID struct { const (
key bool X25519 = multicodec.X25519Pub
code uint64 Ed25519 = multicodec.Ed25519Pub // UCAN required/recommended
str string P256 = multicodec.P256Pub // UCAN required
} P384 = multicodec.P384Pub
P521 = multicodec.P521Pub
Secp256k1 = multicodec.Secp256k1Pub // UCAN required
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.
return []byte(d.str) func Parse(str string) (DID, error) {
} const keyPrefix = "did:key:"
func (d DID) Code() uint64 { if !strings.HasPrefix(str, keyPrefix) {
return d.code return Undef, fmt.Errorf("must start with 'did:key'")
}
func (d DID) DID() DID {
return d
}
func (d DID) Key() bool {
return d.key
}
func (d DID) PubKey() (crypto.PubKey, error) {
if !d.key {
return nil, fmt.Errorf("unsupported did type: %s", d.String())
} }
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{ baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):])
multicodec.Ed25519Pub: crypto.UnmarshalEd25519PublicKey, if err != nil {
multicodec.RsaPub: crypto.UnmarshalRsaPublicKey, return Undef, err
multicodec.Secp256k1Pub: crypto.UnmarshalSecp256k1PublicKey,
multicodec.Es256: crypto.UnmarshalECDSAPublicKey,
}[multicodec.Code(d.code)]
if !ok {
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
} }
if baseCodec != mbase.Base58BTC {
return unmarshaler(d.Bytes()[varint.UvarintSize(d.code):]) return Undef, fmt.Errorf("not Base58BTC encoded")
}
// String formats the decentralized identity document (DID) as a string.
func (d DID) String() string {
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 || code == RSA { switch multicodec.Code(code) {
return DID{str: string(bytes), code: code, 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) {
if !strings.HasPrefix(str, Prefix) {
return Undef, fmt.Errorf("must start with 'did:'")
}
if strings.HasPrefix(str, KeyPrefix) {
code, bytes, err := mbase.Decode(str[len(KeyPrefix):])
if err != nil {
return Undef, err
}
if code != mbase.Base58BTC {
return Undef, fmt.Errorf("not Base58BTC encoded")
}
return Decode(bytes)
}
buf := make([]byte, MethodOffset)
varint.PutUvarint(buf, DIDCore)
suffix, _ := strings.CutPrefix(str, Prefix)
buf = append(buf, suffix...)
return DID{str: string(buf), code: DIDCore}, nil
} }
// MustParse is like Parse but panics instead of returning an error.
func MustParse(str string) DID { func MustParse(str string) DID {
did, err := Parse(str) did, err := Parse(str)
if err != nil { if err != nil {
@@ -124,3 +75,66 @@ func MustParse(str string) DID {
} }
return did 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)
}
codeSize := varint.UvarintSize(uint64(d.code))
return unmarshaler([]byte(d.bytes)[codeSize:])
}
// String formats the decentralized identity document (DID) as a string.
func (d DID) String() string {
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 {
return nil, err
}
return crypto.UnmarshalECDSAPublicKey(pkix)
}
}
func rsaPubKeyUnmarshaller(data []byte) (crypto.PubKey, error) {
rsaPublicKey, err := x509.ParsePKCS1PublicKey(data)
if err != nil {
return nil, err
}
pkix, err := x509.MarshalPKIXPublicKey(rsaPublicKey)
if err != nil {
return nil, err
}
return crypto.UnmarshalRsaPublicKey(pkix)
}

View File

@@ -9,12 +9,8 @@ import (
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 TestMustParseDIDKey(t *testing.T) { func TestMustParseDIDKey(t *testing.T) {
@@ -29,65 +25,17 @@ func TestMustParseDIDKey(t *testing.T) {
}) })
} }
func TestDecodeDIDKey(t *testing.T) {
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
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 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")
}
} }

127
did/didtest/crypto.go Normal file
View File

@@ -0,0 +1,127 @@
// 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"
"testing"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require"
"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
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 {
return privKeys[p]
}
// 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(t *testing.T) string {
pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey().GetPublic())
require.NoError(t, 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,
}
}

82
did/key_spec_test.go Normal file
View 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
}

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

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

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

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

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

13
go.mod
View File

@@ -3,9 +3,13 @@ module github.com/ucan-wg/go-ucan
go 1.23 go 1.23
require ( require (
github.com/dave/jennifer v1.7.1
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/lestrrat-go/jwx/v2 v2.1.1
github.com/libp2p/go-libp2p v0.36.3 github.com/libp2p/go-libp2p v0.36.3
github.com/mr-tron/base58 v1.2.0
github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multibase v0.2.0
github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-multicodec v0.9.0
github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-multihash v0.2.3
@@ -16,15 +20,20 @@ require (
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-cmp v0.5.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // 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/minio/sha256-simd v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.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.25.0 // indirect golang.org/x/crypto v0.25.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.22.0 // indirect

23
go.sum
View File

@@ -1,5 +1,8 @@
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/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
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 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
@@ -9,6 +12,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3
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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
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=
@@ -25,6 +30,18 @@ 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/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
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 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 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 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
@@ -54,6 +71,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=
@@ -61,6 +80,9 @@ 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.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=
@@ -86,6 +108,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
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=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=

137
pkg/args/args.go Normal file
View File

@@ -0,0 +1,137 @@
// 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 (
"fmt"
"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/literal"
)
// 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{},
}
}
// 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
}
a.Values[key] = node
a.Keys = append(a.Keys, key)
return nil
}
// 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 *Args) {
for _, key := range other.Keys {
if _, ok := a.Values[key]; ok {
// don't overwrite
continue
}
a.Values[key] = other.Values[key]
a.Keys = append(a.Keys, key)
}
}
// 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
}

179
pkg/args/args_test.go Normal file
View File

@@ -0,0 +1,179 @@
package args_test
import (
"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/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/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()))
}
const (
argsSchema = "type Args { String : Any }"
argsName = "Args"
)
var (
once sync.Once
ts *schema.TypeSystem
err error
)
func argsType() schema.Type {
once.Do(func() {
ts, err = ipld.LoadSchemaBytes([]byte(argsSchema))
})
if err != nil {
panic(err)
}
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
}

23
pkg/args/readonly.go Normal file
View File

@@ -0,0 +1,23 @@
package args
import "github.com/ipld/go-ipld-prime"
type ReadOnly struct {
args *Args
}
func (r ReadOnly) ToIPLD() (ipld.Node, error) {
return r.args.ToIPLD()
}
func (r ReadOnly) Equals(other *Args) bool {
return r.args.Equals(other)
}
func (r ReadOnly) String() string {
return r.args.String()
}
func (r ReadOnly) WriteableClone() *Args {
return r.args.Clone()
}

View File

@@ -98,7 +98,34 @@ func (c Command) Join(segments ...string) Command {
// Segments returns the ordered segments that comprise the Command as a // Segments returns the ordered segments that comprise the Command as a
// slice of strings. // slice of strings.
func (c Command) Segments() []string { func (c Command) Segments() []string {
return strings.Split(string(c), separator) 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 // String returns the composed representation the command. This is also

View File

@@ -73,6 +73,21 @@ func TestJoin(t *testing.T) {
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

View File

@@ -40,6 +40,13 @@ func TestCarRoundTrip(t *testing.T) {
} }
func FuzzCarRoundTrip(f *testing.F) { 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") example, err := os.ReadFile("testdata/sample-v1.car")
require.NoError(f, err) require.NoError(f, err)

View File

@@ -2,8 +2,10 @@ package container
import ( import (
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"iter"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
@@ -33,13 +35,29 @@ func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type. // GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) { func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
tkn, err := ctn.GetToken(cid) tkn, err := ctn.GetToken(cid)
if errors.Is(err, ErrNotFound) {
return nil, delegation.ErrDelegationNotFound
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
if tkn, ok := tkn.(*delegation.Token); ok { if tkn, ok := tkn.(*delegation.Token); ok {
return tkn, nil return tkn, nil
} }
return nil, fmt.Errorf("not a delegation token") 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 the first found invocation.Token. // GetInvocation returns the first found invocation.Token.
@@ -84,15 +102,36 @@ func FromCbor(r io.Reader) (Reader, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if n.Kind() != datamodel.Kind_List { if n.Kind() != datamodel.Kind_Map {
return nil, fmt.Errorf("not a list") return nil, fmt.Errorf("invalid container format: expected map")
}
if n.Length() != 1 {
return nil, fmt.Errorf("invalid container format: expected single version key")
} }
ctn := make(Reader, n.Length()) // get the first (and only) key-value pair
it := n.MapIterator()
key, tokensNode, err := it.Next()
if err != nil {
return nil, err
}
it := n.ListIterator() version, err := key.AsString()
for !it.Done() { if err != nil {
_, val, err := it.Next() return nil, fmt.Errorf("invalid container format: version must be string")
}
if version != currentContainerVersion {
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 { if err != nil {
return nil, err return nil, err
} }

View File

@@ -130,12 +130,6 @@ func BenchmarkContainerSerialisation(b *testing.B) {
} }
} }
func randBytes(n int) []byte {
b := make([]byte, n)
_, _ = rand.Read(b)
return b
}
func randDID() (crypto.PrivKey, did.DID) { func randDID() (crypto.PrivKey, did.DID) {
privKey, _, err := crypto.GenerateEd25519Key(rand.Reader) privKey, _, err := crypto.GenerateEd25519Key(rand.Reader)
if err != nil { if err != nil {
@@ -150,7 +144,7 @@ func randDID() (crypto.PrivKey, did.DID) {
func randomString(length int) string { func randomString(length int) string {
b := make([]byte, length/2+1) b := make([]byte, length/2+1)
rand.Read(b) _, _ = rand.Read(b)
return fmt.Sprintf("%x", b)[0:length] return fmt.Sprintf("%x", b)[0:length]
} }
@@ -172,7 +166,7 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10))) opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
} }
t, err := delegation.New(priv, aud, cmd, pol, opts...) t, err := delegation.New(iss, aud, cmd, pol, opts...)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -182,3 +176,30 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
} }
return t, c, b 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++ {
_, c, data := randToken()
writer.AddSealed(c, data)
}
buf := bytes.NewBuffer(nil)
err := writer.ToCbor(buf)
require.NoError(f, err)
f.Add(buf.Bytes())
}
f.Fuzz(func(t *testing.T, data []byte) {
start := time.Now()
// search for panics
_, _ = FromCbor(bytes.NewReader(data))
if time.Since(start) > 100*time.Millisecond {
panic("too long")
}
})
}

View File

@@ -14,6 +14,8 @@ import (
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-( // TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
const currentContainerVersion = "ctn-v1"
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together. // Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
type Writer map[cid.Cid][]byte type Writer map[cid.Cid][]byte
@@ -43,10 +45,12 @@ func (ctn Writer) ToCarBase64(w io.Writer) error {
} }
func (ctn Writer) ToCbor(w io.Writer) error { func (ctn Writer) ToCbor(w io.Writer) error {
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) { node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
for _, bytes := range ctn { qp.MapEntry(ma, currentContainerVersion, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
qp.ListEntry(la, qp.Bytes(bytes)) for _, bytes := range ctn {
} qp.ListEntry(la, qp.Bytes(bytes))
}
}))
}) })
if err != nil { if err != nil {
return err return err

View File

@@ -0,0 +1,132 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"fmt"
"io"
)
// KeySize represents valid AES key sizes
type KeySize int
const (
KeySize128 KeySize = 16 // AES-128
KeySize192 KeySize = 24 // AES-192
KeySize256 KeySize = 32 // AES-256 (recommended)
)
// IsValid returns true if the key size is valid for AES
func (ks KeySize) IsValid() bool {
switch ks {
case KeySize128, KeySize192, KeySize256:
return true
default:
return false
}
}
var ErrShortCipherText = errors.New("ciphertext too short")
var ErrNoEncryptionKey = errors.New("encryption key is required")
var ErrInvalidKeySize = errors.New("invalid key size: must be 16, 24, or 32 bytes")
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
// GenerateKey generates a random AES key of default size KeySize256 (32 bytes).
// Returns an error if the specified size is invalid or if key generation fails.
func GenerateKey() ([]byte, error) {
return GenerateKeyWithSize(KeySize256)
}
// GenerateKeyWithSize generates a random AES key of the specified size.
// Returns an error if the specified size is invalid or if key generation fails.
func GenerateKeyWithSize(size KeySize) ([]byte, error) {
if !size.IsValid() {
return nil, ErrInvalidKeySize
}
key := make([]byte, size)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, fmt.Errorf("failed to generate AES key: %w", err)
}
return key, nil
}
// EncryptWithAESKey encrypts data using AES-GCM with the provided key.
// The key must be 16, 24, or 32 bytes long (for AES-128, AES-192, or AES-256).
// Returns the encrypted data with the nonce prepended, or an error if encryption fails.
func EncryptWithAESKey(data, key []byte) ([]byte, error) {
if err := validateAESKey(key); err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, data, nil), nil
}
// DecryptStringWithAESKey decrypts data that was encrypted with EncryptWithAESKey.
// The key must match the one used for encryption.
// Expects the input to have a prepended nonce.
// Returns the decrypted data or an error if decryption fails.
func DecryptStringWithAESKey(data, key []byte) ([]byte, error) {
if err := validateAESKey(key); err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(data) < gcm.NonceSize() {
return nil, ErrShortCipherText
}
nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return decrypted, nil
}
func validateAESKey(key []byte) error {
if key == nil {
return ErrNoEncryptionKey
}
if !KeySize(len(key)).IsValid() {
return ErrInvalidKeySize
}
// check if key is all zeros
for _, b := range key {
if b != 0 {
return nil
}
}
return ErrZeroKey
}

View File

@@ -0,0 +1,124 @@
package crypto
import (
"bytes"
"crypto/rand"
"testing"
"github.com/stretchr/testify/require"
)
func TestAESEncryption(t *testing.T) {
t.Parallel()
key := make([]byte, 32) // generated 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, 31),
wantErr: ErrInvalidKeySize,
},
{
name: "zero key returns error",
data: []byte("hello world"),
key: make([]byte, 32),
wantErr: ErrZeroKey,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
encrypted, err := EncryptWithAESKey(tt.data, tt.key)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
decrypted, err := DecryptStringWithAESKey(encrypted, tt.key)
require.NoError(t, err)
if tt.key == nil {
require.Equal(t, tt.data, encrypted)
require.Equal(t, tt.data, decrypted)
} else {
require.NotEqual(t, tt.data, encrypted)
require.True(t, bytes.Equal(tt.data, decrypted))
}
})
}
}
func TestDecryptionErrors(t *testing.T) {
t.Parallel()
key := make([]byte, 32)
_, err := rand.Read(key)
require.NoError(t, err)
tests := []struct {
name string
data []byte
key []byte
errMsg string
}{
{
name: "short ciphertext",
data: []byte("short"),
key: key,
errMsg: "ciphertext too short",
},
{
name: "invalid ciphertext",
data: make([]byte, 16), // just nonce size
key: key,
errMsg: "message authentication failed",
},
{
name: "missing key",
data: []byte("<22>`M<><4D><EFBFBD>l\u001AIF<49>\u0012<31><32><EFBFBD>=h<>?<3F>c<EFBFBD> <20><>\u0012<31><32><EFBFBD><EFBFBD>\u001C<31>\u0018Ƽ(g"),
key: nil,
errMsg: "encryption key is required",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
_, err := DecryptStringWithAESKey(tt.data, tt.key)
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMsg)
})
}
}

View File

@@ -3,23 +3,26 @@ package meta
import ( import (
"errors" "errors"
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/printer" "github.com/ipld/go-ipld-prime/printer"
"github.com/ucan-wg/go-ucan/pkg/meta/internal/crypto"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
) )
var ErrUnsupported = errors.New("failure adding unsupported type to meta") var ErrNotFound = errors.New("key not found in meta")
var ErrNotFound = errors.New("key-value 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. // 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, // This also serves as a way to construct the underlying IPLD data with minimum allocations
// while hiding the IPLD complexity from the caller. // and transformations, while hiding the IPLD complexity from the caller.
type Meta struct { type Meta struct {
// This type must be compatible with the IPLD type represented by the IPLD
// schema { String : Any }.
Keys []string Keys []string
Values map[string]ipld.Node Values map[string]ipld.Node
} }
@@ -51,6 +54,21 @@ func (m *Meta) GetString(key string) (string, error) {
return v.AsString() 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 := crypto.DecryptStringWithAESKey(v, encryptionKey)
if err != nil {
return "", err
}
return string(decrypted), nil
}
// GetInt64 retrieves a value as an int64. // GetInt64 retrieves a value as an int64.
// Returns ErrNotFound if the given key is missing. // Returns ErrNotFound if the given key is missing.
// Returns datamodel.ErrWrongKind if the value has the wrong type. // Returns datamodel.ErrWrongKind if the value has the wrong type.
@@ -84,6 +102,21 @@ func (m *Meta) GetBytes(key string) ([]byte, error) {
return v.AsBytes() 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 := crypto.DecryptStringWithAESKey(v, encryptionKey)
if err != nil {
return nil, err
}
return decrypted, nil
}
// GetNode retrieves a value as a raw IPLD node. // GetNode retrieves a value as a raw IPLD node.
// Returns ErrNotFound if the given key is missing. // Returns ErrNotFound if the given key is missing.
// Returns datamodel.ErrWrongKind if the value has the wrong type. // Returns datamodel.ErrWrongKind if the value has the wrong type.
@@ -96,35 +129,48 @@ func (m *Meta) GetNode(key string) (ipld.Node, error) {
} }
// Add adds a key/value pair in the meta set. // Add adds a key/value pair in the meta set.
// Accepted types for the value are: bool, string, int, int32, int64, []byte, // Accepted types for val are any CBOR compatible type, or directly IPLD values.
// and ipld.Node.
func (m *Meta) Add(key string, val any) error { func (m *Meta) Add(key string, val any) error {
switch val := val.(type) { if _, ok := m.Values[key]; ok {
case bool: return fmt.Errorf("duplicate key %q", key)
m.Values[key] = basicnode.NewBool(val)
case string:
m.Values[key] = basicnode.NewString(val)
case int:
m.Values[key] = basicnode.NewInt(int64(val))
case int32:
m.Values[key] = basicnode.NewInt(int64(val))
case int64:
m.Values[key] = basicnode.NewInt(val)
case float32:
m.Values[key] = basicnode.NewFloat(float64(val))
case float64:
m.Values[key] = basicnode.NewFloat(val)
case []byte:
m.Values[key] = basicnode.NewBytes(val)
case datamodel.Node:
m.Values[key] = val
default:
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
} }
node, err := literal.Any(val)
if err != nil {
return err
}
m.Keys = append(m.Keys, key) m.Keys = append(m.Keys, key)
m.Values[key] = node
return nil 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.
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 = crypto.EncryptWithAESKey([]byte(val), encryptionKey)
if err != nil {
return err
}
case []byte:
encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey)
if err != nil {
return err
}
default:
return ErrNotEncryptable
}
return m.Add(key, encrypted)
}
// Equals tells if two Meta hold the same key/values. // Equals tells if two Meta hold the same key/values.
func (m *Meta) Equals(other *Meta) bool { func (m *Meta) Equals(other *Meta) bool {
if len(m.Keys) != len(other.Keys) { if len(m.Keys) != len(other.Keys) {
@@ -145,29 +191,23 @@ func (m *Meta) String() string {
buf := strings.Builder{} buf := strings.Builder{}
buf.WriteString("{") buf.WriteString("{")
var i int
for key, node := range m.Values { for key, node := range m.Values {
if i > 0 { buf.WriteString("\n\t")
buf.WriteString(", ")
}
i++
buf.WriteString(key) buf.WriteString(key)
buf.WriteString(":") buf.WriteString(": ")
buf.WriteString(printer.Sprint(node)) buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t"))
buf.WriteString(",")
} }
if len(m.Values) > 0 {
buf.WriteString("\n")
}
buf.WriteString("}") buf.WriteString("}")
return buf.String() return buf.String()
} }
func fqtn(val any) string { // ReadOnly returns a read-only version of Meta.
var name string func (m *Meta) ReadOnly() ReadOnly {
return ReadOnly{m: m}
t := reflect.TypeOf(val)
for t.Kind() == reflect.Pointer {
name += "*"
t = t.Elem()
}
return name + t.PkgPath() + "." + t.Name()
} }

View File

@@ -1,11 +1,12 @@
package meta_test package meta_test
import ( import (
"crypto/rand"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/meta" "github.com/ucan-wg/go-ucan/pkg/meta"
"gotest.tools/v3/assert"
) )
func TestMeta_Add(t *testing.T) { func TestMeta_Add(t *testing.T) {
@@ -13,11 +14,64 @@ func TestMeta_Add(t *testing.T) {
type Unsupported struct{} type Unsupported struct{}
t.Run("error if not primative or Node", func(t *testing.T) { t.Run("error if not primitive or Node", func(t *testing.T) {
t.Parallel() t.Parallel()
err := (&meta.Meta{}).Add("invalid", &Unsupported{}) err := (&meta.Meta{}).Add("invalid", &Unsupported{})
require.ErrorIs(t, err, meta.ErrUnsupported) require.Error(t, err)
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported") })
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")
})
}) })
} }

50
pkg/meta/readonly.go Normal file
View File

@@ -0,0 +1,50 @@
package meta
import (
"github.com/ipld/go-ipld-prime"
)
// ReadOnly wraps a Meta into a read-only facade.
type ReadOnly struct {
m *Meta
}
func (r ReadOnly) GetBool(key string) (bool, error) {
return r.m.GetBool(key)
}
func (r ReadOnly) GetString(key string) (string, error) {
return r.m.GetString(key)
}
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
return r.m.GetEncryptedString(key, encryptionKey)
}
func (r ReadOnly) GetInt64(key string) (int64, error) {
return r.m.GetInt64(key)
}
func (r ReadOnly) GetFloat64(key string) (float64, error) {
return r.m.GetFloat64(key)
}
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
return r.m.GetBytes(key)
}
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
return r.m.GetEncryptedBytes(key, encryptionKey)
}
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
return r.m.GetNode(key)
}
func (r ReadOnly) Equals(other ReadOnly) bool {
return r.m.Equals(other.m)
}
func (r ReadOnly) String() string {
return r.m.String()
}

View File

@@ -2,47 +2,24 @@
package literal package literal
import ( import (
"fmt"
"reflect"
"sort"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
cidlink "github.com/ipld/go-ipld-prime/linking/cid" cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/node/basicnode"
) )
func Bool(val bool) ipld.Node { var Bool = basicnode.NewBool
nb := basicnode.Prototype.Bool.NewBuilder() var Int = basicnode.NewInt
nb.AssignBool(val) var Float = basicnode.NewFloat
return nb.Build() var String = basicnode.NewString
} var Bytes = basicnode.NewBytes
var Link = basicnode.NewLink
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 Link(link ipld.Link) ipld.Node {
nb := basicnode.Prototype.Link.NewBuilder()
nb.AssignLink(link)
return nb.Build()
}
func LinkCid(cid cid.Cid) ipld.Node { func LinkCid(cid cid.Cid) ipld.Node {
return Link(cidlink.Link{Cid: cid}) return Link(cidlink.Link{Cid: cid})
@@ -53,3 +30,158 @@ func Null() ipld.Node {
nb.AssignNull() nb.AssignNull()
return nb.Build() 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) {
// TODO: handle uint overflow below
// some fast path
switch val := v.(type) {
case bool:
return basicnode.NewBool(val), nil
case string:
return basicnode.NewString(val), nil
case int:
return basicnode.NewInt(int64(val)), 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:
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:
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:
return qp.Int(rv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return qp.Int(int64(rv.Uint()))
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))
}

View File

@@ -0,0 +1,262 @@
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"
)
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")))
v, 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 must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}

View File

@@ -10,225 +10,243 @@ import (
) )
// Match determines if the IPLD node satisfies the policy. // Match determines if the IPLD node satisfies the policy.
func (p Policy) Match(node datamodel.Node) bool { // The first Statement failing to match is returned as well.
func (p Policy) Match(node datamodel.Node) (bool, Statement) {
for _, stmt := range p { for _, stmt := range p {
ok := matchStatement(stmt, node) res, leaf := matchStatement(stmt, node)
if !ok { switch res {
return false case matchResultNoData, matchResultFalse:
return false, leaf
case matchResultOptionalNoData, matchResultTrue:
// continue
} }
} }
return true return true, nil
} }
// Filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path // PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
func (p Policy) Filter(path ...string) Policy { // If the data is missing or the non-optional Statement is matching, true is returned.
var filtered Policy //
// 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 { for _, stmt := range p {
newChild, remain := filter(stmt, path) res, leaf := matchStatement(stmt, node)
if newChild != nil && len(remain) == 0 { switch res {
filtered = append(filtered, newChild) 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
} }
} }
return filtered switch cur.Kind() {
}
func matchStatement(statement Statement, node ipld.Node) bool {
switch statement.Kind() {
case KindEqual: case KindEqual:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
one, many, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil {
return false return matchResultNoData, cur
} }
if one != nil { if res == nil { // optional selector didn't match
return datamodel.DeepEqual(s.value, one) return matchResultOptionalNoData, nil
} }
if many != nil { return boolToRes(datamodel.DeepEqual(s.value, res))
for _, n := range many {
if eq := datamodel.DeepEqual(s.value, n); eq {
return true
}
}
}
return false
} }
case KindGreaterThan: case KindGreaterThan:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
one, _, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil || one == nil { if err != nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, one, gt) if res == nil { // optional selector didn't match
return matchResultOptionalNoData, nil
}
return boolToRes(isOrdered(s.value, res, gt))
} }
case KindGreaterThanOrEqual: case KindGreaterThanOrEqual:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
one, _, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil || one == nil { if err != nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, one, gte) if res == nil { // optional selector didn't match
return matchResultOptionalNoData, nil
}
return boolToRes(isOrdered(s.value, res, gte))
} }
case KindLessThan: case KindLessThan:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
one, _, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil || one == nil { if err != nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, one, lt) if res == nil { // optional selector didn't match
return matchResultOptionalNoData, nil
}
return boolToRes(isOrdered(s.value, res, lt))
} }
case KindLessThanOrEqual: case KindLessThanOrEqual:
if s, ok := statement.(equality); ok { if s, ok := cur.(equality); ok {
one, _, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil || one == nil { if err != nil {
return false return matchResultNoData, cur
} }
return isOrdered(s.value, one, lte) if res == nil { // optional selector didn't match
return matchResultOptionalNoData, nil
}
return boolToRes(isOrdered(s.value, res, lte))
} }
case KindNot: case KindNot:
if s, ok := statement.(negation); ok { if s, ok := cur.(negation); ok {
return !matchStatement(s.statement, node) 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: case KindAnd:
if s, ok := statement.(connective); ok { if s, ok := cur.(connective); ok {
for _, cs := range s.statements { for _, cs := range s.statements {
r := matchStatement(cs, node) res, leaf := matchStatement(cs, node)
if !r { switch res {
return false case matchResultNoData, matchResultOptionalNoData:
return res, leaf
case matchResultTrue:
// continue
case matchResultFalse:
return matchResultFalse, leaf
} }
} }
return true return matchResultTrue, nil
} }
case KindOr: case KindOr:
if s, ok := statement.(connective); ok { if s, ok := cur.(connective); ok {
if len(s.statements) == 0 { if len(s.statements) == 0 {
return true return matchResultTrue, nil
} }
for _, cs := range s.statements { for _, cs := range s.statements {
r := matchStatement(cs, node) res, leaf := matchStatement(cs, node)
if r { switch res {
return true case matchResultNoData, matchResultOptionalNoData:
return res, leaf
case matchResultTrue:
return matchResultTrue, leaf
case matchResultFalse:
// continue
} }
} }
return false return matchResultFalse, cur
} }
case KindLike: case KindLike:
if s, ok := statement.(wildcard); ok { if s, ok := cur.(wildcard); ok {
one, _, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil || one == nil {
return false
}
v, err := one.AsString()
if err != nil { if err != nil {
return false return matchResultNoData, cur
} }
return s.pattern.Match(v) 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: case KindAll:
if s, ok := statement.(quantifier); ok { if s, ok := cur.(quantifier); ok {
_, many, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil || many == nil { if err != nil {
return false return matchResultNoData, cur
} }
for _, n := range many { if res == nil {
ok := matchStatement(s.statement, n) return matchResultOptionalNoData, nil
if !ok { }
return false 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 true return matchResultTrue, nil
} }
case KindAny: case KindAny:
if s, ok := statement.(quantifier); ok { if s, ok := cur.(quantifier); ok {
one, many, err := s.selector.Select(node) res, err := s.selector.Select(node)
if err != nil { if err != nil {
return false return matchResultNoData, cur
} }
if one != nil { if res == nil {
ok := matchStatement(s.statement, one) return matchResultOptionalNoData, nil
if ok { }
return true 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
} }
} }
if many != nil { return matchResultFalse, cur
for _, n := range many {
ok := matchStatement(s.statement, n)
if ok {
return true
}
}
}
return false
} }
} }
panic(fmt.Errorf("unimplemented statement kind: %s", statement.Kind())) panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
}
// filter performs a recursive filtering of the Statement, and prunes what doesn't match the given path
func filter(stmt Statement, path []string) (Statement, []string) {
// For each kind, we do some of the following if it applies:
// - test the path against the selector, consuming segments
// - for terminal statements (equality, wildcard), require all the segments to have been consumed
// - recursively filter child (negation, quantifier) or children (connective) statements with the remaining path
switch stmt.(type) {
case equality:
match, remain := stmt.(equality).selector.MatchPath(path...)
if match && len(remain) == 0 {
return stmt, remain
}
return nil, nil
case negation:
newChild, remain := filter(stmt.(negation).statement, path)
if newChild != nil && len(remain) == 0 {
return negation{
statement: newChild,
}, nil
}
return nil, nil
case connective:
var newChildren []Statement
for _, child := range stmt.(connective).statements {
newChild, remain := filter(child, path)
if newChild != nil && len(remain) == 0 {
newChildren = append(newChildren, newChild)
}
}
if len(newChildren) == 0 {
return nil, nil
}
return connective{
kind: stmt.(connective).kind,
statements: newChildren,
}, nil
case wildcard:
match, remain := stmt.(wildcard).selector.MatchPath(path...)
if match && len(remain) == 0 {
return stmt, remain
}
return nil, nil
case quantifier:
match, remain := stmt.(quantifier).selector.MatchPath(path...)
if match && len(remain) == 0 {
return stmt, remain
}
if !match {
return nil, nil
}
newChild, remain := filter(stmt.(quantifier).statement, remain)
if newChild != nil && len(remain) == 0 {
return quantifier{
kind: stmt.(quantifier).kind,
selector: stmt.(quantifier).selector,
statement: newChild,
}, nil
}
return nil, nil
default:
panic(fmt.Errorf("unimplemented statement kind: %s", stmt.Kind()))
}
} }
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool { func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,37 @@ func ExamplePolicy() {
// ] // ]
} }
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) { func TestConstruct(t *testing.T) {
pol, err := policy.Construct( pol, err := policy.Construct(
policy.Equal(".status", literal.String("draft")), policy.Equal(".status", literal.String("draft")),

View File

@@ -2,13 +2,13 @@ package selector
import ( import (
"fmt" "fmt"
"math"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
) )
var ( var (
identity = Selector{segment{str: ".", identity: true}}
indexRegex = regexp.MustCompile(`^-?\d+$`) indexRegex = regexp.MustCompile(`^-?\d+$`)
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`) sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`) fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
@@ -22,7 +22,10 @@ func Parse(str string) (Selector, error) {
return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0])) return nil, newParseError("selector must start with identity segment '.'", str, 0, string(str[0]))
} }
if str == "." { if str == "." {
return identity, nil return Selector{segment{str: ".", identity: true}}, nil
}
if str == ".?" {
return Selector{segment{str: ".?", identity: true, optional: true}}, nil
} }
col := 0 col := 0
@@ -31,56 +34,70 @@ func Parse(str string) (Selector, error) {
seg := tok seg := tok
opt := strings.HasSuffix(tok, "?") opt := strings.HasSuffix(tok, "?")
if opt { if opt {
seg = tok[0 : len(tok)-1] seg = strings.TrimRight(tok, "?")
} }
switch seg { switch {
case ".": case seg == ".":
if len(sel) > 0 && sel[len(sel)-1].Identity() { if len(sel) > 0 && sel[len(sel)-1].Identity() {
return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok) return nil, newParseError("selector contains unsupported recursive descent segment: '..'", str, col, tok)
} }
sel = append(sel, segment{str: ".", identity: true}) sel = append(sel, segment{str: ".", identity: true})
case "[]":
sel = append(sel, segment{str: tok, optional: opt, iterator: true})
default:
if strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]") {
lookup := seg[1 : len(seg)-1]
if indexRegex.MatchString(lookup) { // index case seg == "[]":
idx, err := strconv.Atoi(lookup) sel = append(sel, segment{str: tok, optional: opt, iterator: true})
if err != nil {
return nil, newParseError("invalid index", str, col, tok) case strings.HasPrefix(seg, "[") && strings.HasSuffix(seg, "]"):
} lookup := seg[1 : len(seg)-1]
sel = append(sel, segment{str: tok, optional: opt, index: idx})
} else if strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\"") { // explicit field switch {
sel = append(sel, segment{str: tok, optional: opt, field: lookup[1 : len(lookup)-1]}) // index, [123]
} else if sliceRegex.MatchString(lookup) { // slice [3:5] or [:5] or [3:] case indexRegex.MatchString(lookup):
var rng []int idx, err := strconv.Atoi(lookup)
splt := strings.Split(lookup, ":") if err != nil {
if splt[0] == "" { return nil, newParseError("invalid index", str, col, tok)
rng = append(rng, 0) }
} else { sel = append(sel, segment{str: tok, optional: opt, index: idx})
i, err := strconv.Atoi(splt[0])
if err != nil { // explicit field, ["abcd"]
return nil, newParseError("invalid slice index", str, col, tok) case strings.HasPrefix(lookup, "\"") && strings.HasSuffix(lookup, "\""):
} fieldName := lookup[1 : len(lookup)-1]
rng = append(rng, i) if strings.Contains(fieldName, ":") {
}
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) 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: fieldName})
sel = append(sel, segment{str: tok, optional: opt, field: seg[1:]})
} else { // 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)
}
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)
}
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) 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) col += len(tok)
} }

View File

@@ -0,0 +1,552 @@
package selector
import (
"math"
"testing"
"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("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())
})
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)
})
}

View File

@@ -2,12 +2,14 @@ package selector
import ( import (
"fmt" "fmt"
"math"
"strconv"
"strings" "strings"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/ipld/go-ipld-prime/schema"
) )
// Selector describes a UCAN policy selector, as specified here: // Selector describes a UCAN policy selector, as specified here:
@@ -15,17 +17,14 @@ import (
type Selector []segment type Selector []segment
// Select perform the selection described by the selector on the input IPLD DAG. // Select perform the selection described by the selector on the input IPLD DAG.
// If no error, Select returns either one ipld.Node or a []ipld.Node. // Select can return:
func (s Selector) Select(subject ipld.Node) (ipld.Node, []ipld.Node, error) { // - 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) return resolve(s, subject, nil)
} }
// MatchPath tells if the selector operates on the given (string only) path segments.
// It returns the segments that didn't get consumed by the matching.
func (s Selector) MatchPath(pathSegment ...string) (bool, []string) {
return matchPath(s, pathSegment)
}
func (s Selector) String() string { func (s Selector) String() string {
var res strings.Builder var res strings.Builder
for _, seg := range s { for _, seg := range s {
@@ -39,7 +38,7 @@ type segment struct {
identity bool identity bool
optional bool optional bool
iterator bool iterator bool
slice []int slice []int64 // either 0-length or 2-length
field string field string
index int index int
} }
@@ -65,7 +64,7 @@ func (s segment) Iterator() bool {
} }
// Slice flags that this segment targets a range of a slice. // Slice flags that this segment targets a range of a slice.
func (s segment) Slice() []int { func (s segment) Slice() []int64 {
return s.slice return s.slice
} }
@@ -79,335 +78,168 @@ func (s segment) Index() int {
return s.index return s.index
} }
func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, []ipld.Node, error) { func resolve(sel Selector, subject ipld.Node, at []string) (ipld.Node, error) {
cur := subject errIfNotOptional := func(s segment, err error) error {
for i, seg := range sel { if !s.Optional() {
if seg.Identity() { return err
continue
}
// 1st level: handle the different segment types (iterator, field, slice, index)
// 2nd level: handle different node kinds (list, map, string, bytes)
switch {
case seg.Iterator():
if cur == nil || cur.Kind() == datamodel.Kind_Null {
if seg.Optional() {
// build empty list
nb := basicnode.Prototype.List.NewBuilder()
assembler, err := nb.BeginList(0)
if err != nil {
return nil, nil, err
}
if err = assembler.Finish(); err != nil {
return nil, nil, err
}
return nb.Build(), nil, nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
}
} else {
var many []ipld.Node
switch cur.Kind() {
case datamodel.Kind_List:
it := cur.ListIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return nil, nil, err
}
// check if there are more iterator segments
if len(sel) > i+1 && sel[i+1].Iterator() {
if v.Kind() == datamodel.Kind_List {
// recursively resolve the remaining selector segments
var o ipld.Node
var m []ipld.Node
o, m, err = resolve(sel[i+1:], v, at)
if err != nil {
// if the segment is optional and an error occurs, skip the current iteration.
if seg.Optional() {
continue
} else {
return nil, nil, err
}
}
if m != nil {
many = append(many, m...)
} else if o != nil {
many = append(many, o)
}
} else {
// if the current value is not a list and the next segment is optional, skip the current iteration
if sel[i+1].Optional() {
continue
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at)
}
}
} else {
// if there are no more iterator segments, append the current value to the result
many = append(many, v)
}
}
case datamodel.Kind_Map:
it := cur.MapIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return nil, nil, err
}
if len(sel) > i+1 && sel[i+1].Iterator() {
if v.Kind() == datamodel.Kind_List {
var o ipld.Node
var m []ipld.Node
o, m, err = resolve(sel[i+1:], v, at)
if err != nil {
if seg.Optional() {
continue
} else {
return nil, nil, err
}
}
if m != nil {
many = append(many, m...)
} else if o != nil {
many = append(many, o)
}
} else {
if sel[i+1].Optional() {
continue
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(v)), at)
}
}
} else {
many = append(many, v)
}
}
default:
return nil, nil, newResolutionError(fmt.Sprintf("can not iterate over kind: %s", kindString(cur)), at)
}
return nil, many, nil
}
case seg.Field() != "":
at = append(at, seg.Field())
if cur == nil {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
}
} else {
switch cur.Kind() {
case datamodel.Kind_Map:
n, err := cur.LookupByString(seg.Field())
if err != nil {
if isMissing(err) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("object has no field named: %s", seg.Field()), at)
}
} else {
return nil, nil, err
}
} else {
cur = n
}
case datamodel.Kind_List:
var many []ipld.Node
it := cur.ListIterator()
for !it.Done() {
_, v, err := it.Next()
if err != nil {
return nil, nil, err
}
if v.Kind() == datamodel.Kind_Map {
n, err := v.LookupByString(seg.Field())
if err == nil {
many = append(many, n)
}
}
}
if len(many) > 0 {
cur = nil
return nil, many, nil
} else if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("no elements in list have field named: %s", seg.Field()), at)
}
default:
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
}
}
}
case seg.Slice() != nil:
if cur == nil {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
}
} else {
slice := seg.Slice()
var start, end, length int64
switch cur.Kind() {
case datamodel.Kind_List:
length = cur.Length()
start, end = resolveSliceIndices(slice, length)
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
length = int64(len(b))
start, end = resolveSliceIndices(slice, length)
case datamodel.Kind_String:
str, _ := cur.AsString()
length = int64(len(str))
start, end = resolveSliceIndices(slice, length)
default:
return nil, nil, newResolutionError(fmt.Sprintf("can not slice on kind: %s", kindString(cur)), at)
}
if start < 0 || end < start || end > length {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("slice out of bounds: [%d:%d]", start, end), at)
}
} else {
switch cur.Kind() {
case datamodel.Kind_List:
if end > cur.Length() {
end = cur.Length()
}
nb := basicnode.Prototype.List.NewBuilder()
assembler, _ := nb.BeginList(int64(end - start))
for i := start; i < end; i++ {
item, _ := cur.LookupByIndex(int64(i))
assembler.AssembleValue().AssignNode(item)
}
assembler.Finish()
cur = nb.Build()
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
l := int64(len(b))
if end > l {
end = l
}
cur = basicnode.NewBytes(b[start:end])
case datamodel.Kind_String:
str, _ := cur.AsString()
l := int64(len(str))
if end > l {
end = l
}
cur = basicnode.NewString(str[start:end])
}
}
}
default: // Index()
at = append(at, fmt.Sprintf("%d", seg.Index()))
if cur == nil {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
}
} else {
idx := seg.Index()
switch cur.Kind() {
case datamodel.Kind_List:
if idx < 0 {
idx = int(cur.Length()) + idx
}
if idx < 0 || idx >= int(cur.Length()) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
}
} else {
cur, _ = cur.LookupByIndex(int64(idx))
}
case datamodel.Kind_String:
str, _ := cur.AsString()
if idx < 0 {
idx = len(str) + idx
}
if idx < 0 || idx >= len(str) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
}
} else {
cur = basicnode.NewString(string(str[idx]))
}
case datamodel.Kind_Bytes:
b, _ := cur.AsBytes()
if idx < 0 {
idx = len(b) + idx
}
if idx < 0 || idx >= len(b) {
if seg.Optional() {
cur = nil
} else {
return nil, nil, newResolutionError(fmt.Sprintf("index out of bounds: %d", seg.Index()), at)
}
} else {
cur = basicnode.NewInt(int64(b[idx]))
}
default:
return nil, nil, newResolutionError(fmt.Sprintf("can not access index: %d on kind: %s", seg.Index(), kindString(cur)), at)
}
}
} }
return nil
} }
return cur, nil, nil cur := subject
}
func matchPath(sel Selector, path []string) (bool, []string) {
for _, seg := range sel { for _, seg := range sel {
if len(path) == 0 { // 1st level: handle the different segment types (iterator, field, slice, index)
return true, path // 2nd level: handle different node kinds (list, map, string, bytes)
}
switch { switch {
case seg.Identity(): case seg.Identity():
continue continue
case seg.Iterator(): case seg.Iterator():
// we have reached a [] iterator, it should have matched earlier switch {
return false, nil 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() != "": case seg.Field() != "":
// if exact match on the segment, we continue at = append(at, seg.Field())
if path[0] == seg.Field() { switch {
path = path[1:] case cur == nil:
continue err := newResolutionError(fmt.Sprintf("can not access field: %s on kind: %s", seg.Field(), kindString(cur)), at)
} return nil, errIfNotOptional(seg, err)
return false, nil
case seg.Slice() != nil: case cur.Kind() == datamodel.Kind_Map:
// we have reached a [<int>:<int>] slicing, it should have matched earlier n, err := cur.LookupByString(seg.Field())
return false, nil 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() default: // Index()
// we have reached a [<int>] indexing, it should have matched earlier at = append(at, strconv.Itoa(seg.Index()))
return false, nil
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)
}
} }
} }
return true, path
// segment exhausted, we return where we are
return cur, nil
} }
// resolveSliceIndices resolves the start and end indices for slicing a list or byte array. // resolveSliceIndices resolves the start and end indices for slicing a list or byte array.
@@ -421,26 +253,41 @@ func matchPath(sel Selector, path []string) (bool, []string) {
// //
// Returns: // Returns:
// - start: The resolved start index for slicing. // - start: The resolved start index for slicing.
// - end: The resolved end index for slicing. // - end: The resolved **excluded** end index for slicing.
func resolveSliceIndices(slice []int, length int64) (int64, int64) { func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
start, end := int64(0), length if len(slice) != 2 {
if len(slice) > 0 { panic("should always be 2-length")
start = int64(slice[0])
if start < 0 {
start = length + start
if start < 0 {
start = 0
}
}
} }
if len(slice) > 1 {
end = int64(slice[1]) start, end = slice[0], slice[1]
if end <= 0 {
end = length + end // adjust boundaries
if end < start { switch {
end = start case slice[0] == math.MinInt:
} start = 0
} case slice[0] < 0:
start = length + slice[0]
}
switch {
case slice[1] == math.MaxInt:
end = length
case slice[1] < 0:
end = length + slice[1]
}
// backward iteration is not allowed, shortcut to an empty result
if start >= end {
start, end = 0, 0
}
// clamp out of bound
if start < 0 {
start = 0
}
if start > length {
start = length
}
if end > length {
end = length
} }
return start, end return start, end
@@ -453,19 +300,6 @@ func kindString(n datamodel.Node) string {
return n.Kind().String() 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 { type resolutionerr struct {
msg string msg string
at []string at []string

View File

@@ -1,252 +1,20 @@
package selector package selector
import ( import (
"fmt" "errors"
"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"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/must" "github.com/ipld/go-ipld-prime/must"
basicnode "github.com/ipld/go-ipld-prime/node/basic" basicnode "github.com/ipld/go-ipld-prime/node/basic"
"github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/printer"
"github.com/stretchr/testify/require" "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) { func TestSelect(t *testing.T) {
type name struct { type name struct {
First string First string
@@ -313,14 +81,11 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".") sel, err := Parse(".")
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) res, err := sel.Select(anode)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, one) require.NotEmpty(t, res)
require.Empty(t, many)
fmt.Println(printer.Sprint(one)) age := must.Int(must.Node(res.LookupByString("age")))
age := must.Int(must.Node(one.LookupByString("age")))
require.Equal(t, int64(alice.Age), age) require.Equal(t, int64(alice.Age), age)
}) })
@@ -328,24 +93,18 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".name.first") sel, err := Parse(".name.first")
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) res, err := sel.Select(anode)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, one) require.NotEmpty(t, res)
require.Empty(t, many)
fmt.Println(printer.Sprint(one)) name := must.String(res)
name := must.String(one)
require.Equal(t, alice.Name.First, name) require.Equal(t, alice.Name.First, name)
one, many, err = sel.Select(bnode) res, err = sel.Select(bnode)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, one) require.NotEmpty(t, res)
require.Empty(t, many)
fmt.Println(printer.Sprint(one)) name = must.String(res)
name = must.String(one)
require.Equal(t, bob.Name.First, name) require.Equal(t, bob.Name.First, name)
}) })
@@ -353,108 +112,178 @@ func TestSelect(t *testing.T) {
sel, err := Parse(".name.middle?") sel, err := Parse(".name.middle?")
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) res, err := sel.Select(anode)
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, one) require.NotEmpty(t, res)
require.Empty(t, many)
fmt.Println(printer.Sprint(one)) name := must.String(res)
name := must.String(one)
require.Equal(t, *alice.Name.Middle, name) require.Equal(t, *alice.Name.Middle, name)
one, many, err = sel.Select(bnode) res, err = sel.Select(bnode)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, one) require.Empty(t, res)
require.Empty(t, many)
}) })
t.Run("not exists", func(t *testing.T) { t.Run("not exists", func(t *testing.T) {
sel, err := Parse(".name.foo") sel, err := Parse(".name.foo")
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) res, err := sel.Select(anode)
require.Error(t, err) require.Error(t, err)
require.Empty(t, one) require.Empty(t, res)
require.Empty(t, many)
fmt.Println(err) require.ErrorAs(t, err, &resolutionerr{}, "error should be a resolution error")
require.ErrorAs(t, err, &resolutionerr{}, "error was not a resolution error")
}) })
t.Run("optional not exists", func(t *testing.T) { t.Run("optional not exists", func(t *testing.T) {
sel, err := Parse(".name.foo?") sel, err := Parse(".name.foo?")
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) one, err := sel.Select(anode)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, one) require.Empty(t, one)
require.Empty(t, many)
}) })
t.Run("iterator", func(t *testing.T) { t.Run("iterator", func(t *testing.T) {
sel, err := Parse(".interests[]") sel, err := Parse(".interests[]")
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) res, err := sel.Select(anode)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, one) require.NotEmpty(t, res)
require.NotEmpty(t, many)
for _, n := range many { iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
fmt.Println(printer.Sprint(n))
}
iname := must.String(must.Node(many[0].LookupByString("name")))
require.Equal(t, alice.Interests[0].Name, iname) require.Equal(t, alice.Interests[0].Name, iname)
iname = must.String(must.Node(many[1].LookupByString("name"))) iname = must.String(must.Node(must.Node(res.LookupByIndex(1)).LookupByString("name")))
require.Equal(t, alice.Interests[1].Name, iname) require.Equal(t, alice.Interests[1].Name, iname)
}) })
t.Run("map iterator", func(t *testing.T) { t.Run("slice on string", func(t *testing.T) {
sel, err := Parse(".interests[0][]") sel, err := Parse(`.[1:3]`)
require.NoError(t, err) require.NoError(t, err)
one, many, err := sel.Select(anode) node := basicnode.NewString("hello")
res, err := sel.Select(node)
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, one) require.NotEmpty(t, res)
require.NotEmpty(t, many)
for _, n := range many { str, err := res.AsString()
fmt.Println(printer.Sprint(n)) require.NoError(t, err)
} require.Equal(t, "el", str) // assert sliced substring
require.Equal(t, alice.Interests[0].Name, must.String(many[0]))
require.Equal(t, alice.Interests[0].Experience, int(must.Int(many[2])))
}) })
}
func TestMatch(t *testing.T) { t.Run("out of bounds slicing", func(t *testing.T) {
for _, tc := range []struct { node, err := qp.BuildList(basicnode.Prototype.Any, 3, func(la datamodel.ListAssembler) {
sel string qp.ListEntry(la, qp.Int(1))
path []string qp.ListEntry(la, qp.Int(2))
want bool qp.ListEntry(la, qp.Int(3))
remaining []string
}{
{sel: ".foo.bar", path: []string{"foo", "bar"}, want: true, remaining: []string{}},
{sel: ".foo.bar", path: []string{"foo"}, want: true, remaining: []string{}},
{sel: ".foo.bar", path: []string{"foo", "bar", "baz"}, want: true, remaining: []string{"baz"}},
{sel: ".foo.bar", path: []string{"foo", "faa"}, want: false},
{sel: ".foo.[]", path: []string{"foo", "faa"}, want: false},
{sel: ".foo.[]", path: []string{"foo"}, want: true, remaining: []string{}},
{sel: ".foo.bar?", path: []string{"foo"}, want: true, remaining: []string{}},
{sel: ".foo.bar?", path: []string{"foo", "bar"}, want: true, remaining: []string{}},
{sel: ".foo.bar?", path: []string{"foo", "baz"}, want: false},
} {
t.Run(tc.sel, func(t *testing.T) {
sel := MustParse(tc.sel)
res, remain := sel.MatchPath(tc.path...)
require.Equal(t, tc.want, res)
require.EqualValues(t, tc.remaining, remain)
}) })
} 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) { func FuzzParse(f *testing.F) {
@@ -520,6 +349,10 @@ func FuzzParseAndSelect(f *testing.F) {
} }
// look for panic() // look for panic()
_, _, _ = sel.Select(node) _, err = sel.Select(node)
if err != nil && !errors.As(err, &resolutionerr{}) {
// not normal, we should only have resolution errors
t.Fatal(err)
}
}) })
} }

View File

@@ -1,15 +1,12 @@
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/pkg/policy/selector" "github.com/ucan-wg/go-ucan/pkg/policy/selector"
@@ -26,17 +23,15 @@ 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]`},
@@ -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 := sel.Select(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 := sel.Select(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 := sel.Select(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()

View File

@@ -10,17 +10,16 @@ package delegation
// TODO: change the "delegation" link above when the specification is merged // TODO: change the "delegation" link above when the specification is merged
import ( import (
"crypto/rand"
"errors" "errors"
"fmt" "fmt"
"time" "time"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/meta" "github.com/ucan-wg/go-ucan/pkg/meta"
"github.com/ucan-wg/go-ucan/pkg/policy" "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. // Token is an immutable type that holds the fields of a UCAN delegation.
@@ -47,15 +46,10 @@ type Token struct {
// New creates a validated Token from the provided parameters and options. // New creates a validated Token from the provided parameters and options.
// //
// When creating a delegated token, the Issuer's (iss) DID is assembed // When creating a delegated token, the Issuer's (iss) DID is assembled
// using the public key associated with the private key sent as the first // using the public key associated with the private key sent as the first
// parameter. // parameter.
func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
iss, err := did.FromPrivKey(privKey)
if err != nil {
return nil, err
}
tkn := &Token{ tkn := &Token{
issuer: iss, issuer: iss,
audience: aud, audience: aud,
@@ -72,8 +66,9 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
} }
} }
var err error
if len(tkn.nonce) == 0 { if len(tkn.nonce) == 0 {
tkn.nonce, err = generateNonce() tkn.nonce, err = nonce.Generate()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -92,15 +87,10 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
// When creating a root token, both the Issuer's (iss) and Subject's // When creating a root token, both the Issuer's (iss) and Subject's
// (sub) DIDs are assembled from the public key associated with the // (sub) DIDs are assembled from the public key associated with the
// private key passed as the first argument. // private key passed as the first argument.
func Root(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) { func Root(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
sub, err := did.FromPrivKey(privKey) opts = append(opts, WithSubject(iss))
if err != nil {
return nil, err
}
opts = append(opts, WithSubject(sub)) return New(iss, aud, cmd, pol, opts...)
return New(privKey, aud, cmd, pol, opts...)
} }
// Issuer returns the did.DID representing the Token's issuer. // Issuer returns the did.DID representing the Token's issuer.
@@ -138,8 +128,8 @@ func (t *Token) Nonce() []byte {
} }
// Meta returns the Token's metadata. // Meta returns the Token's metadata.
func (t *Token) Meta() *meta.Meta { func (t *Token) Meta() meta.ReadOnly {
return t.meta return t.meta.ReadOnly()
} }
// NotBefore returns the time at which the Token becomes "active". // NotBefore returns the time at which the Token becomes "active".
@@ -152,6 +142,24 @@ func (t *Token) Expiration() *time.Time {
return t.expiration 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) validate() error { func (t *Token) validate() error {
var errs error var errs error
@@ -184,27 +192,19 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
return nil, fmt.Errorf("parse iss: %w", err) return nil, fmt.Errorf("parse iss: %w", err)
} }
tkn.audience, err = did.Parse(m.Aud) if tkn.audience, err = did.Parse(m.Aud); err != nil {
if err != nil {
return nil, fmt.Errorf("parse audience: %w", err) return nil, fmt.Errorf("parse audience: %w", err)
} }
if m.Sub != nil { if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
tkn.subject, err = did.Parse(*m.Sub) return nil, fmt.Errorf("parse subject: %w", err)
if err != nil {
return nil, fmt.Errorf("parse subject: %w", err)
}
} else {
tkn.subject = did.Undef
} }
tkn.command, err = command.Parse(m.Cmd) if tkn.command, err = command.Parse(m.Cmd); err != nil {
if err != nil {
return nil, fmt.Errorf("parse command: %w", err) return nil, fmt.Errorf("parse command: %w", err)
} }
tkn.policy, err = policy.FromIPLD(m.Pol) if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
if err != nil {
return nil, fmt.Errorf("parse policy: %w", err) return nil, fmt.Errorf("parse policy: %w", err)
} }
@@ -213,17 +213,10 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
} }
tkn.nonce = m.Nonce tkn.nonce = m.Nonce
tkn.meta = &m.Meta tkn.meta = m.Meta
if m.Nbf != nil { tkn.notBefore = parse.OptionalTimestamp(m.Nbf)
t := time.Unix(*m.Nbf, 0) tkn.expiration = parse.OptionalTimestamp(m.Exp)
tkn.notBefore = &t
}
if m.Exp != nil {
t := time.Unix(*m.Exp, 0)
tkn.expiration = &t
}
if err := tkn.validate(); err != nil { if err := tkn.validate(); err != nil {
return nil, err return nil, err
@@ -231,14 +224,3 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
return &tkn, nil return &tkn, nil
} }
// generateNonce creates a 12-byte random nonce.
// TODO: some crypto scheme require more, is that our case?
func generateNonce() ([]byte, error) {
res := make([]byte, 12)
_, err := rand.Read(res)
if err != nil {
return nil, err
}
return res, nil
}

View File

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

View File

@@ -1,14 +1,14 @@
package delegation_test package delegation_test
import ( import (
"encoding/base64"
"testing" "testing"
"time" "time"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation"
@@ -17,16 +17,8 @@ import (
const ( const (
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0" nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM=" subJectCmd = "/foo/bar"
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv" subjectPol = `
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
subJectCmd = "/foo/bar"
subjectPol = `
[ [
[ [
"==", "==",
@@ -64,20 +56,15 @@ const (
] ]
` `
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e" newCID = "zdpuAwa4qv3ncMDPeDoqVxjZy3JoyWsbqUzm94rdA1AvRFkkw"
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X" rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
) )
func TestConstructors(t *testing.T) { func TestConstructors(t *testing.T) {
t.Parallel() t.Parallel()
privKey := privKey(t, issuerPrivKeyCfg)
aud, err := did.Parse(AudienceDID)
sub, err := did.Parse(subjectDID)
require.NoError(t, err)
cmd, err := command.Parse(subJectCmd) cmd, err := command.Parse(subJectCmd)
require.NoError(t, err) require.NoError(t, err)
@@ -88,27 +75,25 @@ func TestConstructors(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
t.Run("New", func(t *testing.T) { t.Run("New", func(t *testing.T) {
tkn, err := delegation.New(privKey, aud, cmd, pol, tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
delegation.WithNonce([]byte(nonce)), delegation.WithNonce([]byte(nonce)),
delegation.WithSubject(sub), delegation.WithSubject(didtest.PersonaAlice.DID()),
delegation.WithExpiration(exp), delegation.WithExpiration(exp),
delegation.WithMeta("foo", "fooo"), delegation.WithMeta("foo", "fooo"),
delegation.WithMeta("bar", "barr"), delegation.WithMeta("bar", "barr"),
) )
require.NoError(t, err) require.NoError(t, err)
data, err := tkn.ToDagJson(privKey) data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
require.NoError(t, err) require.NoError(t, err)
t.Log(string(data))
golden.Assert(t, string(data), "new.dagjson") golden.Assert(t, string(data), "new.dagjson")
}) })
t.Run("Root", func(t *testing.T) { t.Run("Root", func(t *testing.T) {
t.Parallel() t.Parallel()
tkn, err := delegation.Root(privKey, aud, cmd, pol, tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
delegation.WithNonce([]byte(nonce)), delegation.WithNonce([]byte(nonce)),
delegation.WithExpiration(exp), delegation.WithExpiration(exp),
delegation.WithMeta("foo", "fooo"), delegation.WithMeta("foo", "fooo"),
@@ -116,21 +101,109 @@ func TestConstructors(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
data, err := tkn.ToDagJson(privKey) data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
require.NoError(t, err) require.NoError(t, err)
t.Log(string(data))
golden.Assert(t, string(data), "root.dagjson") golden.Assert(t, string(data), "root.dagjson")
}) })
} }
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey { func TestEncryptedMeta(t *testing.T) {
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg) t.Parallel()
cmd, err := command.Parse(subJectCmd)
require.NoError(t, err)
pol, err := policy.FromDagJson(subjectPol)
require.NoError(t, err) require.NoError(t, err)
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar) encryptionKey, err := base64.StdEncoding.DecodeString(aesKey)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, encryptionKey, 32)
return privKey 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.New(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.New(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)
}
})
} }

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

View File

@@ -0,0 +1,33 @@
// Package delegationtest provides a set of pre-built delegation tokens
// for a variety of test cases.
//
// For all delegation tokens, the name of the delegation token is the
// Issuer appended with the Audience. The tokens are generated so that
// an invocation can be created for any didtest.Persona.
//
// Delegation proof-chain names contain each didtest.Persona name in
// order starting with the root delegation (which will always be generated
// by Alice). This is the opposite of the list of cic.Cids that represent the
// proof chain.
//
// For both the generated delegation tokens granted to Carol's Persona and
// the proof chains containing Carol's delegations to Dan, if there is no
// suffix, the proof chain will be deemed valid. If there is a suffix, it
// will consist of either the word "Valid" or "Invalid" and the name of the
// field that has been altered. Only optional fields will generate proof
// chains with Valid suffixes.
//
// If changes are made to the list of Personas included in the chain, or
// in the variants that are specified, the generated Go file and delegation
// tokens stored in the data/ directory should be regenerated by running
// the following command in this directory:
//
// cd generator && go run .
//
// Generated delegation Tokens are stored in the data/ directory and loaded
// into the delegation.Loader.
// Generated references to these tokens and the tokens themselves are
// created in the token_gen.go file. See /token/invocation/invocation_test.go
// for an example of how these delegation tokens and proof-chains can
// be used during testing.
package delegationtest

View File

@@ -0,0 +1,234 @@
package main
import (
"os"
"path/filepath"
"slices"
"time"
"github.com/dave/jennifer/jen"
"github.com/ipfs/go-cid"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/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"
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
)
const (
tokenNamePrefix = "Token"
proorChainNamePrefix = "Proof"
tokenExt = ".dagcbor"
)
var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
type newDelegationParams struct {
privKey crypto.PrivKey
aud did.DID
sub did.DID
cmd command.Command
pol policy.Policy
opts []delegation.Option
}
type token struct {
name string
id cid.Cid
}
type proof struct {
name string
prf []cid.Cid
}
type acc struct {
name string
chain []cid.Cid
}
type variant struct {
name string
variant func(*newDelegationParams)
}
func noopVariant() variant {
return variant{
name: "",
variant: func(_ *newDelegationParams) {},
}
}
type generator struct {
dlgs []token
chains []proof
}
func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari variant) error {
acc.name += personas[0].Name()
proofName := acc.name
if len(vari.name) > 0 {
proofName += "_" + vari.name
}
g.createProofChain(proofName, acc.chain)
if len(personas) < 2 {
return nil
}
name := personas[0].Name() + personas[1].Name()
params := newDelegationParams{
privKey: personas[0].PrivKey(),
aud: personas[1].DID(),
cmd: delegationtest.NominalCommand,
pol: policy.Policy{},
opts: []delegation.Option{
delegation.WithSubject(didtest.PersonaAlice.DID()),
delegation.WithNonce(constantNonce),
},
}
// Create each nominal token and continue the chain
id, err := g.createDelegation(params, name, vari)
if err != nil {
return err
}
acc.chain = append(acc.chain, id)
err = g.chainPersonas(personas[1:], acc, vari)
if err != nil {
return err
}
// If the user is Carol, create variants for each invalid and/or optional
// parameter and also continue the chain
if personas[0] == didtest.PersonaCarol {
variants := []variant{
{name: "InvalidExpandedCommand", variant: func(p *newDelegationParams) {
p.cmd = delegationtest.ExpandedCommand
}},
{name: "ValidAttenuatedCommand", variant: func(p *newDelegationParams) {
p.cmd = delegationtest.AttenuatedCommand
}},
{name: "InvalidSubject", variant: func(p *newDelegationParams) {
p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID()))
}},
{name: "InvalidExpired", variant: func(p *newDelegationParams) {
// Note: this makes the generator not deterministic
p.opts = append(p.opts, delegation.WithExpiration(time.Now().Add(time.Second)))
}},
{name: "InvalidInactive", variant: func(p *newDelegationParams) {
nbf, err := time.Parse(time.RFC3339, "2070-01-01T00:00:00Z")
if err != nil {
panic(err)
}
p.opts = append(p.opts, delegation.WithNotBefore(nbf))
}},
}
// Start a branch in the recursion for each of the variants
for _, v := range variants {
id, err := g.createDelegation(params, name, v)
if err != nil {
return err
}
// replace the previous Carol token id with the one from the variant
acc.chain[len(acc.chain)-1] = id
err = g.chainPersonas(personas[1:], acc, v)
if err != nil {
return err
}
}
}
return nil
}
func (g *generator) createDelegation(params newDelegationParams, name string, vari variant) (cid.Cid, error) {
vari.variant(&params)
issDID, err := did.FromPrivKey(params.privKey)
if err != nil {
return cid.Undef, err
}
tkn, err := delegation.New(issDID, params.aud, params.cmd, params.pol, params.opts...)
if err != nil {
return cid.Undef, err
}
data, id, err := tkn.ToSealed(params.privKey)
if err != nil {
return cid.Undef, err
}
dlgName := tokenNamePrefix + name
if len(vari.name) > 0 {
dlgName += "_" + vari.name
}
err = os.WriteFile(filepath.Join("..", delegationtest.TokenDir, dlgName+tokenExt), data, 0o644)
if err != nil {
return cid.Undef, err
}
g.dlgs = append(g.dlgs, token{
name: dlgName,
id: id,
})
return id, nil
}
func (g *generator) createProofChain(name string, prf []cid.Cid) {
if len(prf) < 1 {
return
}
clone := make([]cid.Cid, len(prf))
copy(clone, prf)
g.chains = append(g.chains, proof{
name: proorChainNamePrefix + name,
prf: clone,
})
}
func (g *generator) writeGoFile() error {
file := jen.NewFile("delegationtest")
file.HeaderComment("Code generated by delegationtest - DO NOT EDIT.")
refs := map[cid.Cid]string{}
for _, d := range g.dlgs {
refs[d.id] = d.name + "CID"
file.Var().Defs(
jen.Id(d.name+"CID").Op("=").Qual("github.com/ipfs/go-cid", "MustParse").Call(jen.Lit(d.id.String())),
jen.Id(d.name).Op("=").Id("mustGetDelegation").Call(jen.Id(d.name+"CID")),
)
file.Line()
}
for _, c := range g.chains {
g := jen.CustomFunc(jen.Options{
Multi: true,
Separator: ",",
Close: "\n",
}, func(g *jen.Group) {
slices.Reverse(c.prf)
for _, p := range c.prf {
g.Id(refs[p])
}
})
file.Var().Id(c.name).Op("=").Index().Qual("github.com/ipfs/go-cid", "Cid").Values(g)
file.Line()
}
return file.Save("../token_gen.go")
}

View File

@@ -0,0 +1,17 @@
package main
import (
"github.com/ucan-wg/go-ucan/did/didtest"
)
func main() {
gen := &generator{}
err := gen.chainPersonas(didtest.Personas(), acc{}, noopVariant())
if err != nil {
panic(err)
}
err = gen.writeGoFile()
if err != nil {
panic(err)
}
}

View File

@@ -0,0 +1,112 @@
package delegationtest
import (
"embed"
"path/filepath"
"sync"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
)
var (
// ExpandedCommand is the parent of the NominalCommand and represents
// the cases where the delegation proof-chain or invocation token tries
// to increase the privileges granted by the root delegation token.
// Execution of this command is generally prohibited in tests.
ExpandedCommand = command.MustParse("/expanded")
// NominalCommand is the command used for most test tokens and proof-
// chains. Execution of this command is generally allowed in tests.
NominalCommand = ExpandedCommand.Join("nominal")
// AttenuatedCommand is a sub-command of the NominalCommand. Execution
// of this command is generally allowed in tests.
AttenuatedCommand = NominalCommand.Join("attenuated")
)
// ProofEmpty provides an empty proof chain for testing purposes.
var ProofEmpty = []cid.Cid{}
const TokenDir = "data"
//go:embed data
var fs embed.FS
var _ delegation.Loader = (*delegationLoader)(nil)
type delegationLoader struct {
tokens map[cid.Cid]*delegation.Token
}
var (
once sync.Once
ldr delegation.Loader
)
// GetDelegationLoader returns a singleton instance of a test
// DelegationLoader containing all the tokens present in the data/
// directory.
func GetDelegationLoader() delegation.Loader {
once.Do(func() {
var err error
ldr, err = loadDelegations()
if err != nil {
panic(err)
}
})
return ldr
}
// GetDelegation implements invocation.DelegationLoader.
func (l *delegationLoader) GetDelegation(id cid.Cid) (*delegation.Token, error) {
tkn, ok := l.tokens[id]
if !ok {
return nil, delegation.ErrDelegationNotFound
}
return tkn, nil
}
func loadDelegations() (delegation.Loader, error) {
dirEntries, err := fs.ReadDir(TokenDir)
if err != nil {
return nil, err
}
tkns := make(map[cid.Cid]*delegation.Token, len(dirEntries))
for _, dirEntry := range dirEntries {
data, err := fs.ReadFile(filepath.Join(TokenDir, dirEntry.Name()))
if err != nil {
return nil, err
}
tkn, id, err := delegation.FromSealed(data)
if err != nil {
return nil, err
}
tkns[id] = tkn
}
return &delegationLoader{
tokens: tkns,
}, nil
}
// GetDelegation is a shortcut that gets (or creates) the DelegationLoader
// and attempts to return the token referenced by the provided CID.
func GetDelegation(id cid.Cid) (*delegation.Token, error) {
return GetDelegationLoader().GetDelegation(id)
}
func mustGetDelegation(id cid.Cid) *delegation.Token {
tkn, err := GetDelegation(id)
if err != nil {
panic(err)
}
return tkn
}

View File

@@ -0,0 +1,240 @@
// Code generated by delegationtest - DO NOT EDIT.
package delegationtest
import gocid "github.com/ipfs/go-cid"
var (
TokenAliceBobCID = gocid.MustParse("bafyreicidrwvmac5lvjypucgityrtjsknojraio7ujjli4r5eyby66wjzm")
TokenAliceBob = mustGetDelegation(TokenAliceBobCID)
)
var (
TokenBobCarolCID = gocid.MustParse("bafyreihxv2uhq43oxllzs2xfvxst7wtvvvl7pohb2chcz6hjvfv2ntea5u")
TokenBobCarol = mustGetDelegation(TokenBobCarolCID)
)
var (
TokenCarolDanCID = gocid.MustParse("bafyreihclsgiroazq3heqdswvj2cafwqbpboicq7immo65scl7ahktpsdq")
TokenCarolDan = mustGetDelegation(TokenCarolDanCID)
)
var (
TokenDanErinCID = gocid.MustParse("bafyreicja6ihewy64p3ake56xukotafjlkh4uqep2qhj52en46zzfwby3e")
TokenDanErin = mustGetDelegation(TokenDanErinCID)
)
var (
TokenErinFrankCID = gocid.MustParse("bafyreicjlx3lobxm6hl5s4htd4ydwkkqeiou6rft4rnvulfdyoew565vka")
TokenErinFrank = mustGetDelegation(TokenErinFrankCID)
)
var (
TokenCarolDan_InvalidExpandedCommandCID = gocid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4")
TokenCarolDan_InvalidExpandedCommand = mustGetDelegation(TokenCarolDan_InvalidExpandedCommandCID)
)
var (
TokenDanErin_InvalidExpandedCommandCID = gocid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm")
TokenDanErin_InvalidExpandedCommand = mustGetDelegation(TokenDanErin_InvalidExpandedCommandCID)
)
var (
TokenErinFrank_InvalidExpandedCommandCID = gocid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe")
TokenErinFrank_InvalidExpandedCommand = mustGetDelegation(TokenErinFrank_InvalidExpandedCommandCID)
)
var (
TokenCarolDan_ValidAttenuatedCommandCID = gocid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe")
TokenCarolDan_ValidAttenuatedCommand = mustGetDelegation(TokenCarolDan_ValidAttenuatedCommandCID)
)
var (
TokenDanErin_ValidAttenuatedCommandCID = gocid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y")
TokenDanErin_ValidAttenuatedCommand = mustGetDelegation(TokenDanErin_ValidAttenuatedCommandCID)
)
var (
TokenErinFrank_ValidAttenuatedCommandCID = gocid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q")
TokenErinFrank_ValidAttenuatedCommand = mustGetDelegation(TokenErinFrank_ValidAttenuatedCommandCID)
)
var (
TokenCarolDan_InvalidSubjectCID = gocid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u")
TokenCarolDan_InvalidSubject = mustGetDelegation(TokenCarolDan_InvalidSubjectCID)
)
var (
TokenDanErin_InvalidSubjectCID = gocid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty")
TokenDanErin_InvalidSubject = mustGetDelegation(TokenDanErin_InvalidSubjectCID)
)
var (
TokenErinFrank_InvalidSubjectCID = gocid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4")
TokenErinFrank_InvalidSubject = mustGetDelegation(TokenErinFrank_InvalidSubjectCID)
)
var (
TokenCarolDan_InvalidExpiredCID = gocid.MustParse("bafyreigenypixaxvhzlry5rjnywvjyl4xvzlzxz2ui74uzys7qdhos4bbu")
TokenCarolDan_InvalidExpired = mustGetDelegation(TokenCarolDan_InvalidExpiredCID)
)
var (
TokenDanErin_InvalidExpiredCID = gocid.MustParse("bafyreifvnfb7zqocpdysedcvjkb4y7tqfuziuqjhbbdoay4zg33pwpbzqi")
TokenDanErin_InvalidExpired = mustGetDelegation(TokenDanErin_InvalidExpiredCID)
)
var (
TokenErinFrank_InvalidExpiredCID = gocid.MustParse("bafyreicvydzt3obkqx7krmoi3zu4tlirlksibxfks5jc7vlvjxjamv2764")
TokenErinFrank_InvalidExpired = mustGetDelegation(TokenErinFrank_InvalidExpiredCID)
)
var (
TokenCarolDan_InvalidInactiveCID = gocid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u")
TokenCarolDan_InvalidInactive = mustGetDelegation(TokenCarolDan_InvalidInactiveCID)
)
var (
TokenDanErin_InvalidInactiveCID = gocid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq")
TokenDanErin_InvalidInactive = mustGetDelegation(TokenDanErin_InvalidInactiveCID)
)
var (
TokenErinFrank_InvalidInactiveCID = gocid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre")
TokenErinFrank_InvalidInactive = mustGetDelegation(TokenErinFrank_InvalidInactiveCID)
)
var ProofAliceBob = []gocid.Cid{
TokenAliceBobCID,
}
var ProofAliceBobCarol = []gocid.Cid{
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan = []gocid.Cid{
TokenCarolDanCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin = []gocid.Cid{
TokenDanErinCID,
TokenCarolDanCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank = []gocid.Cid{
TokenErinFrankCID,
TokenDanErinCID,
TokenCarolDanCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan_InvalidExpandedCommand = []gocid.Cid{
TokenCarolDan_InvalidExpandedCommandCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin_InvalidExpandedCommand = []gocid.Cid{
TokenDanErin_InvalidExpandedCommandCID,
TokenCarolDan_InvalidExpandedCommandCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []gocid.Cid{
TokenErinFrank_InvalidExpandedCommandCID,
TokenDanErin_InvalidExpandedCommandCID,
TokenCarolDan_InvalidExpandedCommandCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan_ValidAttenuatedCommand = []gocid.Cid{
TokenCarolDan_ValidAttenuatedCommandCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin_ValidAttenuatedCommand = []gocid.Cid{
TokenDanErin_ValidAttenuatedCommandCID,
TokenCarolDan_ValidAttenuatedCommandCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []gocid.Cid{
TokenErinFrank_ValidAttenuatedCommandCID,
TokenDanErin_ValidAttenuatedCommandCID,
TokenCarolDan_ValidAttenuatedCommandCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan_InvalidSubject = []gocid.Cid{
TokenCarolDan_InvalidSubjectCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin_InvalidSubject = []gocid.Cid{
TokenDanErin_InvalidSubjectCID,
TokenCarolDan_InvalidSubjectCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank_InvalidSubject = []gocid.Cid{
TokenErinFrank_InvalidSubjectCID,
TokenDanErin_InvalidSubjectCID,
TokenCarolDan_InvalidSubjectCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan_InvalidExpired = []gocid.Cid{
TokenCarolDan_InvalidExpiredCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin_InvalidExpired = []gocid.Cid{
TokenDanErin_InvalidExpiredCID,
TokenCarolDan_InvalidExpiredCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank_InvalidExpired = []gocid.Cid{
TokenErinFrank_InvalidExpiredCID,
TokenDanErin_InvalidExpiredCID,
TokenCarolDan_InvalidExpiredCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDan_InvalidInactive = []gocid.Cid{
TokenCarolDan_InvalidInactiveCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErin_InvalidInactive = []gocid.Cid{
TokenDanErin_InvalidInactiveCID,
TokenCarolDan_InvalidInactiveCID,
TokenBobCarolCID,
TokenAliceBobCID,
}
var ProofAliceBobCarolDanErinFrank_InvalidInactive = []gocid.Cid{
TokenErinFrank_InvalidInactiveCID,
TokenDanErin_InvalidInactiveCID,
TokenCarolDan_InvalidInactiveCID,
TokenBobCarolCID,
TokenAliceBobCID,
}

View File

@@ -0,0 +1,30 @@
package delegationtest_test
import (
"testing"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
)
func TestGetDelegation(t *testing.T) {
t.Run("passes with valid CID", func(t *testing.T) {
t.Parallel()
tkn, err := delegationtest.GetDelegation(delegationtest.TokenAliceBobCID)
require.NoError(t, err)
assert.NotZero(t, tkn)
})
t.Run("fails with unknown CID", func(t *testing.T) {
t.Parallel()
tkn, err := delegationtest.GetDelegation(cid.Undef)
require.ErrorIs(t, err, delegation.ErrDelegationNotFound)
assert.Nil(t, tkn)
})
}

View File

@@ -2,7 +2,6 @@ package delegation_test
import ( import (
"bytes" "bytes"
"crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
@@ -13,9 +12,8 @@ import (
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy" "github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal" "github.com/ucan-wg/go-ucan/pkg/policy/literal"
@@ -26,15 +24,7 @@ import (
// The following example shows how to create a delegation.Token with // The following example shows how to create a delegation.Token with
// distinct DIDs for issuer (iss), audience (aud) and subject (sub). // distinct DIDs for issuer (iss), audience (aud) and subject (sub).
func ExampleNew() { func ExampleNew() {
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader) fmt.Println("issDid:", didtest.PersonaBob.DID().String())
printThenPanicOnErr(err)
issDid, err := did.FromPubKey(issPub)
printThenPanicOnErr(err)
fmt.Println("issDid:", issDid)
audDid := did.MustParse(AudienceDID)
subDid := did.MustParse(subjectDID)
// The command defines the shape of the arguments that will be evaluated against the policy // The command defines the shape of the arguments that will be evaluated against the policy
cmd := command.MustParse("/foo/bar") cmd := command.MustParse("/foo/bar")
@@ -51,8 +41,8 @@ func ExampleNew() {
)), )),
) )
tkn, err := delegation.New(issPriv, audDid, cmd, pol, tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol,
delegation.WithSubject(subDid), delegation.WithSubject(didtest.PersonaAlice.DID()),
delegation.WithExpirationIn(time.Hour), delegation.WithExpirationIn(time.Hour),
delegation.WithNotBeforeIn(time.Minute), delegation.WithNotBeforeIn(time.Minute),
delegation.WithMeta("foo", "bar"), delegation.WithMeta("foo", "bar"),
@@ -61,101 +51,91 @@ func ExampleNew() {
printThenPanicOnErr(err) printThenPanicOnErr(err)
// "Seal", meaning encode and wrap into a signed envelope. // "Seal", meaning encode and wrap into a signed envelope.
data, id, err := tkn.ToSealed(issPriv) data, id, err := tkn.ToSealed(didtest.PersonaBob.PrivKey())
printThenPanicOnErr(err) printThenPanicOnErr(err)
printCIDAndSealed(id, data) printCIDAndSealed(id, data)
// Example output: // Example output:
// //
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe // issDid: did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU
// //
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW // CID (base58BTC): zdpuAsqfZkgg2jgZyob23sq1J9xwtf9PHgt1PsskVCMq7Vvxk
//
// DAG-CBOR (base64) out: glhA5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDWzqY2lzc3g4ZGlkOmtleTp6Nk1raFZGem5QZVI1NzJyVEs1MVVqb1ROcG5GOGN4dVdmUG05b0JNUHI3eThBQmVjbmJmGmcNXxZjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3RBMXVCZENwcTR1SkJxRTlqak1pTHl4WkJnOWE2eGdQUEtKak1xc3M2WmMyZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMu0HMgJ5Y+M84I/66
// //
// DAG-CBOR (base64) out: lhAOnjc0bPptlI5MxRBrIK3YmAP1CxKfXOPkz6MHt/UJCx2gCN+6gXZX2N+BIJvmy8XmAO5sT2GYimiV7HlJH1AA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rZ3VwY2hoNUh3dUhhaFM3WXN5RThiTHVhMU1yOHAyaUtOUmh5dlN2UkFzOW5jY21kaC9mb28vYmFyY2V4cBpnROP/Y2lzc3g4ZGlkOmtleTp6Nk1rdkpQbUVaWlliZ2l3MW91VDFvb3VUc1RGQkhKU3RzOW9waFZzTmdjUm1ZeFVjbmJmGmdE1itjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3V1a2syc2tEWExRbjdOSzNFaDlqTW5kWWZ2REJ4eGt0Z3BpZEpBcWI3TTNwZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMv+Diy6GExIuM1eX4
// Converted to DAG-JSON out: // Converted to DAG-JSON out:
// [ // [
// { // {
// "/": {
// "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
// }
// },
// {
// "h": {
// "/": { // "/": {
// "bytes": "NO0BcQ" // "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
// } // }
// }, // },
// "ucan/dlg@1.0.0-rc.1": { // {
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv", // "h": {
// "cmd": "/foo/bar",
// "exp": 1728933098,
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
// "meta": {
// "baz": 123,
// "foo": "bar"
// },
// "nbf": 1728929558,
// "nonce": {
// "/": { // "/": {
// "bytes": "u0HMgJ5Y+M84I/66" // "bytes": "NO0BcQ"
// } // }
// }, // },
// "pol": [ // "ucan/dlg@1.0.0-rc.1": {
// [ // "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
// "==", // "cmd": "/foo/bar",
// ".status", // "exp": 1728933098,
// "draft" // "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
// ], // "meta": {
// [ // "baz": 123,
// "all", // "foo": "bar"
// ".reviewer", // },
// "nbf": 1728929558,
// "nonce": {
// "/": {
// "bytes": "u0HMgJ5Y+M84I/66"
// }
// },
// "pol": [
// [ // [
// "like", // "==",
// ".email", // ".status",
// "*@example.com" // "draft"
// ] // ],
// ],
// [
// "any",
// ".tags",
// [ // [
// "or", // "all",
// ".reviewer",
// [ // [
// "like",
// ".email",
// "*@example.com"
// ]
// ],
// [
// "any",
// ".tags",
// [
// "or",
// [ // [
// "==", // [
// ".", // "==",
// "news" // ".",
// ], // "news"
// [ // ],
// "==", // [
// ".", // "==",
// "press" // ".",
// "press"
// ]
// ] // ]
// ] // ]
// ] // ]
// ] // ],
// ], // "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2" // }
// } // }
// } // ]
// ]
} }
// The following example shows how to create a UCAN root delegation.Token // The following example shows how to create a UCAN root delegation.Token
// - a delegation.Token with the subject (sub) set to the value of issuer // - a delegation.Token with the subject (sub) set to the value of issuer
// (iss). // (iss).
func ExampleRoot() { func ExampleRoot() {
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
printThenPanicOnErr(err)
issDid, err := did.FromPubKey(issPub)
printThenPanicOnErr(err)
fmt.Println("issDid:", issDid)
audDid := did.MustParse(AudienceDID)
// The command defines the shape of the arguments that will be evaluated against the policy // The command defines the shape of the arguments that will be evaluated against the policy
cmd := command.MustParse("/foo/bar") cmd := command.MustParse("/foo/bar")
@@ -171,7 +151,7 @@ func ExampleRoot() {
)), )),
) )
tkn, err := delegation.Root(issPriv, audDid, cmd, pol, tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
delegation.WithExpirationIn(time.Hour), delegation.WithExpirationIn(time.Hour),
delegation.WithNotBeforeIn(time.Minute), delegation.WithNotBeforeIn(time.Minute),
delegation.WithMeta("foo", "bar"), delegation.WithMeta("foo", "bar"),
@@ -180,7 +160,7 @@ func ExampleRoot() {
printThenPanicOnErr(err) printThenPanicOnErr(err)
// "Seal", meaning encode and wrap into a signed envelope. // "Seal", meaning encode and wrap into a signed envelope.
data, id, err := tkn.ToSealed(issPriv) data, id, err := tkn.ToSealed(didtest.PersonaAlice.PrivKey())
printThenPanicOnErr(err) printThenPanicOnErr(err)
printCIDAndSealed(id, data) printCIDAndSealed(id, data)
@@ -189,82 +169,82 @@ func ExampleRoot() {
// //
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF // issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
// //
// CID (base58BTC): zdpuAwLojgfvFCbjz2FsKrvN1khDQ9mFGT6b6pxjMfz73Roed // CID (base58BTC): zdpuAkwYz8nY7uU8j3F6wVTfFY1VEoExwvUAYBEwRWfTozddE
// //
// DAG-CBOR (base64) out: glhA6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDW0wY2lzc3g4ZGlkOmtleTp6Nk1rbldKcXoxN1k0QWZzWFNKVUZLb211QlI0R1RrVmlNN2tKWXV0elRNa0N5RkZjbmJmGmcNX1xjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa25XSnF6MTdZNEFmc1hTSlVGS29tdUJSNEdUa1ZpTTdrSll1dHpUTWtDeUZGZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMJOsjYi1Pq3OIB0La // DAG-CBOR (base64) out: glhAVpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rdkpQbUVaWlliZ2l3MW91VDFvb3VUc1RGQkhKU3RzOW9waFZzTmdjUm1ZeFVjY21kaC9mb28vYmFyY2V4cBpnROVoY2lzc3g4ZGlkOmtleTp6Nk1rdXVrazJza0RYTFFuN05LM0VoOWpNbmRZZnZEQnh4a3RncGlkSkFxYjdNM3BjbmJmGmdE15RjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3V1a2syc2tEWExRbjdOSzNFaDlqTW5kWWZ2REJ4eGt0Z3BpZEpBcWI3TTNwZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMwzDc03WBciJIGPWG
// //
// Converted to DAG-JSON out: // Converted to DAG-JSON out:
// [ // [
// { // {
// "/": {
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
// }
// },
// {
// "h": {
// "/": { // "/": {
// "bytes": "NO0BcQ" // "bytes": "VpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSAw"
// } // }
// }, // },
// "ucan/dlg@1.0.0-rc.1": { // {
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv", // "h": {
// "cmd": "/foo/bar",
// "exp": 1728933168,
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
// "meta": {
// "baz": 123,
// "foo": "bar"
// },
// "nbf": 1728929628,
// "nonce": {
// "/": { // "/": {
// "bytes": "JOsjYi1Pq3OIB0La" // "bytes": "NO0BcQ"
// } // }
// }, // },
// "pol": [ // "ucan/dlg@1.0.0-rc.1": {
// [ // "aud": "did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU",
// "==", // "cmd": "/foo/bar",
// ".status", // "exp": 1732568424,
// "draft" // "iss": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p",
// ], // "meta": {
// [ // "baz": 123,
// "all", // "foo": "bar"
// ".reviewer", // },
// "nbf": 1732564884,
// "nonce": {
// "/": {
// "bytes": "wzDc03WBciJIGPWG"
// }
// },
// "pol": [
// [ // [
// "like", // "==",
// ".email", // ".status",
// "*@example.com" // "draft"
// ] // ],
// ],
// [
// "any",
// ".tags",
// [ // [
// "or", // "all",
// ".reviewer",
// [ // [
// "like",
// ".email",
// "*@example.com"
// ]
// ],
// [
// "any",
// ".tags",
// [
// "or",
// [ // [
// "==", // [
// ".", // "==",
// "news" // ".",
// ], // "news"
// [ // ],
// "==", // [
// ".", // "==",
// "press" // ".",
// "press"
// ]
// ] // ]
// ] // ]
// ] // ]
// ] // ],
// ], // "sub": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"
// "sub": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF" // }
// } // }
// } // ]
// ]
} }
// The following example demonstrates how to get a delegation.Token from // The following example demonstrates how to get a delegation.Token from
// a DAG-CBOR []byte. // a DAG-CBOR []byte.
func ExampleToken_FromSealed() { func ExampleFromSealed() {
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL" const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64) cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)

View File

@@ -1,6 +1,7 @@
package delegation package delegation
import ( import (
"fmt"
"io" "io"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
@@ -193,8 +194,16 @@ func FromIPLD(node datamodel.Node) (*Token, error) {
} }
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
var sub *string // sanity check that privKey and issuer are matching
issPub, err := t.issuer.PubKey()
if err != nil {
return nil, err
}
if !issPub.Equals(privKey.GetPublic()) {
return nil, fmt.Errorf("private key doesn't match the issuer")
}
var sub *string
if t.subject != did.Undef { if t.subject != did.Undef {
s := t.subject.String() s := t.subject.String()
sub = &s sub = &s
@@ -224,10 +233,15 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
Cmd: t.command.String(), Cmd: t.command.String(),
Pol: pol, Pol: pol,
Nonce: t.nonce, Nonce: t.nonce,
Meta: *t.meta, Meta: t.meta,
Nbf: nbf, Nbf: nbf,
Exp: exp, Exp: exp,
} }
// seems like it's a requirement to have a null meta if there are no values?
if len(model.Meta.Keys) == 0 {
model.Meta = nil
}
return envelope.ToIPLD(privKey, model) return envelope.ToIPLD(privKey, model)
} }

View File

@@ -0,0 +1,17 @@
package delegation
import (
"fmt"
"github.com/ipfs/go-cid"
)
// ErrDelegationNotFound is returned if a delegation token is not found
var ErrDelegationNotFound = fmt.Errorf("delegation not found")
// Loader is a delegation token loader.
type Loader interface {
// GetDelegation returns the delegation.Token matching the given CID.
// If not found, ErrDelegationNotFound is returned.
GetDelegation(cid cid.Cid) (*Token, error)
}

View File

@@ -44,6 +44,22 @@ func WithMeta(key string, val any) Option {
} }
} }
// WithEncryptedMetaString adds a key/value pair in the "meta" field.
// The string value is encrypted with the given aesKey.
func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option {
return func(t *Token) error {
return t.meta.AddEncrypted(key, val, encryptionKey)
}
}
// WithEncryptedMetaBytes adds a key/value pair in the "meta" field.
// The []byte value is encrypted with the given aesKey.
func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option {
return func(t *Token) error {
return t.meta.AddEncrypted(key, val, encryptionKey)
}
}
// WithNotBefore set's the Token's optional "notBefore" field to the value // WithNotBefore set's the Token's optional "notBefore" field to the value
// of the provided time.Time. // of the provided time.Time.
func WithNotBefore(nbf time.Time) Option { func WithNotBefore(nbf time.Time) Option {

View File

@@ -26,17 +26,17 @@ const Tag = "ucan/dlg@1.0.0-rc.1"
var schemaBytes []byte var schemaBytes []byte
var ( var (
once sync.Once once sync.Once
ts *schema.TypeSystem ts *schema.TypeSystem
err error errSchema error
) )
func mustLoadSchema() *schema.TypeSystem { func mustLoadSchema() *schema.TypeSystem {
once.Do(func() { once.Do(func() {
ts, err = ipld.LoadSchemaBytes(schemaBytes) ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
}) })
if err != nil { if errSchema != nil {
panic(fmt.Errorf("failed to load IPLD schema: %s", err)) panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
} }
return ts return ts
} }
@@ -66,7 +66,7 @@ type tokenPayloadModel struct {
Nonce []byte Nonce []byte
// Arbitrary Metadata // Arbitrary Metadata
Meta meta.Meta Meta *meta.Meta
// "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer // "Not before" UTC Unix Timestamp in seconds (valid from), 53-bits integer
// optional: can be nil // optional: can be nil

View File

@@ -3,7 +3,6 @@ package delegation_test
import ( import (
"bytes" "bytes"
_ "embed" _ "embed"
"fmt"
"testing" "testing"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
@@ -11,6 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/token/delegation" "github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/internal/envelope" "github.com/ucan-wg/go-ucan/token/internal/envelope"
) )
@@ -22,7 +22,7 @@ func TestSchemaRoundTrip(t *testing.T) {
t.Parallel() t.Parallel()
delegationJson := golden.Get(t, "new.dagjson") delegationJson := golden.Get(t, "new.dagjson")
privKey := privKey(t, issuerPrivKeyCfg) privKey := didtest.PersonaAlice.PrivKey()
t.Run("via buffers", func(t *testing.T) { t.Run("via buffers", func(t *testing.T) {
t.Parallel() t.Parallel()
@@ -36,18 +36,13 @@ func TestSchemaRoundTrip(t *testing.T) {
cborBytes, id, err := p1.ToSealed(privKey) cborBytes, id, err := p1.ToSealed(privKey)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id)) assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
fmt.Println("cborBytes length", len(cborBytes))
fmt.Println("cbor", string(cborBytes))
p2, c2, err := delegation.FromSealed(cborBytes) p2, c2, err := delegation.FromSealed(cborBytes)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, id, c2) assert.Equal(t, id, c2)
fmt.Println("read Cbor", p2)
readJson, err := p2.ToDagJson(privKey) readJson, err := p2.ToDagJson(privKey)
require.NoError(t, err) require.NoError(t, err)
fmt.Println("readJson length", len(readJson))
fmt.Println("json: ", string(readJson))
assert.JSONEq(t, string(delegationJson), string(readJson)) assert.JSONEq(t, string(delegationJson), string(readJson))
}) })
@@ -65,7 +60,6 @@ func TestSchemaRoundTrip(t *testing.T) {
cborBytes := &bytes.Buffer{} cborBytes := &bytes.Buffer{}
id, err := p1.ToSealedWriter(cborBytes, privKey) id, err := p1.ToSealedWriter(cborBytes, privKey)
t.Log(len(id.Bytes()), id.Bytes())
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id)) assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
@@ -79,6 +73,16 @@ func TestSchemaRoundTrip(t *testing.T) {
assert.JSONEq(t, string(delegationJson), readJson.String()) assert.JSONEq(t, string(delegationJson), readJson.String())
}) })
t.Run("fails with wrong PrivKey", func(t *testing.T) {
t.Parallel()
p1, err := delegation.FromDagJson(delegationJson)
require.NoError(t, err)
_, _, err = p1.ToSealed(didtest.PersonaBob.PrivKey())
require.EqualError(t, err, "private key doesn't match the issuer")
})
} }
func BenchmarkSchemaLoad(b *testing.B) { func BenchmarkSchemaLoad(b *testing.B) {
@@ -90,7 +94,7 @@ func BenchmarkSchemaLoad(b *testing.B) {
func BenchmarkRoundTrip(b *testing.B) { func BenchmarkRoundTrip(b *testing.B) {
delegationJson := golden.Get(b, "new.dagjson") delegationJson := golden.Get(b, "new.dagjson")
privKey := privKey(b, issuerPrivKeyCfg) privKey := didtest.PersonaAlice.PrivKey()
b.Run("via buffers", func(b *testing.B) { b.Run("via buffers", func(b *testing.B) {
p1, _ := delegation.FromDagJson(delegationJson) p1, _ := delegation.FromDagJson(delegationJson)

View File

@@ -1 +1 @@
[{"/":{"bytes":"FM6otj0r/noJWiGAC5WV86xAazxrF173IihuHJgEt35CtSzjeaelrR3UwaSr8xbE9sLpo5xJhUbo0QLI273hDA"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"}}] [{"/":{"bytes":"BBabgnWqd+cjwG1td0w9BudNocmUwoR89RMZTqZHk3osCXEI/bOkko0zTvlusaE4EMBBeSzZDKzjvunLBfdiBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"}}]

View File

@@ -1 +1 @@
[{"/":{"bytes":"aYBq08tfm0zQZnPg/5tB9kM5mklRU9PPIkV7CK68jEgbd76JbCGuu75vfLyBu3WTqKzLSJ583pbwu668m/7MBQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"}}] [{"/":{"bytes":"BBabgnWqd+cjwG1td0w9BudNocmUwoR89RMZTqZHk3osCXEI/bOkko0zTvlusaE4EMBBeSzZDKzjvunLBfdiBg"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/dlg@1.0.0-rc.1":{"aud":"did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU","cmd":"/foo/bar","exp":7258118400,"iss":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p","meta":{"bar":"barr","foo":"fooo"},"nonce":{"/":{"bytes":"NnJvRGhHaTBraU5yaVFBejdKM2QrYk9lb0kvdGo4RU5pa21RTmJ0am5EMA"}},"pol":[["==",".status","draft"],["all",".reviewer",["like",".email","*@example.com"]],["any",".tags",["or",[["==",".","news"],["==",".","press"]]]]],"sub":"did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"}}]

View File

@@ -2,22 +2,22 @@ package token
import ( import (
"io" "io"
"time"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/codec" "github.com/ipld/go-ipld-prime/codec"
"github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/meta"
) )
type Token interface { type Token interface {
Marshaller Marshaller
// Issuer returns the did.DID representing the Token's issuer. // IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
Issuer() did.DID // This does NOT do any other kind of verifications.
// Meta returns the Token's metadata. IsValidNow() bool
Meta() *meta.Meta // 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.
IsValidAt(t time.Time) bool
} }
type Marshaller interface { type Marshaller interface {

View File

@@ -187,6 +187,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type") return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
} }
// TODO: can we use the already serialized CBOR data here, instead of encoding again the payload?
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode) data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
if err != nil { if err != nil {
return zero, err return zero, err

View File

@@ -0,0 +1,31 @@
package nonce
import "crypto/rand"
// TODO: some crypto scheme require more, is that our case?
//
// The spec mention:
// The REQUIRED nonce parameter nonce MAY be any value.
// A randomly generated string is RECOMMENDED to provide a unique UCAN, though it MAY
// also be a monotonically increasing count of the number of links in the hash chain.
// This field helps prevent replay attacks and ensures a unique CID per delegation.
// The iss, aud, and exp fields together will often ensure that UCANs are unique,
// but adding the nonce ensures uniqueness.
//
// The recommended size of the nonce differs by key type. In many cases, a random
// 12-byte nonce is sufficient. If uncertain, check the nonce in your DID's crypto suite.
//
// 12 bytes is 10^28, 16 bytes is 10^38. Both sounds like a lot of random to achieve
// those goals, but maybe the crypto voodoo require more.
//
// The rust implementation use 16 bytes nonce.
// Generate creates a 12-byte random nonce.
func Generate() ([]byte, error) {
res := make([]byte, 12)
_, err := rand.Read(res)
if err != nil {
return nil, err
}
return res, nil
}

View File

@@ -0,0 +1,22 @@
package parse
import (
"time"
"github.com/ucan-wg/go-ucan/did"
)
func OptionalDID(s *string) (did.DID, error) {
if s == nil {
return did.Undef, nil
}
return did.Parse(*s)
}
func OptionalTimestamp(sec *int64) *time.Time {
if sec == nil {
return nil
}
t := time.Unix(*sec, 0)
return &t
}

View File

@@ -0,0 +1,37 @@
package invocation
import "errors"
// Loading errors
var (
// ErrMissingDelegation
ErrMissingDelegation = errors.New("loader missing delegation for proof chain")
)
// Time bound errors
var (
// ErrTokenExpired is returned if a token is invalid at execution time
ErrTokenInvalidNow = errors.New("token has expired")
)
// Principal alignment errors
var (
// ErrNoProof is returned when no delegations were provided to prove
// that the invocation should be executed.
ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation")
// ErrLastNotRoot is returned if the last delegation token in the proof
// chain is not a root delegation token.
ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token")
// ErrBrokenChain is returned when the Audience of a delegation is
// not the Issuer of the previous one.
ErrBrokenChain = errors.New("delegation proof chain doesn't connect the invocation to the subject")
// ErrWrongSub is returned when the Subject of a delegation is not the invocation audience.
ErrWrongSub = errors.New("delegation subject need to match the invocation audience")
// ErrCommandNotCovered is returned when a delegation command doesn't cover (identical or parent of) the
// next delegation or invocation's command.
ErrCommandNotCovered = errors.New("allowed command doesn't cover the next delegation or invocation")
)

View File

@@ -0,0 +1,201 @@
package invocation_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/codec/dagjson"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/invocation"
)
func ExampleNew() {
privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
if err != nil {
fmt.Println("failed to create setup:", err.Error())
return
}
inv, err := invocation.New(iss, sub, cmd, prf,
invocation.WithArgument("uri", args["uri"]),
invocation.WithArgument("headers", args["headers"]),
invocation.WithArgument("payload", args["payload"]),
invocation.WithMeta("env", "development"),
invocation.WithMeta("tags", meta["tags"]),
invocation.WithExpirationIn(time.Minute),
invocation.WithoutInvokedAt())
if err != nil {
fmt.Println("failed to create invocation:", err.Error())
return
}
data, cid, err := inv.ToSealed(privKey)
if err != nil {
fmt.Println("failed to seal invocation:", err.Error())
return
}
json, err := prettyDAGJSON(data)
if err != nil {
fmt.Println("failed to pretty DAG-JSON:", err.Error())
return
}
fmt.Println("CID:", cid)
fmt.Println("Token (pretty DAG-JSON):")
fmt.Println(json)
// Expected CID and DAG-JSON output:
// CID: bafyreid2n5q45vk4osned7k5huocbe3mxbisonh5vujepqftc5ftr543ae
// Token (pretty DAG-JSON):
// [
// {
// "/": {
// "bytes": "gvyL7kdSkgmaDpDU/Qj9ohRwxYLCHER52HFMSFEqQqEcQC9qr4JCPP1f/WybvGGuVzYiA0Hx4JO+ohNz8BxUAA"
// }
// },
// {
// "h": {
// "/": {
// "bytes": "NO0BcQ"
// }
// },
// "ucan/inv@1.0.0-rc.1": {
// "args": {
// "headers": {
// "Content-Type": "application/json"
// },
// "payload": {
// "body": "UCAN is great",
// "draft": true,
// "title": "UCAN for Fun and Profit",
// "topics": [
// "authz",
// "journal"
// ]
// },
// "uri": "https://example.com/blog/posts"
// },
// "cmd": "/crud/create",
// "exp": 1729788921,
// "iss": "did:key:z6MkhniGGyP88eZrq2dpMvUPdS2RQMhTUAWzcu6kVGUvEtCJ",
// "meta": {
// "env": "development",
// "tags": [
// "blog",
// "post",
// "pr#123"
// ]
// },
// "nonce": {
// "/": {
// "bytes": "2xXPoZwWln1TfXIp"
// }
// },
// "prf": [
// {
// "/": "bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"
// },
// {
// "/": "bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"
// },
// {
// "/": "bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"
// }
// ],
// "sub": "did:key:z6MktWuvPvBe5UyHnDGuEdw8aJ5qrhhwLG6jy7cQYM6ckP6P"
// }
// }
// ]
}
func prettyDAGJSON(data []byte) (string, error) {
var node ipld.Node
node, err := ipld.Decode(data, dagcbor.Decode)
if err != nil {
return "", err
}
jsonData, err := ipld.Encode(node, dagjson.Encode)
if err != nil {
return "", err
}
var out bytes.Buffer
if err := json.Indent(&out, jsonData, "", " "); err != nil {
return "", err
}
return out.String(), nil
}
func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]any, prf []cid.Cid, meta map[string]any, errs error) {
var err error
privKey, iss, err = did.GenerateEd25519()
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to generate Issuer identity: %w", err))
}
_, sub, err = did.GenerateEd25519()
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to generate Subject identity: %w", err))
}
cmd, err = command.Parse("/crud/create")
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to parse command: %w", err))
}
headers := map[string]string{
"Content-Type": "application/json",
}
payload := map[string]any{
"body": "UCAN is great",
"draft": true,
"title": "UCAN for Fun and Profit",
"topics": []string{"authz", "journal"},
}
args = map[string]any{
// you can also directly pass IPLD values
"uri": basicnode.NewString("https://example.com/blog/posts"),
"headers": headers,
"payload": payload,
}
prf = make([]cid.Cid, 3)
for i, v := range []string{
"zdpuAzx4sBrBCabrZZqXgvK3NDzh7Mf5mKbG11aBkkMCdLtCp",
"zdpuApTCXfoKh2sB1KaUaVSGofCBNPUnXoBb6WiCeitXEibZy",
"zdpuAoFdXRPw4n6TLcncoDhq1Mr6FGbpjAiEtqSBrTSaYMKkf",
} {
prf[i], err = cid.Parse(v)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to parse proof cid: %w", err))
}
}
meta = map[string]any{
"env": basicnode.NewString("development"),
"tags": []string{"blog", "post", "pr#123"},
}
return // WARNING: named return values
}

View File

@@ -8,37 +8,132 @@
package invocation package invocation
import ( import (
"crypto/rand"
"errors" "errors"
"fmt" "fmt"
"time" "time"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/did" "github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/command" "github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/meta" "github.com/ucan-wg/go-ucan/pkg/meta"
"github.com/ucan-wg/go-ucan/token/delegation"
"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 invocation. // Token is an immutable type that holds the fields of a UCAN invocation.
type Token struct { type Token struct {
// Issuer DID (invoker) // The DID of the Invoker
issuer did.DID issuer did.DID
// Audience DID (receiver/executor) // The DID of Subject being invoked
audience did.DID
// Subject DID (subject being invoked)
subject did.DID subject did.DID
// The Command to invoke // The DID of the intended Executor if different from the Subject
audience did.DID
// The Command
command command.Command command command.Command
// TODO: args // The Command's arguments
// TODO: prf arguments *args.Args
// A unique, random nonce // CIDs of the delegation.Token that prove the chain of authority
nonce []byte // They need to form a strictly linear chain, and being ordered starting from the
// leaf Delegation (with aud matching the invocation's iss), in a strict sequence
// where the iss of the previous Delegation matches the aud of the next Delegation.
proof []cid.Cid
// Arbitrary Metadata // Arbitrary Metadata
meta *meta.Meta meta *meta.Meta
// A unique, random nonce
nonce []byte
// The timestamp at which the Invocation becomes invalid // The timestamp at which the Invocation becomes invalid
expiration *time.Time expiration *time.Time
// The timestamp at which the Invocation was created // The timestamp at which the Invocation was created
invokedAt *time.Time invokedAt *time.Time
// TODO: cause
// An optional CID of the Receipt that enqueued the Task
cause *cid.Cid
}
// New creates an invocation Token with the provided options.
//
// If no nonce is provided, a random 12-byte nonce is generated. Use the
// WithNonce or WithEmptyNonce options to specify provide your own nonce
// or to leave the nonce empty respectively.
//
// If no invokedAt is provided, the current time is used. Use the
// WithInvokedAt or WithInvokedAtIn Options to specify a different time
// or the WithoutInvokedAt Option to clear the Token's invokedAt field.
//
// With the exception of the WithMeta option, all others will overwrite
// the previous contents of their target field.
func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) {
iat := time.Now()
tkn := Token{
issuer: iss,
subject: sub,
command: cmd,
arguments: args.New(),
proof: prf,
meta: meta.NewMeta(),
nonce: nil,
invokedAt: &iat,
}
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
}
func (t *Token) ExecutionAllowed(loader delegation.Loader) error {
return t.executionAllowed(loader, t.arguments)
}
func (t *Token) ExecutionAllowedWithArgsHook(loader delegation.Loader, hook func(args args.ReadOnly) (*args.Args, error)) error {
newArgs, err := hook(t.arguments.ReadOnly())
if err != nil {
return err
}
return t.executionAllowed(loader, newArgs)
}
func (t *Token) executionAllowed(loader delegation.Loader, arguments *args.Args) error {
delegations, err := t.loadProofs(loader)
if err != nil {
// All referenced delegations must be available - 4b
return err
}
if err := t.verifyProofs(delegations); err != nil {
return err
}
if err := t.verifyTimeBound(delegations); err != nil {
return err
}
if err := t.verifyArgs(delegations, arguments); err != nil {
return err
}
return nil
} }
// Issuer returns the did.DID representing the Token's issuer. // Issuer returns the did.DID representing the Token's issuer.
@@ -46,40 +141,75 @@ func (t *Token) Issuer() did.DID {
return t.issuer return t.issuer
} }
// Subject returns the did.DID representing the Token's subject.
func (t *Token) Subject() did.DID {
return t.subject
}
// Audience returns the did.DID representing the Token's audience. // Audience returns the did.DID representing the Token's audience.
func (t *Token) Audience() did.DID { func (t *Token) Audience() did.DID {
return t.audience 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. // Command returns the capability's command.Command.
func (t *Token) Command() command.Command { func (t *Token) Command() command.Command {
return t.command return t.command
} }
// Arguments returns the arguments to be used when the command is
// invoked.
func (t *Token) Arguments() args.ReadOnly {
return t.arguments.ReadOnly()
}
// Proof() returns the ordered list of cid.Cid which reference the
// delegation Tokens that authorize this invocation.
func (t *Token) Proof() []cid.Cid {
return t.proof
}
// Meta returns the Token's metadata.
func (t *Token) Meta() meta.ReadOnly {
return t.meta.ReadOnly()
}
// Nonce returns the random Nonce encapsulated in this Token. // Nonce returns the random Nonce encapsulated in this Token.
func (t *Token) Nonce() []byte { func (t *Token) Nonce() []byte {
return t.nonce return t.nonce
} }
// Meta returns the Token's metadata.
func (t *Token) Meta() *meta.Meta {
return t.meta
}
// Expiration returns the time at which the Token expires. // Expiration returns the time at which the Token expires.
func (t *Token) Expiration() *time.Time { func (t *Token) Expiration() *time.Time {
return t.expiration return t.expiration
} }
// InvokedAt returns the time.Time at which the invocation token was
// created.
func (t *Token) InvokedAt() *time.Time {
return t.invokedAt
}
// Cause returns the Token's (optional) cause field which may specify
// which describes the Receipt that requested the invocation.
func (t *Token) Cause() *cid.Cid {
return t.cause
}
// 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
}
return true
}
func (t *Token) validate() error { func (t *Token) validate() error {
var errs error var errs error
@@ -90,8 +220,7 @@ func (t *Token) validate() error {
} }
requiredDID(t.issuer, "Issuer") requiredDID(t.issuer, "Issuer")
requiredDID(t.subject, "Subject")
// TODO
if len(t.nonce) < 12 { if len(t.nonce) < 12 {
errs = errors.Join(errs, fmt.Errorf("token nonce too small")) errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
@@ -100,25 +229,58 @@ func (t *Token) validate() error {
return errs return errs
} }
func (t *Token) loadProofs(loader delegation.Loader) (res []*delegation.Token, err error) {
res = make([]*delegation.Token, len(t.proof))
for i, c := range t.proof {
res[i], err = loader.GetDelegation(c)
if err != nil {
return nil, fmt.Errorf("%w: need %s", ErrMissingDelegation, c)
}
}
return res, nil
}
// tokenFromModel build a decoded view of the raw IPLD data. // tokenFromModel build a decoded view of the raw IPLD data.
// This function also serves as validation. // This function also serves as validation.
func tokenFromModel(m tokenPayloadModel) (*Token, error) { func tokenFromModel(m tokenPayloadModel) (*Token, error) {
var ( var (
tkn Token tkn Token
err error
) )
// TODO if tkn.issuer, err = did.Parse(m.Iss); err != nil {
return nil, fmt.Errorf("parse iss: %w", err)
}
if tkn.subject, err = did.Parse(m.Sub); err != nil {
return nil, fmt.Errorf("parse subject: %w", err)
}
if tkn.audience, err = parse.OptionalDID(m.Aud); err != nil {
return nil, fmt.Errorf("parse audience: %w", err)
}
if tkn.command, err = command.Parse(m.Cmd); err != nil {
return nil, fmt.Errorf("parse command: %w", err)
}
if len(m.Nonce) == 0 {
return nil, fmt.Errorf("nonce is required")
}
tkn.nonce = m.Nonce
tkn.arguments = m.Args
tkn.proof = m.Prf
tkn.meta = m.Meta
tkn.expiration = parse.OptionalTimestamp(m.Exp)
tkn.invokedAt = parse.OptionalTimestamp(m.Iat)
tkn.cause = m.Cause
if err := tkn.validate(); err != nil {
return nil, err
}
return &tkn, nil return &tkn, nil
} }
// generateNonce creates a 12-byte random nonce.
// TODO: some crypto scheme require more, is that our case?
func generateNonce() ([]byte, error) {
res := make([]byte, 12)
_, err := rand.Read(res)
if err != nil {
return nil, err
}
return res, nil
}

View File

@@ -1,23 +1,32 @@
type DID string type DID string
# The Invocation Payload attaches sender, receiver, and provenance to the Task. # The Invocation Payload attaches sender, receiver, and provenance to the Task.
type Payload struct { type Payload struct {
# Issuer DID (sender) # The DID of the invoker
iss DID iss DID
# Audience DID (receiver) # The Subject being invoked
aud DID sub DID
# Principal that the chain is about (the Subject) # The DID of the intended Executor if different from the Subject
sub optional DID aud optional DID
# The Command to eventually invoke # The Command
cmd String cmd String
# The Command's Arguments
# A unique, random nonce args { String : Any}
nonce Bytes # Delegations that prove the chain of authority
prf [ Link ]
# Arbitrary Metadata # Arbitrary Metadata
meta {String : Any} meta optional { String : Any }
# A unique, random nonce
nonce optional Bytes
# The timestamp at which the Invocation becomes invalid # The timestamp at which the Invocation becomes invalid
exp nullable Int exp nullable Int
# The Timestamp at which the Invocation was created
iat optional Int
# An optional CID of the Receipt that enqueued the Task
cause optional Link
} }

View File

@@ -0,0 +1,139 @@
package invocation_test
import (
"testing"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
"github.com/ucan-wg/go-ucan/token/invocation"
)
const (
missingPrivKeyCfg = "CAESQMjRvrEIjpPYRQKmkAGw/pV0XgE958rYa4vlnKJjl1zz/sdnGnyV1xKLJk8D39edyjhHWyqcpgFnozQK62SG16k="
missingTknCIDStr = "bafyreigwypmw6eul6vadi6g6lnfbsfo2zck7gfzsbjoroqs3djhnzzc7mm"
missingDIDStr = "did:key:z6MkwboxFsH3kEuehBZ5fLkRmxi68yv1u38swA4r9Jm2VRma"
)
var emptyArguments = args.New()
func TestToken_ExecutionAllowed(t *testing.T) {
t.Parallel()
t.Run("passes - only root", func(t *testing.T) {
t.Parallel()
testPasses(t, didtest.PersonaBob, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBob)
})
t.Run("passes - valid chain", func(t *testing.T) {
t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
})
t.Run("passes - proof chain attenuates command", func(t *testing.T) {
t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand)
})
t.Run("passes - invocation attenuates command", func(t *testing.T) {
t.Parallel()
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
})
t.Run("fails - no proof", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrNoProof, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofEmpty)
})
t.Run("fails - missing referenced delegation", func(t *testing.T) {
t.Parallel()
missingTknCID, err := cid.Parse(missingTknCIDStr)
require.NoError(t, err)
prf := []cid.Cid{missingTknCID, delegationtest.TokenAliceBobCID}
testFails(t, invocation.ErrMissingDelegation, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, prf)
})
t.Run("fails - referenced delegation expired", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpired)
})
t.Run("fails - referenced delegation inactive", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidInactive)
})
t.Run("fails - last (or only) delegation not root", func(t *testing.T) {
t.Parallel()
prf := []cid.Cid{delegationtest.TokenErinFrankCID, delegationtest.TokenDanErinCID, delegationtest.TokenCarolDanCID}
testFails(t, invocation.ErrLastNotRoot, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf)
})
t.Run("fails - broken chain", func(t *testing.T) {
t.Parallel()
prf := []cid.Cid{delegationtest.TokenCarolDanCID, delegationtest.TokenAliceBobCID}
testFails(t, invocation.ErrBrokenChain, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf)
})
t.Run("fails - first not issued to invoker", func(t *testing.T) {
t.Parallel()
prf := []cid.Cid{delegationtest.TokenBobCarolCID, delegationtest.TokenAliceBobCID}
testFails(t, invocation.ErrBrokenChain, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf)
})
t.Run("fails - proof chain expands command", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand)
})
t.Run("fails - invocation expands command", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
})
t.Run("fails - inconsistent subject", func(t *testing.T) {
t.Parallel()
testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject)
})
}
func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) error {
t.Helper()
// TODO: use the args and add minimal test to check that they are verified against the policy
tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...)
require.NoError(t, err)
return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader())
}
func testFails(t *testing.T, expErr error, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) {
err := test(t, persona, cmd, args, prf, opts...)
require.ErrorIs(t, err, expErr)
}
func testPasses(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) {
err := test(t, persona, cmd, args, prf, opts...)
require.NoError(t, err)
}

View File

@@ -1,6 +1,7 @@
package invocation package invocation
import ( import (
"fmt"
"io" "io"
"github.com/ipfs/go-cid" "github.com/ipfs/go-cid"
@@ -193,14 +194,21 @@ func FromIPLD(node datamodel.Node) (*Token, error) {
} }
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) { func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
var sub *string // sanity check that privKey and issuer are matching
issPub, err := t.issuer.PubKey()
if t.subject != did.Undef { if err != nil {
s := t.subject.String() return nil, err
sub = &s }
if !issPub.Equals(privKey.GetPublic()) {
return nil, fmt.Errorf("private key doesn't match the issuer")
} }
// TODO var aud *string
if t.audience != did.Undef {
a := t.audience.String()
aud = &a
}
var exp *int64 var exp *int64
if t.expiration != nil { if t.expiration != nil {
@@ -208,14 +216,29 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
exp = &u exp = &u
} }
var iat *int64
if t.invokedAt != nil {
i := t.invokedAt.Unix()
iat = &i
}
model := &tokenPayloadModel{ model := &tokenPayloadModel{
Iss: t.issuer.String(), Iss: t.issuer.String(),
Aud: t.audience.String(), Aud: aud,
Sub: sub, Sub: t.subject.String(),
Cmd: t.command.String(), Cmd: t.command.String(),
Args: t.arguments,
Prf: t.proof,
Meta: t.meta,
Nonce: t.nonce, Nonce: t.nonce,
Meta: *t.meta,
Exp: exp, Exp: exp,
Iat: iat,
Cause: t.cause,
}
// seems like it's a requirement to have a null meta if there are no values?
if len(model.Meta.Keys) == 0 {
model.Meta = nil
} }
return envelope.ToIPLD(privKey, model) return envelope.ToIPLD(privKey, model)

View File

@@ -0,0 +1,38 @@
package invocation_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/token/invocation"
)
func TestSealUnsealRoundtrip(t *testing.T) {
t.Parallel()
privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
require.NoError(t, err)
tkn1, err := invocation.New(iss, sub, cmd, prf,
invocation.WithArgument("uri", args["uri"]),
invocation.WithArgument("headers", args["headers"]),
invocation.WithArgument("payload", args["payload"]),
invocation.WithMeta("env", "development"),
invocation.WithMeta("tags", meta["tags"]),
invocation.WithExpirationIn(time.Minute),
invocation.WithoutInvokedAt(),
)
require.NoError(t, err)
data, cid1, err := tkn1.ToSealed(privKey)
require.NoError(t, err)
tkn2, cid2, err := invocation.FromSealed(data)
require.NoError(t, err)
assert.Equal(t, cid1, cid2)
assert.Equal(t, tkn1, tkn2)
}

141
token/invocation/options.go Normal file
View File

@@ -0,0 +1,141 @@
package invocation
import (
"time"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/did"
)
// Option is a type that allows optional fields to be set during the
// creation of an invocation Token.
type Option func(*Token) error
// WithArgument adds a key/value pair to the Token's Arguments field.
func WithArgument(key string, val any) Option {
return func(t *Token) error {
return t.arguments.Add(key, val)
}
}
// WithAudience sets the Token's audience to the provided did.DID.
//
// If the provided did.DID is the same as the Token's subject, the
// audience is not set.
func WithAudience(aud did.DID) Option {
return func(t *Token) error {
if t.subject.String() != aud.String() {
t.audience = aud
}
return nil
}
}
// WithMeta adds a key/value pair in the "meta" field.
//
// WithMeta can be used multiple times in the same call.
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
// and ipld.Node.
func WithMeta(key string, val any) Option {
return func(t *Token) error {
return t.meta.Add(key, val)
}
}
// WithEncryptedMetaString adds a key/value pair in the "meta" field.
// The string value is encrypted with the given aesKey.
func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option {
return func(t *Token) error {
return t.meta.AddEncrypted(key, val, encryptionKey)
}
}
// WithEncryptedMetaBytes adds a key/value pair in the "meta" field.
// The []byte value is encrypted with the given aesKey.
func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option {
return func(t *Token) error {
return t.meta.AddEncrypted(key, val, encryptionKey)
}
}
// WithNonce sets the Token's nonce with the given value.
//
// If this option is not used, a random 12-byte nonce is generated for
// this required field. If you truly want to create an invocation Token
// without a nonce, use the WithEmptyNonce Option which will set the
// nonce to an empty byte array.
func WithNonce(nonce []byte) Option {
return func(t *Token) error {
t.nonce = nonce
return nil
}
}
// WithEmptyNonce sets the Token's nonce to an empty byte slice as
// suggested by the UCAN spec for invocation tokens that represent
// idempotent operations.
func WithEmptyNonce() Option {
return func(t *Token) error {
t.nonce = []byte{}
return nil
}
}
// WithExpiration set's the Token's optional "expiration" field to the
// value of the provided time.Time.
func WithExpiration(exp time.Time) Option {
return func(t *Token) error {
exp = exp.Round(time.Second)
t.expiration = &exp
return nil
}
}
// WithExpirationIn set's the Token's optional "expiration" field to
// Now() plus the given duration.
func WithExpirationIn(after time.Duration) Option {
return WithExpiration(time.Now().Add(after))
}
// WithInvokedAt sets the Token's invokedAt field to the provided
// time.Time.
//
// If this Option is not provided, the invocation Token's iat field will
// be set to the value of time.Now(). If you want to create an invocation
// Token without this field being set, use the WithoutInvokedAt Option.
func WithInvokedAt(iat time.Time) Option {
return func(t *Token) error {
t.invokedAt = &iat
return nil
}
}
// WithInvokedAtIn sets the Token's invokedAt field to Now() plus the
// given duration.
func WithInvokedAtIn(after time.Duration) Option {
return WithInvokedAt(time.Now().Add(after))
}
// WithoutInvokedAt clears the Token's invokedAt field.
func WithoutInvokedAt() Option {
return func(t *Token) error {
t.invokedAt = nil
return nil
}
}
// WithCause sets the Token's cause field to the provided cid.Cid.
func WithCause(cause *cid.Cid) Option {
return func(t *Token) error {
t.cause = cause
return nil
}
}

141
token/invocation/proof.go Normal file
View File

@@ -0,0 +1,141 @@
package invocation
import (
"fmt"
"time"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation"
)
// # Invocation token validation
//
// Per the specification, invocation Tokens must be validated before the command is executed.
// This validation effectively happens in multiple places in the codebase.
// Steps 1 and 2 are the same for all token types.
//
// 1. When a token is read/unsealed from its containing envelope (`envelope` package):
// a. The envelope can be decoded.
// b. The envelope contains a Signature, VarsigHeader and Payload.
// c. The Payload contains an iss field that contains a valid `did:key`.
// d. The public key can be extracted from the `did:key`.
// e. The public key type is supported by go-ucan.
// f. The Signature can be decoded per the VarsigHeader.
// g. The SigPayload can be verified using the Signature and public key.
// h. The field key of the TokenPayload matches the expected tag.
//
// 2. When the token is created or passes step one (token constructor or decoder):
// a. All required fields are present
// b. All populated fields respect their own rules (example: a policy is legal)
//
// 3. When an unsealed invocation passes steps one and two for execution (verifyTimeBound below):
// a. The invocation cannot be expired (expiration in the future or absent).
// b. All the delegation must not be expired (expiration in the future or absent).
// c. All the delegation must be active (nbf in the past or absent).
//
// 4. When the proof chain is being validated (verifyProofs below):
// a. There must be at least one delegation in the proof chain.
// b. All referenced delegations must be available.
// c. The first proof must be issued to the Invoker (audience DID).
// d. The Issuer of each delegation must be the Audience in the next one.
// e. The last token must be a root delegation.
// f. The Subject of each delegation must equal the invocation's Audience field.
// g. The command of each delegation must "allow" the one before it.
//
// 5. If steps 1-4 pass:
// a. The policy must "match" the arguments. (verifyArgs below)
// b. The nonce (if present) is not reused. (out of scope for go-ucan)
// verifyProofs controls that the proof chain allows the invocation:
// - principal alignment
// - command alignment
func (t *Token) verifyProofs(delegations []*delegation.Token) error {
// There must be at least one delegation referenced - 4a
if len(delegations) < 1 {
return ErrNoProof
}
cmd := t.command
iss := t.issuer
aud := t.audience
if !aud.Defined() {
aud = t.subject
}
// control from the invocation to the root
for i, dlgCid := range t.proof {
dlg := delegations[i]
// The Subject of each delegation must equal the invocation's Audience field. - 4f
if dlg.Subject() != aud {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject())
}
// The first proof must be issued to the Invoker (audience DID). - 4c
// The Issuer of each delegation must be the Audience in the next one. - 4d
if dlg.Audience() != iss {
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())
}
iss = dlg.Issuer()
// The command of each delegation must "allow" the one before it. - 4g
if !dlg.Command().Covers(cmd) {
return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd)
}
cmd = dlg.Command()
}
// The last prf value must be a root delegation (have the issuer field match the Subject field) - 4e
if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() {
return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer())
}
return nil
}
func (t *Token) verifyTimeBound(dlgs []*delegation.Token) error {
return t.verifyTimeBoundAt(time.Now(), dlgs)
}
func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error {
// The invocation cannot be expired (expiration in the future or absent). - 3a
if !t.IsValidAt(at) {
return fmt.Errorf("%w: invocation", ErrTokenInvalidNow)
}
for i, dlgCid := range t.proof {
dlg := delegations[i]
// All the delegation must not be expired (expiration in the future or absent). - 3b
// All the delegation must be active (nbf in the past or absent). - 3c
if !dlg.IsValidAt(at) {
return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid)
}
}
return nil
}
func (t *Token) verifyArgs(delegations []*delegation.Token, arguments *args.Args) error {
var count int
for i := range t.proof {
count += len(delegations[i].Policy())
}
policies := make(policy.Policy, 0, count)
for i := range t.proof {
policies = append(policies, delegations[i].Policy()...)
}
argsIpld, err := arguments.ToIPLD()
if err != nil {
return err
}
ok, statement := policies.Match(argsIpld)
if !ok {
return fmt.Errorf("the following UCAN policy is not satisfied: %v", statement.String())
}
return nil
}

View File

@@ -5,10 +5,12 @@ import (
"fmt" "fmt"
"sync" "sync"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/node/bindnode"
"github.com/ipld/go-ipld-prime/schema" "github.com/ipld/go-ipld-prime/schema"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/meta" "github.com/ucan-wg/go-ucan/pkg/meta"
"github.com/ucan-wg/go-ucan/token/internal/envelope" "github.com/ucan-wg/go-ucan/token/internal/envelope"
) )
@@ -23,17 +25,17 @@ const Tag = "ucan/inv@1.0.0-rc.1"
var schemaBytes []byte var schemaBytes []byte
var ( var (
once sync.Once once sync.Once
ts *schema.TypeSystem ts *schema.TypeSystem
err error errSchema error
) )
func mustLoadSchema() *schema.TypeSystem { func mustLoadSchema() *schema.TypeSystem {
once.Do(func() { once.Do(func() {
ts, err = ipld.LoadSchemaBytes(schemaBytes) ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
}) })
if err != nil { if errSchema != nil {
panic(fmt.Errorf("failed to load IPLD schema: %s", err)) panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
} }
return ts return ts
} }
@@ -44,28 +46,34 @@ func payloadType() schema.Type {
var _ envelope.Tokener = (*tokenPayloadModel)(nil) var _ envelope.Tokener = (*tokenPayloadModel)(nil)
// TODO
type tokenPayloadModel struct { type tokenPayloadModel struct {
// Issuer DID (sender) // The DID of the Invoker
Iss string Iss string
// Audience DID (receiver) // The DID of Subject being invoked
Aud string Sub string
// Principal that the chain is about (the Subject) // The DID of the intended Executor if different from the Subject
// optional: can be nil Aud *string
Sub *string
// The Command to eventually invoke // The Command
Cmd string Cmd string
// The Command's Arguments
Args *args.Args
// Delegations that prove the chain of authority
Prf []cid.Cid
// Arbitrary Metadata
Meta *meta.Meta
// A unique, random nonce // A unique, random nonce
Nonce []byte Nonce []byte
// Arbitrary Metadata
Meta meta.Meta
// The timestamp at which the Invocation becomes invalid // The timestamp at which the Invocation becomes invalid
// optional: can be nil // optional: can be nil
Exp *int64 Exp *int64
// The timestamp at which the Invocation was created
Iat *int64
// An optional CID of the Receipt that enqueued the Task
Cause *cid.Cid
} }
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype { func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {

View File

@@ -0,0 +1,102 @@
package invocation_test
import (
"bytes"
"fmt"
"testing"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gotest.tools/v3/golden"
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/token/internal/envelope"
"github.com/ucan-wg/go-ucan/token/invocation"
)
const (
issuerPrivKeyCfg = "CAESQK45xBfqIxRp7ZdRdck3tIJZKocCqvANQc925dCJhFwO7DJNA2j94zkF0TNx5mpXV0s6utfkFdHddWTaPVU6yZc="
newCID = "zdpuAqY6Zypg4UnpbSUgDvYGneyFaTKaZevzxgSxV4rmv3Fpp"
)
func TestSchemaRoundTrip(t *testing.T) {
t.Parallel()
invocationJson := golden.Get(t, "new.dagjson")
privKey := privKey(t, issuerPrivKeyCfg)
t.Run("via buffers", func(t *testing.T) {
t.Parallel()
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
p1, err := invocation.FromDagJson(invocationJson)
require.NoError(t, err)
cborBytes, id, err := p1.ToSealed(privKey)
require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
fmt.Println("cborBytes length", len(cborBytes))
fmt.Println("cbor", string(cborBytes))
p2, c2, err := invocation.FromSealed(cborBytes)
require.NoError(t, err)
assert.Equal(t, id, c2)
fmt.Println("read Cbor", p2)
readJson, err := p2.ToDagJson(privKey)
require.NoError(t, err)
fmt.Println("readJson length", len(readJson))
fmt.Println("json: ", string(readJson))
assert.JSONEq(t, string(invocationJson), string(readJson))
})
t.Run("via streaming", func(t *testing.T) {
t.Parallel()
buf := bytes.NewBuffer(invocationJson)
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
p1, err := invocation.FromDagJsonReader(buf)
require.NoError(t, err)
cborBytes := &bytes.Buffer{}
id, err := p1.ToSealedWriter(cborBytes, privKey)
require.NoError(t, err)
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
p2, c2, err := invocation.FromSealedReader(cborBytes)
require.NoError(t, err)
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(c2))
readJson := &bytes.Buffer{}
require.NoError(t, p2.ToDagJsonWriter(readJson, privKey))
assert.JSONEq(t, string(invocationJson), readJson.String())
})
t.Run("fails with wrong PrivKey", func(t *testing.T) {
t.Parallel()
p1, err := invocation.FromDagJson(invocationJson)
require.NoError(t, err)
_, _, err = p1.ToSealed(didtest.PersonaBob.PrivKey())
require.EqualError(t, err, "private key doesn't match the issuer")
})
}
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
require.NoError(t, err)
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
require.NoError(t, err)
return privKey
}

1
token/invocation/testdata/new.dagjson vendored Normal file
View File

@@ -0,0 +1 @@
[{"/":{"bytes":"o/vTvTs8SEkD9QL/eNhhW0fAng/SGBouywCbUnOfsF2RFHxaV02KTCyzgDxlJLZ2XN/Vk5igLmlKL3QIXMaeCQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/inv@1.0.0-rc.1":{"args":{"headers":{"Content-Type":"application/json"},"payload":{"body":"UCAN is great","draft":true,"title":"UCAN for Fun and Profit","topics":["authz","journal"]},"uri":"https://example.com/blog/posts"},"cmd":"/crud/create","exp":1730812145,"iss":"did:key:z6MkvMGkN5nbUQLBVqJhr13Zdqyh9rR1VuF16PuZbfocBxpv","meta":{"env":"development","tags":["blog","post","pr#123"]},"nonce":{"/":{"bytes":"q1AH6MJrqoTH6av7"}},"prf":[{"/":"bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"},{"/":"bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"},{"/":"bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"}],"sub":"did:key:z6MkuFj35aiTL7YQiVMobuSeUQju92g7wZzufS3HAc6NFFcQ"}}]