Compare commits
181 Commits
match-trac
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82bce556f | ||
|
|
a54d66afe5 | ||
|
|
14a57d7391 | ||
|
|
68469db91a | ||
|
|
cc1d68be0c | ||
|
|
6d3846ac62 | ||
|
|
6aa33b1547 | ||
|
|
9589cc8b44 | ||
|
|
879c0ab03b | ||
|
|
fe14765c8d | ||
|
|
1b28cb49bf | ||
|
|
e1fc838caf | ||
|
|
f29b9e94fc | ||
|
|
506ed21b94 | ||
|
|
126177b9e5 | ||
|
|
2bddab8b0c | ||
|
|
45ead12131 | ||
|
|
9d5e170409 | ||
|
|
9d047f038d | ||
|
|
4c5afcb084 | ||
|
|
e218b49577 | ||
|
|
4c81ac778e | ||
|
|
7ae65e7c8e | ||
|
|
7f9cb6426c | ||
|
|
9c141029c3 | ||
|
|
0c6717cfbc | ||
|
|
6d7fd28324 | ||
|
|
ccc0120e78 | ||
|
|
bf27b97a57 | ||
|
|
10dd4fa6d1 | ||
|
|
5695609f8b | ||
|
|
b7ddead7c8 | ||
|
|
9cbec37685 | ||
|
|
00dbd975b1 | ||
|
|
c792a4cce5 | ||
|
|
8f3f1c775e | ||
|
|
704ed25768 | ||
|
|
95bdbc4fc5 | ||
|
|
416345dba9 | ||
|
|
042d6dc52f | ||
|
|
8bb3a4f4d0 | ||
|
|
47156a8ad6 | ||
|
|
ce6d163627 | ||
|
|
c3c2c96008 | ||
|
|
903632695f | ||
|
|
f2d75b7815 | ||
|
|
4f09829abe | ||
|
|
5660df32b5 | ||
|
|
2f2a74c7ec | ||
|
|
0592717637 | ||
|
|
80c2d60ab3 | ||
|
|
c518c6657a | ||
|
|
78825f4f55 | ||
|
|
7f1adbd945 | ||
|
|
0f59088d0b | ||
|
|
72e0f353e7 | ||
|
|
d0d4ec3abe | ||
|
|
bb24081b28 | ||
|
|
3688ccea01 | ||
|
|
e9105896d7 | ||
|
|
15751c7362 | ||
|
|
d52218fa5a | ||
|
|
64d3024dec | ||
|
|
78d37d92ef | ||
|
|
da806b1bc5 | ||
|
|
311b942a6d | ||
|
|
56eab758ed | ||
|
|
105323b989 | ||
|
|
5b816ccc62 | ||
|
|
28272e6900 | ||
|
|
a854389e32 | ||
|
|
117a75e2c4 | ||
|
|
a25bfbaf45 | ||
|
|
bff482f73b | ||
|
|
ff79bbb443 | ||
|
|
3997a86184 | ||
|
|
200d6a8ae2 | ||
|
|
0349e7e463 | ||
|
|
dff52f80c4 | ||
|
|
5b7a63a2c6 | ||
|
|
66675f7030 | ||
|
|
7e54be49e1 | ||
|
|
15535d3474 | ||
|
|
170e597e71 | ||
|
|
ce1a4b6e32 | ||
|
|
d1d047cd9e | ||
|
|
3680637090 | ||
|
|
1166a68e5c | ||
|
|
ba4db9bce8 | ||
|
|
20369dba49 | ||
|
|
ade2c7f858 | ||
|
|
943a318b26 | ||
|
|
2d79cdc54e | ||
|
|
60bdc8873b | ||
|
|
820057e41e | ||
|
|
ba0038b0ae | ||
|
|
4a4b200312 | ||
|
|
caae2f58bf | ||
|
|
ec627138cb | ||
|
|
4ec409edc6 | ||
|
|
c61fc8d8b3 | ||
|
|
d90715d1fe | ||
|
|
5f8536e480 | ||
|
|
c19e38356d | ||
|
|
aea1880386 | ||
|
|
2fb5a3dc01 | ||
|
|
e980d6c0b9 | ||
|
|
1098e76cba | ||
|
|
bb36d61d93 | ||
|
|
417ef78570 | ||
|
|
00d2380f14 | ||
|
|
8ca088bf27 | ||
|
|
25ca34923f | ||
|
|
fc4c8f2de1 | ||
|
|
64b989452f | ||
|
|
92065ca0d3 | ||
|
|
814cec1495 | ||
|
|
0f70557309 | ||
|
|
89e4d5d419 | ||
|
|
9057cbcba6 | ||
|
|
98d9cadcbd | ||
|
|
e938d64220 | ||
|
|
c577d73f3e | ||
|
|
be185a8496 | ||
|
|
17a57c622a | ||
|
|
6298fa28bd | ||
|
|
d3e97aaa08 | ||
|
|
fdff79d23a | ||
|
|
a26d836025 | ||
|
|
9f47418bdf | ||
|
|
81c7a0f80d | ||
|
|
3987e8649c | ||
|
|
17a1d54b6f | ||
|
|
7cb0f97b30 | ||
|
|
c4a53f42b6 | ||
|
|
522181b16a | ||
|
|
633b3d210a | ||
|
|
3c705ca150 | ||
|
|
1fa2b5e6fc | ||
|
|
11bc085c60 | ||
|
|
a4a8634eb8 | ||
|
|
d353dfe652 | ||
|
|
1e5ecdc205 | ||
|
|
f9065d39d8 | ||
|
|
cddade4670 | ||
|
|
948087744d | ||
|
|
bfb93d6988 | ||
|
|
cfcb199818 | ||
|
|
85557ab6b5 | ||
|
|
adc2b8d0da | ||
|
|
bcdaf0cca3 | ||
|
|
d754c5837b | ||
|
|
d89fb395e3 | ||
|
|
4932e32052 | ||
|
|
a52b48cf47 | ||
|
|
e6e4d85381 | ||
|
|
962e897ff5 | ||
|
|
58bb5cdb8f | ||
|
|
ce7f653ab0 | ||
|
|
7d4f973171 | ||
|
|
3dc0011628 | ||
|
|
08f821f23d | ||
|
|
1b61f2e4db | ||
|
|
187e7a869c | ||
|
|
a98653b769 | ||
|
|
31d16ac468 | ||
|
|
d2b004c405 | ||
|
|
b4e222f8a0 | ||
|
|
824c8fe523 | ||
|
|
a1aaf47d7c | ||
|
|
728696f169 | ||
|
|
f2b4c3ac20 | ||
|
|
7a7db684c3 | ||
|
|
d7454156d2 | ||
|
|
d3ad6715d9 | ||
|
|
602bdf9c7a | ||
|
|
d21c17c4ca | ||
|
|
76c015e78b | ||
|
|
f44cf8af78 | ||
|
|
2b2fc4a13f | ||
|
|
d784c92c29 |
4
.github/workflows/bench.yml
vendored
4
.github/workflows/bench.yml
vendored
@@ -2,7 +2,7 @@ name: go continuous benchmark
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v1
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "stable"
|
||||
- name: Run benchmark
|
||||
|
||||
4
.github/workflows/gotest.yml
vendored
4
.github/workflows/gotest.yml
vendored
@@ -13,8 +13,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
name: ${{ matrix.os}} (go ${{ matrix.go }})
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
- name: Go information
|
||||
|
||||
77
Readme.md
Normal file
77
Readme.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<div align="center">
|
||||
<a href="https://github.com/ucan-wg/go-ucan" target="_blank">
|
||||
<img src="https://raw.githubusercontent.com/ucan-wg/go-ucan/v1/assets/logo.png" alt="go-ucan Logo" height="250"></img>
|
||||
</a>
|
||||
|
||||
<h1 align="center">go-ucan</h1>
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/badge/UCAN-v1.0.0--rc.1-blue" alt="UCAN v1.0.0-rc.1">
|
||||
<a href="https://github.com/ucan-wg/go-ucan/tags">
|
||||
<img alt="GitHub Tag" src="https://img.shields.io/github/v/tag/ucan-wg/go-ucan">
|
||||
</a>
|
||||
<a href="https://github.com/ucan-wg/go-ucan/actions?query=">
|
||||
<img src="https://github.com/ucan-wg/go-ucan/actions/workflows/gotest.yml/badge.svg" alt="Build Status">
|
||||
</a>
|
||||
<a href="https://ucan-wg.github.io/go-ucan/dev/bench/">
|
||||
<img alt="Go benchmarks" src="https://img.shields.io/badge/Benchmarks-go-blue">
|
||||
</a>
|
||||
<a href="https://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md">
|
||||
<img alt="Apache 2.0 + MIT License" src="https://img.shields.io/badge/License-Apache--2.0+MIT-green">
|
||||
</a>
|
||||
<a href="https://pkg.go.dev/github.com/ucan-wg/go-ucan">
|
||||
<img src="https://img.shields.io/badge/Docs-godoc-blue" alt="Docs">
|
||||
</a>
|
||||
<a href="https://discord.gg/JSyFG6XgVM">
|
||||
<img src="https://img.shields.io/static/v1?label=Discord&message=join%20us!&color=mediumslateblue" alt="Discord">
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
This is a go library to help the next generation of web and decentralized applications make use
|
||||
of UCANs in their authorization flows.
|
||||
|
||||
User Controlled Authorization Networks (UCANs) are a way of doing authorization where users are fully in control. OAuth is designed for a centralized world, UCAN is the distributed user controlled version.
|
||||
|
||||
## Resources
|
||||
|
||||
### Specifications
|
||||
|
||||
The UCAN specification is separated in multiple sub-spec:
|
||||
- [Main specification](https://github.com/ucan-wg/spec)
|
||||
- [Delegation](https://github.com/ucan-wg/delegation/tree/v1_ipld)
|
||||
- [Invocation](https://github.com/ucan-wg/invocation)
|
||||
|
||||
Not implemented yet:
|
||||
- [Revocation](https://github.com/ucan-wg/revocation/tree/first-draft)
|
||||
- [Promise](https://github.com/ucan-wg/promise/tree/v1-rc1)
|
||||
|
||||
### Talks
|
||||
|
||||
- [Decentralizing Auth, and UCAN Too - Brooklyn Zelenka (2023)](https://www.youtube.com/watch?v=MuHfrqw9gQA)
|
||||
- [What's New in UCAN 1.0 - Brooklyn Zelenka (2024)](https://www.youtube.com/watch?v=-uohQzZcwF4)
|
||||
|
||||
## Status
|
||||
|
||||
`go-ucan` currently support the required parts of the UCAN specification: the main specification, delegation and invocation.
|
||||
|
||||
Besides that, `go-ucan` also includes:
|
||||
- a simplified [DID](https://www.w3.org/TR/did-core/) and [did-key](https://w3c-ccg.github.io/did-method-key/) implementation
|
||||
- a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together, see [SPEC](pkg/container/SPEC.md)
|
||||
- support for encrypted values in token's metadata
|
||||
|
||||
## Getting Help
|
||||
|
||||
For usage questions, usecases, or issues reach out to us in our `go-ucan`
|
||||
[Discord channel](https://discord.gg/3EHEQ6M8BC).
|
||||
|
||||
We would be happy to try to answer your question or try opening a new issue on
|
||||
Github.
|
||||
|
||||
## UCAN Gopher
|
||||
|
||||
Artwork by [Bruno Monts](https://www.instagram.com/bruno_monts). Thank you [Renee French](http://reneefrench.blogspot.com/) for creating the [Go Gopher](https://blog.golang.org/gopher)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the double license [Apache 2.0 + MIT](https://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md).
|
||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -78,7 +78,7 @@ func MustParse(str string) 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
|
||||
return d.code != 0 || len(d.bytes) > 0
|
||||
}
|
||||
|
||||
// PubKey returns the public key encapsulated by the did:key.
|
||||
@@ -102,6 +102,9 @@ func (d DID) PubKey() (crypto.PubKey, error) {
|
||||
|
||||
// String formats the decentralized identity document (DID) as a string.
|
||||
func (d DID) String() string {
|
||||
if d == Undef {
|
||||
return "(undefined)"
|
||||
}
|
||||
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
|
||||
return "did:key:" + key
|
||||
}
|
||||
|
||||
150
did/didtest/crypto.go
Normal file
150
did/didtest/crypto.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Package didtest provides Personas that can be used for testing. Each
|
||||
// Persona has a name, crypto.PrivKey and associated crypto.PubKey and
|
||||
// did.DID.
|
||||
package didtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
)
|
||||
|
||||
const (
|
||||
alicePrivKeyB64 = "CAESQHdNJLBBiuc1AdwPHBkubB2KS1p0cv2JEF7m8tfwtrcm5ajaYPm+XmVCmtcHOF2lGDlmaiDA7emfwD3IrcyES0M="
|
||||
bobPrivKeyB64 = "CAESQHBz+AIop1g+9iBDj+ufUc/zm9/ry7c6kDFO8Wl/D0+H63V9hC6s9l4npf3pYEFCjBtlR0AMNWMoFQKSlYNKo20="
|
||||
carolPrivKeyB64 = "CAESQPrCgkcHnYFXDT9AlAydhPECBEivEuuVx9dJxLjVvDTmJIVNivfzg6H4mAiPfYS+5ryVVUZTHZBzvMuvvvG/Ks0="
|
||||
danPrivKeyB64 = "CAESQCgNhzofKhC+7hW6x+fNd7iMPtQHeEmKRhhlduf/I7/TeOEFYAEflbJ0sAhMeDJ/HQXaAvsWgHEbJ3ZLhP8q2B0="
|
||||
erinPrivKeyB64 = "CAESQKhCJo5UBpQcthko8DKMFsbdZ+qqQ5oc01CtLCqrE90dF2GfRlrMmot3WPHiHGCmEYi5ZMEHuiSI095e/6O4Bpw="
|
||||
frankPrivKeyB64 = "CAESQDlXPKsy3jHh7OWTWQqyZF95Ueac5DKo7xD0NOBE5F2BNr1ZVxRmJ2dBELbOt8KP9sOACcO9qlCB7uMA1UQc7sk="
|
||||
)
|
||||
|
||||
// Persona is a generic participant used for cryptographic testing.
|
||||
type Persona int
|
||||
|
||||
// The provided Personas were selected from the first few generic
|
||||
// participants listed in this [table].
|
||||
//
|
||||
// [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems
|
||||
const (
|
||||
PersonaAlice Persona = iota + 1
|
||||
PersonaBob
|
||||
PersonaCarol
|
||||
PersonaDan
|
||||
PersonaErin
|
||||
PersonaFrank
|
||||
)
|
||||
|
||||
var privKeys map[Persona]crypto.PrivKey
|
||||
|
||||
func init() {
|
||||
privKeys = make(map[Persona]crypto.PrivKey, 6)
|
||||
for persona, privKeyCfg := range privKeyB64() {
|
||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
privKeys[persona] = privKey
|
||||
}
|
||||
}
|
||||
|
||||
// DID returns a did.DID based on the Persona's Ed25519 public key.
|
||||
func (p Persona) DID() did.DID {
|
||||
d, err := did.FromPrivKey(p.PrivKey())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Name returns the username of the Persona.
|
||||
func (p Persona) Name() string {
|
||||
name, ok := map[Persona]string{
|
||||
PersonaAlice: "Alice",
|
||||
PersonaBob: "Bob",
|
||||
PersonaCarol: "Carol",
|
||||
PersonaDan: "Dan",
|
||||
PersonaErin: "Erin",
|
||||
PersonaFrank: "Frank",
|
||||
}[p]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// PrivKey returns the Ed25519 private key for the Persona.
|
||||
func (p Persona) PrivKey() crypto.PrivKey {
|
||||
res, ok := privKeys[p]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (p Persona) PrivKeyConfig() string {
|
||||
res, ok := privKeyB64()[p]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// PubKey returns the Ed25519 public key for the Persona.
|
||||
func (p Persona) PubKey() crypto.PubKey {
|
||||
return p.PrivKey().GetPublic()
|
||||
}
|
||||
|
||||
// PubKeyConfig returns the marshaled and encoded Ed25519 public key
|
||||
// for the Persona.
|
||||
func (p Persona) PubKeyConfig() string {
|
||||
pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey().GetPublic())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return crypto.ConfigEncodeKey(pubKeyMar)
|
||||
}
|
||||
|
||||
func privKeyB64() map[Persona]string {
|
||||
return map[Persona]string{
|
||||
PersonaAlice: alicePrivKeyB64,
|
||||
PersonaBob: bobPrivKeyB64,
|
||||
PersonaCarol: carolPrivKeyB64,
|
||||
PersonaDan: danPrivKeyB64,
|
||||
PersonaErin: erinPrivKeyB64,
|
||||
PersonaFrank: frankPrivKeyB64,
|
||||
}
|
||||
}
|
||||
|
||||
// Personas returns an (alphabetically) ordered list of the defined
|
||||
// Persona values.
|
||||
func Personas() []Persona {
|
||||
return []Persona{
|
||||
PersonaAlice,
|
||||
PersonaBob,
|
||||
PersonaCarol,
|
||||
PersonaDan,
|
||||
PersonaErin,
|
||||
PersonaFrank,
|
||||
}
|
||||
}
|
||||
|
||||
// DidToName retrieve the persona's name from its DID.
|
||||
func DidToName(d did.DID) string {
|
||||
return map[did.DID]string{
|
||||
PersonaAlice.DID(): "Alice",
|
||||
PersonaBob.DID(): "Bob",
|
||||
PersonaCarol.DID(): "Carol",
|
||||
PersonaDan.DID(): "Dan",
|
||||
PersonaErin.DID(): "Erin",
|
||||
PersonaFrank.DID(): "Frank",
|
||||
}[d]
|
||||
}
|
||||
16
go.mod
16
go.mod
@@ -4,16 +4,17 @@ go 1.23
|
||||
|
||||
require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/ipfs/go-cid v0.4.1
|
||||
github.com/ipfs/go-cid v0.5.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/lestrrat-go/jwx/v2 v2.1.3
|
||||
github.com/libp2p/go-libp2p v0.33.0
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/multiformats/go-multibase v0.2.0
|
||||
github.com/multiformats/go-multicodec v0.9.0
|
||||
github.com/multiformats/go-multihash v0.2.3
|
||||
github.com/multiformats/go-varint v0.0.7
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
gotest.tools/v3 v3.5.1
|
||||
)
|
||||
|
||||
@@ -21,7 +22,7 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // 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.9 // 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
|
||||
@@ -34,9 +35,8 @@ require (
|
||||
github.com/polydawn/refmt v0.89.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/sys v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
|
||||
41
go.sum
41
go.sum
@@ -16,14 +16,14 @@ 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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
|
||||
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
|
||||
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -36,14 +36,14 @@ github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCG
|
||||
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/jwx/v2 v2.1.3 h1:Ud4lb2QuxRClYAmRleF50KrbKIoM1TddXgBrneT5/Jo=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.3/go.mod h1:q6uFgbgZfEmQrfJfrCo90QcQOcXFMfbI/fO0NqRtvZo=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-libp2p v0.36.3 h1:NHz30+G7D8Y8YmznrVZZla0ofVANrvBl2c+oARfMeDQ=
|
||||
github.com/libp2p/go-libp2p v0.36.3/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8=
|
||||
github.com/libp2p/go-libp2p v0.33.0 h1:yTPSr8sJRbfeEYXyeN8VPVSlTlFjtMUwGDRniwaf/xQ=
|
||||
github.com/libp2p/go-libp2p v0.33.0/go.mod h1:RIJFRQVUBKy82dnW7J5f1homqqv6NcsDJAl3e7CRGfE=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
@@ -52,8 +52,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
|
||||
github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
|
||||
github.com/multiformats/go-multiaddr v0.12.2 h1:9G9sTY/wCYajKa9lyfWPmpZAwe6oV+Wb1zcmMS1HG24=
|
||||
github.com/multiformats/go-multiaddr v0.12.2/go.mod h1:GKyaTYjZRdcUhyOetrxTk9z0cW+jA/YrnqTOvKgi44M=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||
@@ -81,25 +81,24 @@ github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2
|
||||
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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
|
||||
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
|
||||
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ=
|
||||
google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
187
pkg/args/args.go
Normal file
187
pkg/args/args.go
Normal file
@@ -0,0 +1,187 @@
|
||||
// Package args provides the type that represents the Arguments passed to
|
||||
// a command within an invocation.Token as well as a convenient Add method
|
||||
// to incrementally build the underlying map.
|
||||
package args
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("key not found in meta")
|
||||
|
||||
// Args are the Command's arguments when an invocation Token is processed by the executor.
|
||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||
// and transformations, while hiding the IPLD complexity from the caller.
|
||||
type Args struct {
|
||||
// This type must be compatible with the IPLD type represented by the IPLD
|
||||
// schema { String : Any }.
|
||||
|
||||
Keys []string
|
||||
Values map[string]ipld.Node
|
||||
}
|
||||
|
||||
// New returns a pointer to an initialized Args value.
|
||||
func New() *Args {
|
||||
return &Args{
|
||||
Values: map[string]ipld.Node{},
|
||||
}
|
||||
}
|
||||
|
||||
// GetNode retrieves a value as a raw IPLD node.
|
||||
// Returns ErrNotFound if the given key is missing.
|
||||
func (a *Args) GetNode(key string) (ipld.Node, error) {
|
||||
v, ok := a.Values[key]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Add inserts a key/value pair in the Args set.
|
||||
//
|
||||
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||
func (a *Args) Add(key string, val any) error {
|
||||
if _, ok := a.Values[key]; ok {
|
||||
return fmt.Errorf("duplicate key %q", key)
|
||||
}
|
||||
|
||||
node, err := literal.Any(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
||||
return fmt.Errorf("value for key %q: %w", key, err)
|
||||
}
|
||||
|
||||
a.Values[key] = node
|
||||
a.Keys = append(a.Keys, key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type Iterator interface {
|
||||
Iter() iter.Seq2[string, ipld.Node]
|
||||
}
|
||||
|
||||
// Include merges the provided arguments into the existing arguments.
|
||||
//
|
||||
// If duplicate keys are encountered, the new value is silently dropped
|
||||
// without causing an error.
|
||||
func (a *Args) Include(other Iterator) {
|
||||
for key, value := range other.Iter() {
|
||||
if _, ok := a.Values[key]; ok {
|
||||
// don't overwrite
|
||||
continue
|
||||
}
|
||||
a.Values[key] = value
|
||||
a.Keys = append(a.Keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Len return the number of arguments.
|
||||
func (a *Args) Len() int {
|
||||
return len(a.Keys)
|
||||
}
|
||||
|
||||
// Iter iterates over the args key/values
|
||||
func (a *Args) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return func(yield func(string, ipld.Node) bool) {
|
||||
for _, key := range a.Keys {
|
||||
if !yield(key, a.Values[key]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ToIPLD wraps an instance of an Args with an ipld.Node.
|
||||
func (a *Args) ToIPLD() (ipld.Node, error) {
|
||||
sort.Strings(a.Keys)
|
||||
|
||||
return qp.BuildMap(basicnode.Prototype.Any, int64(len(a.Keys)), func(ma datamodel.MapAssembler) {
|
||||
for _, key := range a.Keys {
|
||||
qp.MapEntry(ma, key, qp.Node(a.Values[key]))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Equals tells if two Args hold the same values.
|
||||
func (a *Args) Equals(other *Args) bool {
|
||||
if len(a.Keys) != len(other.Keys) {
|
||||
return false
|
||||
}
|
||||
if len(a.Values) != len(other.Values) {
|
||||
return false
|
||||
}
|
||||
for _, key := range a.Keys {
|
||||
if !ipld.DeepEqual(a.Values[key], other.Values[key]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Args) String() string {
|
||||
sort.Strings(a.Keys)
|
||||
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("{")
|
||||
|
||||
for _, key := range a.Keys {
|
||||
buf.WriteString("\n\t")
|
||||
buf.WriteString(key)
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(strings.ReplaceAll(printer.Sprint(a.Values[key]), "\n", "\n\t"))
|
||||
buf.WriteString(",")
|
||||
}
|
||||
|
||||
if len(a.Keys) > 0 {
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
buf.WriteString("}")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ReadOnly returns a read-only version of Args.
|
||||
func (a *Args) ReadOnly() ReadOnly {
|
||||
return ReadOnly{args: a}
|
||||
}
|
||||
|
||||
// Clone makes a deep copy.
|
||||
func (a *Args) Clone() *Args {
|
||||
res := &Args{
|
||||
Keys: make([]string, len(a.Keys)),
|
||||
Values: make(map[string]ipld.Node, len(a.Values)),
|
||||
}
|
||||
copy(res.Keys, a.Keys)
|
||||
for k, v := range a.Values {
|
||||
res.Values[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Validate checks that all values in the Args are valid according to UCAN specs
|
||||
func (a *Args) Validate() error {
|
||||
for key, value := range a.Values {
|
||||
if err := limits.ValidateIntegerBoundsIPLD(value); err != nil {
|
||||
return fmt.Errorf("value for key %q: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
297
pkg/args/args_test.go
Normal file
297
pkg/args/args_test.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package args_test
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
func TestArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
intKey = "intKey"
|
||||
mapKey = "mapKey"
|
||||
nilKey = "nilKey"
|
||||
boolKey = "boolKey"
|
||||
linkKey = "linkKey"
|
||||
listKey = "listKey"
|
||||
nodeKey = "nodeKey"
|
||||
uintKey = "uintKey"
|
||||
bytesKey = "bytesKey"
|
||||
floatKey = "floatKey"
|
||||
stringKey = "stringKey"
|
||||
)
|
||||
|
||||
const (
|
||||
expIntVal = int64(-42)
|
||||
expBoolVal = true
|
||||
expUintVal = uint(42)
|
||||
expStringVal = "stringVal"
|
||||
)
|
||||
|
||||
var (
|
||||
expMapVal = map[string]string{"keyOne": "valOne", "keyTwo": "valTwo"}
|
||||
// expNilVal = (map[string]string)(nil)
|
||||
expLinkVal = cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")
|
||||
expListVal = []string{"elem1", "elem2", "elem3"}
|
||||
expNodeVal = literal.String("nodeVal")
|
||||
expBytesVal = []byte{0xde, 0xad, 0xbe, 0xef}
|
||||
expFloatVal = 42.0
|
||||
)
|
||||
|
||||
argsIn := args.New()
|
||||
|
||||
for _, a := range []struct {
|
||||
key string
|
||||
val any
|
||||
}{
|
||||
{key: intKey, val: expIntVal},
|
||||
{key: mapKey, val: expMapVal},
|
||||
// {key: nilKey, val: expNilVal},
|
||||
{key: boolKey, val: expBoolVal},
|
||||
{key: linkKey, val: expLinkVal},
|
||||
{key: listKey, val: expListVal},
|
||||
{key: uintKey, val: expUintVal},
|
||||
{key: nodeKey, val: expNodeVal},
|
||||
{key: bytesKey, val: expBytesVal},
|
||||
{key: floatKey, val: expFloatVal},
|
||||
{key: stringKey, val: expStringVal},
|
||||
} {
|
||||
require.NoError(t, argsIn.Add(a.key, a.val))
|
||||
}
|
||||
|
||||
// Round-trip to DAG-CBOR
|
||||
argsOut := roundTripThroughDAGCBOR(t, argsIn)
|
||||
assert.ElementsMatch(t, argsIn.Keys, argsOut.Keys)
|
||||
assert.Equal(t, argsIn.Values, argsOut.Values)
|
||||
|
||||
actMapVal := map[string]string{}
|
||||
mit := argsOut.Values[mapKey].MapIterator()
|
||||
|
||||
for !mit.Done() {
|
||||
k, v, err := mit.Next()
|
||||
require.NoError(t, err)
|
||||
ks := must(k.AsString())
|
||||
vs := must(v.AsString())
|
||||
actMapVal[ks] = vs
|
||||
}
|
||||
|
||||
actListVal := []string{}
|
||||
lit := argsOut.Values[listKey].ListIterator()
|
||||
|
||||
for !lit.Done() {
|
||||
_, v, err := lit.Next()
|
||||
require.NoError(t, err)
|
||||
vs := must(v.AsString())
|
||||
|
||||
actListVal = append(actListVal, vs)
|
||||
}
|
||||
|
||||
assert.Equal(t, expIntVal, must(argsOut.Values[intKey].AsInt()))
|
||||
assert.Equal(t, expMapVal, actMapVal) // TODO: special accessor
|
||||
// TODO: the nil map comes back empty (but the right type)
|
||||
// assert.Equal(t, expNilVal, actNilVal)
|
||||
assert.Equal(t, expBoolVal, must(argsOut.Values[boolKey].AsBool()))
|
||||
assert.Equal(t, expLinkVal.String(), must(argsOut.Values[linkKey].AsLink()).(datamodel.Link).String()) // TODO: special accessor
|
||||
assert.Equal(t, expListVal, actListVal) // TODO: special accessor
|
||||
assert.Equal(t, expNodeVal, argsOut.Values[nodeKey])
|
||||
assert.Equal(t, expUintVal, uint(must(argsOut.Values[uintKey].AsInt())))
|
||||
assert.Equal(t, expBytesVal, must(argsOut.Values[bytesKey].AsBytes()))
|
||||
assert.Equal(t, expFloatVal, must(argsOut.Values[floatKey].AsFloat()))
|
||||
assert.Equal(t, expStringVal, must(argsOut.Values[stringKey].AsString()))
|
||||
}
|
||||
|
||||
func TestArgs_Include(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
argsIn := args.New()
|
||||
require.NoError(t, argsIn.Add("key1", "val1"))
|
||||
require.NoError(t, argsIn.Add("key2", "val2"))
|
||||
|
||||
argsOther := args.New()
|
||||
require.NoError(t, argsOther.Add("key2", "valOther")) // This should not overwrite key2 above
|
||||
require.NoError(t, argsOther.Add("key3", "val3"))
|
||||
require.NoError(t, argsOther.Add("key4", "val4"))
|
||||
|
||||
argsIn.Include(argsOther)
|
||||
|
||||
assert.Len(t, argsIn.Values, 4)
|
||||
assert.Equal(t, "val1", must(argsIn.Values["key1"].AsString()))
|
||||
assert.Equal(t, "val2", must(argsIn.Values["key2"].AsString()))
|
||||
assert.Equal(t, "val3", must(argsIn.Values["key3"].AsString()))
|
||||
assert.Equal(t, "val4", must(argsIn.Values["key4"].AsString()))
|
||||
}
|
||||
|
||||
func TestIterCloneEquals(t *testing.T) {
|
||||
a := args.New()
|
||||
|
||||
require.NoError(t, a.Add("foo", "bar"))
|
||||
require.NoError(t, a.Add("baz", 1234))
|
||||
|
||||
expected := map[string]ipld.Node{
|
||||
"foo": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
}
|
||||
|
||||
// args -> iter
|
||||
require.Equal(t, expected, maps.Collect(a.Iter()))
|
||||
|
||||
// readonly -> iter
|
||||
ro := a.ReadOnly()
|
||||
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||
|
||||
// args -> clone -> iter
|
||||
clone := a.Clone()
|
||||
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
||||
|
||||
// readonly -> WriteableClone -> iter
|
||||
wclone := ro.WriteableClone()
|
||||
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
||||
|
||||
require.True(t, a.Equals(wclone))
|
||||
require.True(t, ro.Equals(wclone.ReadOnly()))
|
||||
}
|
||||
|
||||
func TestInclude(t *testing.T) {
|
||||
a1 := args.New()
|
||||
|
||||
require.NoError(t, a1.Add("samekey", "bar"))
|
||||
require.NoError(t, a1.Add("baz", 1234))
|
||||
|
||||
a2 := args.New()
|
||||
|
||||
require.NoError(t, a2.Add("samekey", "othervalue")) // check no overwrite
|
||||
require.NoError(t, a2.Add("otherkey", 1234))
|
||||
|
||||
a1.Include(a2)
|
||||
|
||||
require.Equal(t, map[string]ipld.Node{
|
||||
"samekey": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
"otherkey": basicnode.NewInt(1234),
|
||||
}, maps.Collect(a1.Iter()))
|
||||
}
|
||||
|
||||
func TestArgsIntegerBounds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
val int64
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
key: "valid",
|
||||
val: 42,
|
||||
},
|
||||
{
|
||||
name: "max safe integer",
|
||||
key: "max",
|
||||
val: limits.MaxInt53,
|
||||
},
|
||||
{
|
||||
name: "min safe integer",
|
||||
key: "min",
|
||||
val: limits.MinInt53,
|
||||
},
|
||||
{
|
||||
name: "exceeds max safe integer",
|
||||
key: "tooBig",
|
||||
val: limits.MaxInt53 + 1,
|
||||
wantErr: "exceeds safe integer bounds",
|
||||
},
|
||||
{
|
||||
name: "below min safe integer",
|
||||
key: "tooSmall",
|
||||
val: limits.MinInt53 - 1,
|
||||
wantErr: "exceeds safe integer bounds",
|
||||
},
|
||||
{
|
||||
name: "duplicate key",
|
||||
key: "duplicate",
|
||||
val: 42,
|
||||
wantErr: "duplicate key",
|
||||
},
|
||||
}
|
||||
|
||||
a := args.New()
|
||||
require.NoError(t, a.Add("duplicate", 1)) // tests duplicate key
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := a.Add(tt.key, tt.val)
|
||||
if tt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
val, err := a.GetNode(tt.key)
|
||||
require.NoError(t, err)
|
||||
i, err := val.AsInt()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.val, i)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
argsSchema = "type Args { String : Any }"
|
||||
argsName = "Args"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
errSchema error
|
||||
)
|
||||
|
||||
func argsType() schema.Type {
|
||||
once.Do(func() {
|
||||
ts, errSchema = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||
})
|
||||
if errSchema != nil {
|
||||
panic(errSchema)
|
||||
}
|
||||
|
||||
return ts.TypeByName(argsName)
|
||||
}
|
||||
|
||||
func roundTripThroughDAGCBOR(t *testing.T, argsIn *args.Args) *args.Args {
|
||||
t.Helper()
|
||||
|
||||
node, err := argsIn.ToIPLD()
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := ipld.Encode(node, dagcbor.Encode)
|
||||
require.NoError(t, err)
|
||||
|
||||
var argsOut args.Args
|
||||
_, err = ipld.Unmarshal(data, dagcbor.Decode, &argsOut, argsType())
|
||||
require.NoError(t, err)
|
||||
|
||||
return &argsOut
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
71
pkg/args/builder.go
Normal file
71
pkg/args/builder.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
// Builder allows the fluid construction of an Args.
|
||||
type Builder struct {
|
||||
args *Args
|
||||
errs error
|
||||
}
|
||||
|
||||
// NewBuilder returns a Builder which will assemble the Args.
|
||||
func NewBuilder() *Builder {
|
||||
return &Builder{
|
||||
args: New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Add inserts a new key/val into the Args being assembled while collecting
|
||||
// any errors caused by duplicate keys.
|
||||
func (b *Builder) Add(key string, val any) *Builder {
|
||||
b.errs = errors.Join(b.errs, b.args.Add(key, val))
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// Build returns the assembled Args or an error containing a list of
|
||||
// errors encountered while trying to build the Args.
|
||||
func (b *Builder) Build() (*Args, error) {
|
||||
if b.errs != nil {
|
||||
return nil, b.errs
|
||||
}
|
||||
|
||||
return b.args, nil
|
||||
}
|
||||
|
||||
// BuildIPLD is the same as Build except it takes the additional step of
|
||||
// converting the Args to an ipld.Node.
|
||||
func (b *Builder) BuildIPLD() (ipld.Node, error) {
|
||||
args, err := b.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return args.ToIPLD()
|
||||
}
|
||||
|
||||
// MustBuild is the same as Build except it panics if an error occurs.
|
||||
func (b *Builder) MustBuild() *Args {
|
||||
args, err := b.Build()
|
||||
|
||||
if err != nil {
|
||||
panic(b.errs)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// MustBuildIPLD is the same as BuildIPLD except it panics if an error
|
||||
// occurs.
|
||||
func (b *Builder) MustBuildIPLD() ipld.Node {
|
||||
node, err := b.BuildIPLD()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
81
pkg/args/builder_test.go
Normal file
81
pkg/args/builder_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package args_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
)
|
||||
|
||||
func TestBuilder_XXX(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
keyOne = "key1"
|
||||
valOne = "string"
|
||||
keyTwo = "key2"
|
||||
valTwo = 42
|
||||
)
|
||||
|
||||
exp := args.New()
|
||||
exp.Add(keyOne, valOne)
|
||||
exp.Add(keyTwo, valTwo)
|
||||
|
||||
expNode, err := exp.ToIPLD()
|
||||
require.NoError(t, err)
|
||||
|
||||
disjointKeys := args.NewBuilder().
|
||||
Add(keyOne, valOne).
|
||||
Add(keyTwo, valTwo)
|
||||
|
||||
duplicateKeys := args.NewBuilder().
|
||||
Add(keyOne, valOne).
|
||||
Add(keyTwo, valTwo).
|
||||
Add(keyOne, "oh no!")
|
||||
|
||||
t.Run("MustBuild succeeds with disjoint keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act *args.Args
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
act = disjointKeys.MustBuild()
|
||||
})
|
||||
assert.Equal(t, exp, act)
|
||||
})
|
||||
|
||||
t.Run("MustBuild fails with duplicate keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act *args.Args
|
||||
|
||||
require.Panics(t, func() {
|
||||
act = duplicateKeys.MustBuild()
|
||||
})
|
||||
assert.Nil(t, act)
|
||||
})
|
||||
|
||||
t.Run("MustBuildIPLD succeeds with disjoint keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act ipld.Node
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
act = disjointKeys.MustBuildIPLD()
|
||||
})
|
||||
assert.Equal(t, expNode, act)
|
||||
})
|
||||
|
||||
t.Run("MustBuildIPLD fails with duplicate keys", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var act ipld.Node
|
||||
|
||||
require.Panics(t, func() {
|
||||
act = duplicateKeys.MustBuildIPLD()
|
||||
})
|
||||
assert.Nil(t, act)
|
||||
})
|
||||
}
|
||||
39
pkg/args/readonly.go
Normal file
39
pkg/args/readonly.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
type ReadOnly struct {
|
||||
args *Args
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||
return r.args.GetNode(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) Len() int {
|
||||
return r.args.Len()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return r.args.Iter()
|
||||
}
|
||||
|
||||
func (r ReadOnly) ToIPLD() (ipld.Node, error) {
|
||||
return r.args.ToIPLD()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||
return r.args.Equals(other.args)
|
||||
}
|
||||
|
||||
func (r ReadOnly) String() string {
|
||||
return r.args.String()
|
||||
}
|
||||
|
||||
func (r ReadOnly) WriteableClone() *Args {
|
||||
return r.args.Clone()
|
||||
}
|
||||
@@ -98,7 +98,34 @@ func (c Command) Join(segments ...string) Command {
|
||||
// Segments returns the ordered segments that comprise the Command as a
|
||||
// slice of strings.
|
||||
func (c Command) Segments() []string {
|
||||
return strings.Split(string(c), separator)
|
||||
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
|
||||
|
||||
@@ -73,6 +73,21 @@ func TestJoin(t *testing.T) {
|
||||
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 {
|
||||
name string
|
||||
inp string
|
||||
|
||||
109
pkg/container/SPEC.md
Normal file
109
pkg/container/SPEC.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# UCAN container Specification v0.1.0
|
||||
|
||||
## Editors
|
||||
|
||||
* [Michael Muré], [Consensys]
|
||||
|
||||
## Authors
|
||||
|
||||
* [Michael Muré], [Consensys]
|
||||
* [Hugo Dias]
|
||||
|
||||
## Language
|
||||
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [BCP 14] when, and only when, they appear in all capitals, as shown here.
|
||||
|
||||
# 0 Abstract
|
||||
|
||||
[User-Controlled Authorization Network (UCAN)][UCAN] is a trustless, secure, local-first, user-originated authorization and revocation scheme. This document describes a container format for transmitting one or more UCAN tokens as bytes, regardless of the transport.
|
||||
|
||||
# 1 Introduction
|
||||
|
||||
The UCAN spec itself is transport agnostic. This specification describes how to transfer one or more [UCAN] tokens bundled together, regardless of the transport.
|
||||
|
||||
# 2 Container format
|
||||
|
||||
## 2.1 Inner structure
|
||||
|
||||
UCAN tokens, regardless of their kind ([Delegation], [Invocation], [Revocation], [Promise]) MUST be first signed and serialized into DAG-CBOR bytes according to their respective specification. As the token's CID is not part of the serialized container, any CID returned by this operation is to be ignored.
|
||||
|
||||
All the tokens' bytes MUST be assembled in a [CBOR] array. The ordering of tokens in the array MUST NOT matter. This array SHOULD NOT have duplicate entries.
|
||||
|
||||
That array is then inserted as the value under the `ctn-v1` string key, in a CBOR map. There MUST NOT be other keys.
|
||||
|
||||
For clarity, the CBOR shape is given below:
|
||||
|
||||
```json
|
||||
{
|
||||
"ctn-v1": [
|
||||
<token1 bytes>,
|
||||
<token2 bytes>,
|
||||
<token3 bytes>,
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 2.2 Serialisation
|
||||
|
||||
To serialize the container into bytes, the inner CBOR structure MUST then be serialized into bytes according to the CBOR specification. The resulting bytes MAY be compressed by a supported algorithm, then MAY be encoded with a supported base encoding.
|
||||
|
||||
The following compression algorithms are REQUIRED to be supported:
|
||||
- [GZIP]
|
||||
|
||||
The following base encoding combinations are REQUIRED to be supported:
|
||||
- base64, standard alphabet, padding
|
||||
- base64, URL alphabet, no padding
|
||||
|
||||
The CBOR bytes MUST be prepended by a single byte header to indicate the selection combination of base encoding and compression. This header value MUST be set according to the following table:
|
||||
|
||||
| Header as hex | Header as ASCII | Base encoding | Compression |
|
||||
|---------------|-----------------|-------------------------|----------------|
|
||||
| 0x40 | @ | raw bytes | no compression |
|
||||
| 0x42 | B | base64 std padding | no compression |
|
||||
| 0x43 | C | base64 url (no padding) | no compression |
|
||||
| 0x4D | M | raw bytes | gzip |
|
||||
| 0x4F | O | base64 std padding | gzip |
|
||||
| 0x50 | P | base64 url (no padding) | gzip |
|
||||
|
||||
For clarity, the resulting serialisation is in the form of `<header byte><cbor bytes, optionally compressed, optionally encoded>`.
|
||||
|
||||
# 3 FAQ
|
||||
|
||||
## 3.1 Why not include the UCAN CIDs?
|
||||
|
||||
Several attacks are possible if UCAN tokens aren't validated. If CIDs aren't validated, at least two attacks are possible: [privilege escalation] and [cache poisoning], as UCAN delegation proofs depends on a correct hash-linked structure.
|
||||
|
||||
By not including the CID in the container, the recipient is forced to hash (and thus validate) the CIDs for each token. If presented with a claimed CID paired with the token bytes, implementers could ignore CID validation, breaking a core part of the proof chain security model. Hash functions are very fast on a couple kilobytes of data so the overhead is still very low. It also significantly reduces the size of the container.
|
||||
|
||||
## 3.2 Why compress? Why not always compress?
|
||||
|
||||
Compression is a relatively demanding operation. As such, using it is a tradeoff between size on the wire and CPU/memory usage, both when writing and reading a container. The transport itself can make compression worthwhile or not: for example, HTTP/2 and HTTP/3 headers are already compressed, but HTTP/1 headers are not. This being highly contextual, the choice is left to the final implementer.
|
||||
|
||||
# 4 Implementation recommendations
|
||||
|
||||
## 4.1 Dissociate reader and writer
|
||||
|
||||
While it is tempting to write a single implementation to read and write a container, it is RECOMMENDED to separate the implementation into a reader and a writer. The writer can simply accept arbitrary tokens as bytes, while the reader provides a read-only view with convenient access functions.
|
||||
|
||||
# 5 Acknowledgments
|
||||
|
||||
Many thanks to all the [Fission] team and in particular to [Brooklyn Zelenka] for creating and pushing [UCAN] and other critical pieces like [WNFS], and generally being awesome and supportive people.
|
||||
|
||||
<!-- External Links -->
|
||||
|
||||
[BCP 14]: https://www.rfc-editor.org/info/bcp14
|
||||
[Brooklyn Zelenka]: https://github.com/expede
|
||||
[CBOR]: https://www.rfc-editor.org/rfc/rfc8949.html
|
||||
[Consensys]: https://consensys.io/
|
||||
[Delegation]: https://github.com/ucan-wg/delegation/tree/v1_ipld
|
||||
[Fission]: https://fission.codes
|
||||
[GZIP]: https://datatracker.ietf.org/doc/html/rfc1952
|
||||
[Hugo Dias]: https://github.com/hugomrdias
|
||||
[Invocation]: https://github.com/ucan-wg/invocation
|
||||
[Michael Muré]: https://github.com/MichaelMure/
|
||||
[Promise]: https://github.com/ucan-wg/promise/tree/v1-rc1
|
||||
[Revocation]: https://github.com/ucan-wg/revocation/tree/first-draft
|
||||
[UCAN]: https://github.com/ucan-wg/spec
|
||||
[WNFS]: https://github.com/wnfs-wg
|
||||
[cache poisoning]: https://en.wikipedia.org/wiki/Cache_poisoning
|
||||
[privilede escalation]: https://en.wikipedia.org/wiki/Privilege_escalation
|
||||
@@ -40,6 +40,13 @@ func TestCarRoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
func FuzzCarRoundTrip(f *testing.F) {
|
||||
// Note: this fuzzing is somewhat broken.
|
||||
// After some time, the fuzzer discover that a varint can be serialized in different
|
||||
// ways that lead to the same integer value. This means that the CAR format can have
|
||||
// multiple legal binary representation for the exact same data, which is what we are
|
||||
// trying to detect here. Ideally, the format would be stricter, but that's how things
|
||||
// are.
|
||||
|
||||
example, err := os.ReadFile("testdata/sample-v1.car")
|
||||
require.NoError(f, err)
|
||||
|
||||
|
||||
1
pkg/container/containertest/Base64StdPadding
Normal file
1
pkg/container/containertest/Base64StdPadding
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/containertest/Base64StdPaddingGzipped
Normal file
1
pkg/container/containertest/Base64StdPaddingGzipped
Normal file
@@ -0,0 +1 @@
|
||||
OH4sIAAAAAAAA/5zX+ZOUxR3HcY7FwhKToFJGFAU1ghzL8/RztpxzrzsHszuzcy1Ru5/unmd6dubZnXsGE9EtFVnFSCiNEFfFiCSKqIVI8ECBKGiJiMSTFdCYeGDK6GpMWcSUtUOq8ts++Qfev7zq+6nu+5lRys+riKuTYwdvSCw9NrDty6c/Wf668n73nFsqQ988uh1Z8rTNu9X6hkO33S/vG1i92+//ontN6/BubU54fntiUnXqrPNfo9p5bnTWd3PP3IhMt/z52L5i2UD5+aQnvVRsFVqFeQWjVdxsoDKp6SRDrsjS+hUNNZjNtFdJsa1CzUwCxSK8imjO7curtFHwBqy8US3hlBT08XQlkWeGkSPmfGZZ8zEqGLTWOzX9yyemGZli8X+b1XAxESdxALIVX6E3xkiY1coeswLDMOp1BVRgOrGnqxEtukoJo9fqub7fQD09Rmv3z/vRYtZaQT1lOs4olvH/nyU5WkIPmEAQkAYg5lhDqigxjeimJoqEMgC4JipMViURiCYTBQHLBHGZCkyRNcgYzVt5gwauDa67+uiirx+KuQZnjgB9/2H3noUzrj79rQdv27HpuSufnOrfvu3Myf98rfv5Tfds2OX9de9w92Mfm/m9s2eOe/etzPnd/1jdeqI8vvuVbYOb8Q2Ltky0CZRDlAfcJKqHGSklXclQxR+mPKbgcM5n5awkRYmwM2SltSQqjRYo3RFD/lDOnW/TYrxd7ayVQaQme10dVl3RPCardrQHkFJUvTUT2QCyk20CaQpUFF1CnFJJpDqVoGYSDKhuMJVDlaoqpIIMTIZVhcoMcCYAnSgC1GkT6BF9x2d9X19nTBw/+cgI0KO+/W+sk4cmDz7/7AM7D7s2+BdIFw0vGLv9yzm7Vv3h1QN/WZHwxy9Qj0evv2ftkX891BLy7V2cWjY+sGV9f+zYHh4eYxOoXE8E3BFPNB+q0Kw7XexLpfXeXiPrd5WC3N0Wj7iBFfeQMqi06aMFKtcMf9wRrbN4EKOa7KxYfRkQ0/3ecJ/LV/dnkl2OgEPSvIViNmQDyE62CSRDgWEBilwECpWwADVsKkQnhgQZ13QZMVWVITGRCLCmI8ANCWuiSiSqNIF+ExuqnnSds6b7zv4XRoDWzN08c6H1GByE86wXj+561L/q9pc+G1jy4/ya71uPb3kXLJmwe/jlHFu3+NhFK2/8Sce4sZtiLQezq/uVBx6OXnv3n0+zC+T1F5MuENTKFW8u0ld3GcVQNl0gZodSiCk01UkMV9SVauRNXR0tEOtkic5YKtXlDgKt4VFKuhWoGiku417g6wuUimZ7DbTJDAGv2waQnWwTCIka0XVJ4JCKVJGQLGqmIamaYaiQKwpVIFQMgZgUCQLSicqxTKCuyxLATaD4JZf/4mhvbf19y15uGwFyrodHo9vOP7kzObhklvuTfQ/j3IQLn5nz6dKNn0xo7H/nkQs+HT70VvLwmWzvm3c/Ofn7g3f91jvkb7/5vpWHHl8EX3/B7sSxiq9R5bkIsAJKJurhPj9z1GgDh3gy18ViyOP2sa5oUso4C9aoJ84fq/fKKm3LtRe8gWTe568V8z7sMCU1FnA7gJnOhXq9Lk+XaHTamTgb2SaQQIFEEaJcgLIqQEEQgaloGsKIMs5ESRYFQVYlU2UEEVnEXCUSA0zTBLUJJM/nb351ecu/h6dLlREg67y3b3be96Ky/p7L7jzeO9s5Y9Ka5fNxZHc8cVC6+K83iXPfO/GjL5ybrbZrWq4QPu64rfu7adMP7v/uyt/f0r7yJjBpkk2gvgQPOTusdDGqhOuKNxROw0ba29lFUjCbqNV1MSDWZZxgViRkjBbI8jUa1aKl0Y42qz3sTVt11dQSeo5WqsGKD1NY0sN6QS8aUhXaALKTPQWEFU0nKuMi0aGCDayoJjIwZjrGHMoEAUI1QTINxUAShCo3DCwTWdOZ0QRaAhdf1yIf7unm5atGgObsH8rs5FO2wH1r/zb84c8WrNLL179/za+G51WvkvaKXjguP3XmXWetHTix59zd32Te9h2ftGTbhU/dv3PvjcvAlBnjzrAJVOjMUKhKznS4jZR10ZWMVz1JZ5zWHbmuSqnglzpSMqPVuiPgkkcL1Butd1DQVQmGgByp0S5Xo5SLKXlfvD1QDolQZ2oxXIwFk42AUrEBZCfbBBJlSZFFReIIQZlqhgR1U0MSlilROBAMTACBAjYZMyjUich1w1B+2Dd4auIOnLPCY3wFNzxIc+tGgKqX5an27Qtn37C2d+ykS7XPnz30TSi9onDd2eeMK7N4/6x78XLQMnnlm6/mFlbIeRee9qWybfCjBceSi6YVdg2s7ZxgE6ga7sp29QGTJ+LOWAooJcsXJ9loQgvFmZOXOgNt7nox1jBRyAVGC8SNbK0zCyKmrDsDRrrhShLqqGQ1DKJyJVMvknY/jNV52hQbThtAdrJNIIgVSdBljUuCwCSgMxGbBhENJBLIBWYgAzCsAZNAwdAFXeZMVVX4wywKTaBZTxz9E9y6b0JPgu8YAZo9A258I6l75KfJM2Omb+T3zjDPTW393SUXnHjswB3DY05a7wQ7ux9fXjtjx4QxS6fPfmns3598sWf9lkvB83sOXrxytt1XXK5IHCHk1Tx6sc0fTPdlCjTBYmWHHNH8XR2s3khVjKqeFD085R71BeXrdVytWKqSqCaDMiehTm/Q4jCVrUcdrBKTOfR4vEGto01N2bkgG9lTF6RBwFQicAqJALGiaropCwggCWMuAyJThAxETUIA0CVV57qkUEVlOtSbQGMGJuaOTFzyx/FHjj81AvTqwIExd3vB9FuXrdHmrp7106G33/vgo9tP3zjF8t5ZOLz52q2bjt1x6+yTH6zQbmGpiasOJ5Xw+oU7n3v8YMuH77Vs/bbF7kcVmjBmuhuhVMyVUNWiSXg9XMs2yvUkpCnma8BKxlK0eqXT8o0WqBgpmI5YKuLvyLiUeqTsp42S6qiasUCjlFG9Xk1pr3ujpN0BLWoDyE62CSRpMtBFTeaipKqYyZJKTEwURGWkcVHAWDcY0TSTYipjjFUu6YL6w2P8v0AnP45M2du/fWgHeOWD/wQAAP//IuQ6Y1MQAAA=
|
||||
1
pkg/container/containertest/Base64URL
Normal file
1
pkg/container/containertest/Base64URL
Normal file
File diff suppressed because one or more lines are too long
1
pkg/container/containertest/Base64URLGzipped
Normal file
1
pkg/container/containertest/Base64URLGzipped
Normal file
@@ -0,0 +1 @@
|
||||
PH4sIAAAAAAAA_5zWa5cUxR3HcVcRiejmuCII3siKwgnubnd1dVW1eIS5rzM9w8zs3HZETFd19_T07PTcZ2cH1KMgoB41YgIo3nM0G1GCRhCRiILIEaIhIEo0Bi9c9ewaFfEazYMd8iCPdvIGvk8-p_71e1RnZaujyt_e2_LQLYk5Fbb1QefFR9-_5LzWO59-9PfWoWV_nN_f177z06eH31j13Je8-Wpk35bTccKjtV_22MRfnz9DufuSg8Ntn02KzZtw83sLxj-mGE441FIoVZhidal9qTl8J9fJdRRZJz_IlIpaI2pavTKjDVxZR_5MQXa4aFIFgr-gMCrrOTWUqWNajNZERzin0GLRz7IxsTcfAgnGsqrRpedyXVQpMq2Wn5K68dmLWLpU-p9msRbQ-usuHEsGorZK0BbwxWzxvCdrhDK2vL23UMCRTM0RlStVxPK5vpsXMaWvj3Vee90i5Wq9s6r0VbRTWalC__-smtXKyu8MERNOwJSZkCACeAFgzkC6xGkYAhOLqkJEiBXJkJAuCUBUTMIA4KEgAEWzchbT5DXHfxwc7LiqNnPmRwdHgBa8NO6jDQ9uvW6wa7DHYetfP3DxO4s7JhybLa1YcerUV-Yae7aDww_P2_zjvH9euWqOOW3yjBWtO2__bKzLvfnR_enEpmaBMtFuy4qEsgrlqRMpPm86mql6apjI4V63V_bFgl5WT-USpQhDowVK2amiFmM8tEUsiRJcCeowHM9AvVQreLGE83LMkcBBI99dAE0ANZNtAGFewJjwkilBJPG8TphoKEQhmihKpsgBXuIVhXEGRRQpRGAmhzkRqIpOaQNo4afuoTWTj60cfjw5fQQoxy98ZPGmF3YsWLJo8voJ587Y9-X38geyLBq_nHXZIy9fPun5m1pebqVzv7J-NuaM08XEzYl3v3kmtfuCr2Yc-VAa3vtxS7NAnpKSKsBEzVaMphWdFB0kgNSEgHpwiPe7a0qaxYtVG_SVq_7RAhkey-asYmfUZ1XKMbGSr-Vo1UZ60rZyNV6Uy8l8MY_iES1ic0abAGom2wCCEqcLVFRNpEKBaghAaCBJUYgKOVNVGMQCUyXRQBqFTJWwKQkIIoZFQW8AvZV64bRDez_6flYbbmu8oDa_euu2yJB9w1vX3uO-YrZvobj2yK07V7-1YebjrRcgbsvS4fiyJ9_gpx4KvT5n_A7PTz-cqF_asuuaoTFnn2N-bbU2CaSHi0bJlrEi3kyh6NFrgqOu59VKOJ2t9Xj4sllT4wXqJdFQfzI2aiBvIGSG_Zls2IsDSdKblRV3QlQz2X7Z2evudVBk16QIM1yiCZsBaiLbAOIEnROQppu8AjgKiKqIhgQVwDQCTCKJkqCIBIqGpoicCilvcoxDWJewcPLE3TD0fHzb0y_BrtRd2gjQN3fd--36ju_CYzuV8adcsOeA95UvVv-09N1t05_bN7DllouWTN-18r4nTm-9Y8uZ2L02fj1XLizHlzyfrZ-x65Pt503b0uyJs0oDhpWuSh4vwFpJjlcDciKRCbtQTzmvxkWrKIWCUQm51FomO1ogKyMLyXSPx0xHxKrixwFT7a9XgzUUTuedXgDi8XKmgGJmJOwMNQHUTLYBhERGkE6YCTXEYYAYxxuE5xGnCbopAkWjAtCgbhCMKAUQmQqkvA4wJaQBpNyhPfDyK9Ijs-yOv40ArTv_hoMtSbB12NjtzD1Ef35ow47k_D3frn41f-2LX6YmdTxzzba3t180_7XFV5m-fz304tj2Td-t7Nx_oblszv7ctI2Hmz1xhayl1myOkhEOxUt2S_S4shFvMI7dIbseyPa4AlalJ8q6g2KhGBktUJGVfQEaSsXtsK7EnXnC6zIP84ZHrYUyGR9vcxXj9lwuaIvG7E0ANZM9-QcBHkNCNFMFEKhYFTRiSBoUOV0RTE3DKmWMCJKhijzgeJWakgoIwyLUuAZQSlq-Y177mBOSHt49AjRF-PBAe9d1H3ffcvyZczrHLAhP9X3jC5y797YFa-mNxYl_evDqo4fu_8NZh3d88un1R1f_5cfBf8_76p3tv_psyooll_k-nj2mWaC6b8BCmVrCytYjIFZKF1wytmUFKdijVuoCLIdrBV8dk3AxbIwWqJRAkpyRMnkY9ReLcipX8PR3u6RcrheykFINGCCc7-lJ25LMpjUB1Ez25IljDHFYpCYUiUYR4yVkUMKYIEiSiTDBSFcRgAajPAOU6ibjmShBToOwAXRf-6V31zceuXHdjHV9I0D7Jn7wgXzr4TVX_-PYw_mpLatO69_qW7R1Q-m8o9OcsGP2rM_f3lM_3n0_-aL18zWHV-eHzkik9r4_LriMt16a98Tm889sdiTASDqRwFLW3evj6_mU6i96cqInGEY4Ea0F1brlsPUXJSlCrcpogfRCzGll1YKRsqXrWEuVajq21wK5buKv5FxOV7wQy4Wz5Wwg3NRIaCbbABKopoi6BkymSSrTRCpSQ8UqEEQBmkTTBQp0VcSGBgAvYaiZCgQMUiz89wXdVtm7cNtjqvLmb5ZtHAHq2vfX9Uem2q-ZXTzw3oub0ruemj52f29hwuWeZ2_6_qylidcO5d9p3310-_wptTV3Dv1WXZzbuLsNDU2-dzDy7Sf84MxxTQKZAxWi5wO0Agci0WTOnStHklUpbPaHSi7JcsV9UbvH6_IHzHTVNeqRwLtlj6rV_T3ZehxGex2KLA3EXX4XhNmaGZdj-VjCRgy37HA4mhkJTWQbQBImhOkcbyIeQA1oiOcNBVBEdBWYlOc0IgBAREPnBYoABSavUAI0RhFuAM22T_nzs8NTTyz94YFVI0Bze_9-4utz3dvfW5B7fXL7uJWvr-1rO-uG5evusY_jlk-ynvzheOfE5IFIt4ieunAfO3bK-CWuX2w-0HbQP_fDi-8Y2NPsitNYLCfr2YLGEhrwg7LNoL3ZWKLSAzwDxNvNV_OA1L0oFVdToz5xuURswBupyoo7Ggua8Vg8kIjV5TLvcyCUDJsV7PXKuglTehn5mwBqJnvyBSEd6qpKTV5HCEOeU4mBCJSwpImmIkBd4BRGoUEUHUlMQ6amQVVVJB2CBpBwxRsH-9-cvnYnOfvd_wQAAP__hMJ751MQAAA
|
||||
BIN
pkg/container/containertest/Bytes
Normal file
BIN
pkg/container/containertest/Bytes
Normal file
Binary file not shown.
BIN
pkg/container/containertest/BytesGzipped
Normal file
BIN
pkg/container/containertest/BytesGzipped
Normal file
Binary file not shown.
21
pkg/container/containertest/containertest.go
Normal file
21
pkg/container/containertest/containertest.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package containertest
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed Base64StdPadding
|
||||
var Base64StdPadding string
|
||||
|
||||
//go:embed Base64StdPaddingGzipped
|
||||
var Base64StdPaddingGzipped string
|
||||
|
||||
//go:embed Base64URL
|
||||
var Base64URL string
|
||||
|
||||
//go:embed Base64URLGzipped
|
||||
var Base64URLGzipped string
|
||||
|
||||
//go:embed Bytes
|
||||
var Bytes []byte
|
||||
|
||||
//go:embed BytesGzipped
|
||||
var BytesGzipped []byte
|
||||
118
pkg/container/packaging.go
Normal file
118
pkg/container/packaging.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const containerVersionTag = "ctn-v1"
|
||||
|
||||
type header byte
|
||||
|
||||
const (
|
||||
headerRawBytes = header(0x40)
|
||||
headerBase64StdPadding = header(0x42)
|
||||
headerBase64URL = header(0x43)
|
||||
headerRawBytesGzip = header(0x4D)
|
||||
headerBase64StdPaddingGzip = header(0x4F)
|
||||
headerBase64URLGzip = header(0x50)
|
||||
)
|
||||
|
||||
func (h header) encoder(w io.Writer) *payloadWriter {
|
||||
res := &payloadWriter{rawWriter: w, writer: w, header: h}
|
||||
|
||||
switch h {
|
||||
case headerBase64StdPadding, headerBase64StdPaddingGzip:
|
||||
b64Writer := base64.NewEncoder(base64.StdEncoding, res.writer)
|
||||
res.writer = b64Writer
|
||||
res.closers = append([]io.Closer{b64Writer}, res.closers...)
|
||||
case headerBase64URL, headerBase64URLGzip:
|
||||
b64Writer := base64.NewEncoder(base64.RawURLEncoding, res.writer)
|
||||
res.writer = b64Writer
|
||||
res.closers = append([]io.Closer{b64Writer}, res.closers...)
|
||||
}
|
||||
|
||||
switch h {
|
||||
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
|
||||
gzipWriter := gzip.NewWriter(res.writer)
|
||||
res.writer = gzipWriter
|
||||
res.closers = append([]io.Closer{gzipWriter}, res.closers...)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func payloadDecoder(r io.Reader) (io.Reader, error) {
|
||||
headerBuf := make([]byte, 1)
|
||||
_, err := r.Read(headerBuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := header(headerBuf[0])
|
||||
|
||||
switch h {
|
||||
case headerRawBytes,
|
||||
headerBase64StdPadding,
|
||||
headerBase64URL,
|
||||
headerRawBytesGzip,
|
||||
headerBase64StdPaddingGzip,
|
||||
headerBase64URLGzip:
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown container header")
|
||||
}
|
||||
|
||||
switch h {
|
||||
case headerBase64StdPadding, headerBase64StdPaddingGzip:
|
||||
r = base64.NewDecoder(base64.StdEncoding, r)
|
||||
case headerBase64URL, headerBase64URLGzip:
|
||||
r = base64.NewDecoder(base64.RawURLEncoding, r)
|
||||
}
|
||||
|
||||
switch h {
|
||||
case headerRawBytesGzip, headerBase64StdPaddingGzip, headerBase64URLGzip:
|
||||
gzipReader, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = gzipReader
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
var _ io.WriteCloser = &payloadWriter{}
|
||||
|
||||
// payloadWriter is tasked with two things:
|
||||
// - prepend the header byte
|
||||
// - call Close() on all the underlying io.Writer
|
||||
type payloadWriter struct {
|
||||
rawWriter io.Writer
|
||||
writer io.Writer
|
||||
header header
|
||||
headerWrote bool
|
||||
closers []io.Closer
|
||||
}
|
||||
|
||||
func (w *payloadWriter) Write(p []byte) (n int, err error) {
|
||||
if !w.headerWrote {
|
||||
_, err := w.rawWriter.Write([]byte{byte(w.header)})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.headerWrote = true
|
||||
}
|
||||
return w.writer.Write(p)
|
||||
}
|
||||
|
||||
func (w *payloadWriter) Close() error {
|
||||
var errs error
|
||||
for _, closer := range w.closers {
|
||||
if err := closer.Close(); err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"strings"
|
||||
|
||||
"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/cbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token"
|
||||
@@ -17,96 +18,67 @@ import (
|
||||
)
|
||||
|
||||
var ErrNotFound = fmt.Errorf("not found")
|
||||
var ErrMultipleInvocations = fmt.Errorf("multiple invocations")
|
||||
|
||||
// Reader is a token container reader. It exposes the tokens conveniently decoded.
|
||||
type Reader map[cid.Cid]token.Token
|
||||
type Reader map[cid.Cid]bundle
|
||||
|
||||
// GetToken returns an arbitrary decoded token, from its CID.
|
||||
// If not found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
|
||||
tkn, ok := ctn[cid]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return tkn, nil
|
||||
type bundle struct {
|
||||
sealed []byte
|
||||
token token.Token
|
||||
}
|
||||
|
||||
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
||||
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||
tkn, err := ctn.GetToken(cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
||||
return tkn, nil
|
||||
}
|
||||
return nil, fmt.Errorf("not a delegation token")
|
||||
// FromBytes decodes a container from a []byte
|
||||
func FromBytes(data []byte) (Reader, error) {
|
||||
return FromReader(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// FromString decodes a container from a string
|
||||
func FromString(s string) (Reader, error) {
|
||||
return FromReader(strings.NewReader(s))
|
||||
}
|
||||
|
||||
// GetInvocation returns the first found invocation.Token.
|
||||
// If none are found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||
for _, t := range ctn {
|
||||
if inv, ok := t.(*invocation.Token); ok {
|
||||
return inv, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func FromCar(r io.Reader) (Reader, error) {
|
||||
_, it, err := readCar(r)
|
||||
// FromReader decodes a container from an io.Reader.
|
||||
func FromReader(r io.Reader) (Reader, error) {
|
||||
payload, err := payloadDecoder(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctn := make(Reader)
|
||||
|
||||
for block, err := range it {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = ctn.addToken(block.data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCarBase64(r io.Reader) (Reader, error) {
|
||||
return FromCar(base64.NewDecoder(base64.StdEncoding, r))
|
||||
}
|
||||
|
||||
func FromCbor(r io.Reader) (Reader, error) {
|
||||
n, err := ipld.DecodeStreaming(r, dagcbor.Decode)
|
||||
n, err := ipld.DecodeStreaming(payload, cbor.Decode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n.Kind() != datamodel.Kind_List {
|
||||
return nil, fmt.Errorf("not a list")
|
||||
if n.Kind() != datamodel.Kind_Map {
|
||||
return nil, fmt.Errorf("invalid container format: expected map")
|
||||
}
|
||||
if n.Length() != 1 {
|
||||
return nil, fmt.Errorf("invalid container format: expected single version key")
|
||||
}
|
||||
|
||||
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()
|
||||
for !it.Done() {
|
||||
_, val, err := it.Next()
|
||||
version, err := key.AsString()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid container format: version must be string")
|
||||
}
|
||||
if version != containerVersionTag {
|
||||
return nil, fmt.Errorf("unsupported container version: %s", version)
|
||||
}
|
||||
|
||||
if tokensNode.Kind() != datamodel.Kind_List {
|
||||
return nil, fmt.Errorf("invalid container format: tokens must be a list")
|
||||
}
|
||||
|
||||
ctn := make(Reader, tokensNode.Length())
|
||||
it2 := tokensNode.ListIterator()
|
||||
for !it2.Done() {
|
||||
_, val, err := it2.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -122,8 +94,105 @@ func FromCbor(r io.Reader) (Reader, error) {
|
||||
return ctn, nil
|
||||
}
|
||||
|
||||
func FromCborBase64(r io.Reader) (Reader, error) {
|
||||
return FromCbor(base64.NewDecoder(base64.StdEncoding, r))
|
||||
// GetToken returns an arbitrary decoded token, from its CID.
|
||||
// If not found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
|
||||
bndl, ok := ctn[cid]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return bndl.token, nil
|
||||
}
|
||||
|
||||
// GetSealed returns an arbitrary sealed token, from its CID.
|
||||
// If not found, ErrNotFound is returned.
|
||||
func (ctn Reader) GetSealed(cid cid.Cid) ([]byte, error) {
|
||||
bndl, ok := ctn[cid]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return bndl.sealed, nil
|
||||
}
|
||||
|
||||
// GetAllTokens return all the tokens in the container.
|
||||
func (ctn Reader) GetAllTokens() iter.Seq[token.Bundle] {
|
||||
return func(yield func(token.Bundle) bool) {
|
||||
for c, bndl := range ctn {
|
||||
if !yield(token.Bundle{
|
||||
Cid: c,
|
||||
Decoded: bndl.token,
|
||||
Sealed: bndl.sealed,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
||||
// If not found, delegation.ErrDelegationNotFound is returned.
|
||||
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||
tkn, err := ctn.GetToken(cid)
|
||||
if err != nil { // only ErrNotFound expected
|
||||
return nil, delegation.ErrDelegationNotFound
|
||||
}
|
||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
||||
return tkn, nil
|
||||
}
|
||||
return nil, delegation.ErrDelegationNotFound
|
||||
}
|
||||
|
||||
// GetAllDelegations returns all the delegation.Token in the container.
|
||||
func (ctn Reader) GetAllDelegations() iter.Seq[delegation.Bundle] {
|
||||
return func(yield func(delegation.Bundle) bool) {
|
||||
for c, bndl := range ctn {
|
||||
if t, ok := bndl.token.(*delegation.Token); ok {
|
||||
if !yield(delegation.Bundle{
|
||||
Cid: c,
|
||||
Decoded: t,
|
||||
Sealed: bndl.sealed,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetInvocation returns a single invocation.Token.
|
||||
// If none are found, ErrNotFound is returned.
|
||||
// If more than one invocation exists, ErrMultipleInvocations is returned.
|
||||
func (ctn Reader) GetInvocation() (*invocation.Token, error) {
|
||||
var res *invocation.Token
|
||||
for _, bndl := range ctn {
|
||||
if inv, ok := bndl.token.(*invocation.Token); ok {
|
||||
if res != nil {
|
||||
return nil, ErrMultipleInvocations
|
||||
}
|
||||
res = inv
|
||||
}
|
||||
}
|
||||
if res == nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetAllInvocations returns all the invocation.Token in the container.
|
||||
func (ctn Reader) GetAllInvocations() iter.Seq[invocation.Bundle] {
|
||||
return func(yield func(invocation.Bundle) bool) {
|
||||
for c, bndl := range ctn {
|
||||
if t, ok := bndl.token.(*invocation.Token); ok {
|
||||
if !yield(invocation.Bundle{
|
||||
Cid: c,
|
||||
Decoded: t,
|
||||
Sealed: bndl.sealed,
|
||||
}) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ctn Reader) addToken(data []byte) error {
|
||||
@@ -131,6 +200,19 @@ func (ctn Reader) addToken(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctn[c] = tkn
|
||||
ctn[c] = bundle{
|
||||
sealed: data,
|
||||
token: tkn,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToWriter convert a container Reader into a Writer.
|
||||
// Most likely, you only want to use this in tests for convenience.
|
||||
func (ctn Reader) ToWriter() Writer {
|
||||
writer := NewWriter()
|
||||
for _, bndl := range ctn {
|
||||
writer.AddSealed(bndl.sealed)
|
||||
}
|
||||
return writer
|
||||
}
|
||||
|
||||
@@ -22,14 +22,22 @@ import (
|
||||
|
||||
func TestContainerRoundTrip(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
writer func(ctn Writer, w io.Writer) error
|
||||
reader func(io.Reader) (Reader, error)
|
||||
name string
|
||||
expectedHeader header
|
||||
writer any
|
||||
}{
|
||||
{"car", Writer.ToCar, FromCar},
|
||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
||||
{"cbor", Writer.ToCbor, FromCbor},
|
||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
||||
{"Bytes", headerRawBytes, Writer.ToBytes},
|
||||
{"BytesWriter", headerRawBytes, Writer.ToBytesWriter},
|
||||
{"BytesGzipped", headerRawBytesGzip, Writer.ToBytesGzipped},
|
||||
{"BytesGzippedWriter", headerRawBytesGzip, Writer.ToBytesGzippedWriter},
|
||||
{"Base64StdPadding", headerBase64StdPadding, Writer.ToBase64StdPadding},
|
||||
{"Base64StdPaddingWriter", headerBase64StdPadding, Writer.ToBase64StdPaddingWriter},
|
||||
{"Base64StdPaddingGzipped", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzipped},
|
||||
{"Base64StdPaddingGzippedWriter", headerBase64StdPaddingGzip, Writer.ToBase64StdPaddingGzippedWriter},
|
||||
{"Base64URL", headerBase64URL, Writer.ToBase64URL},
|
||||
{"Base64URLWriter", headerBase64URL, Writer.ToBase64URLWriter},
|
||||
{"Base64URLGzipped", headerBase64URLGzip, Writer.ToBase64URLGzipped},
|
||||
{"Base64URLGzipWriter", headerBase64URLGzip, Writer.ToBase64URLGzipWriter},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tokens := make(map[cid.Cid]*delegation.Token)
|
||||
@@ -39,21 +47,53 @@ func TestContainerRoundTrip(t *testing.T) {
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
dlg, c, data := randToken()
|
||||
writer.AddSealed(c, data)
|
||||
writer.AddSealed(data)
|
||||
tokens[c] = dlg
|
||||
dataSize += len(data)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
var reader Reader
|
||||
var serialLen int
|
||||
|
||||
err := tc.writer(writer, buf)
|
||||
require.NoError(t, err)
|
||||
switch fn := tc.writer.(type) {
|
||||
case func(ctn Writer, w io.Writer) error:
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := fn(writer, buf)
|
||||
require.NoError(t, err)
|
||||
serialLen = buf.Len()
|
||||
|
||||
t.Logf("data size %d", dataSize)
|
||||
t.Logf("container overhead: %d%%, %d bytes", int(float32(buf.Len()-dataSize)/float32(dataSize)*100.0), buf.Len()-dataSize)
|
||||
h, err := buf.ReadByte()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, byte(tc.expectedHeader), h)
|
||||
err = buf.UnreadByte()
|
||||
require.NoError(t, err)
|
||||
|
||||
reader, err := tc.reader(bytes.NewReader(buf.Bytes()))
|
||||
require.NoError(t, err)
|
||||
reader, err = FromReader(bytes.NewReader(buf.Bytes()))
|
||||
require.NoError(t, err)
|
||||
|
||||
case func(ctn Writer) ([]byte, error):
|
||||
b, err := fn(writer)
|
||||
require.NoError(t, err)
|
||||
serialLen = len(b)
|
||||
|
||||
require.Equal(t, byte(tc.expectedHeader), b[0])
|
||||
|
||||
reader, err = FromBytes(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
case func(ctn Writer) (string, error):
|
||||
s, err := fn(writer)
|
||||
require.NoError(t, err)
|
||||
serialLen = len(s)
|
||||
|
||||
require.Equal(t, byte(tc.expectedHeader), s[0])
|
||||
|
||||
reader, err = FromString(s)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Logf("data size %d, container size %d, overhead: %d%%, %d bytes",
|
||||
dataSize, serialLen, int(float32(serialLen-dataSize)/float32(dataSize)*100.0), serialLen-dataSize)
|
||||
|
||||
for c, dlg := range tokens {
|
||||
tknRead, err := reader.GetToken(c)
|
||||
@@ -98,16 +138,18 @@ func BenchmarkContainerSerialisation(b *testing.B) {
|
||||
writer func(ctn Writer, w io.Writer) error
|
||||
reader func(io.Reader) (Reader, error)
|
||||
}{
|
||||
{"car", Writer.ToCar, FromCar},
|
||||
{"carBase64", Writer.ToCarBase64, FromCarBase64},
|
||||
{"cbor", Writer.ToCbor, FromCbor},
|
||||
{"cborBase64", Writer.ToCborBase64, FromCborBase64},
|
||||
{"Bytes", Writer.ToBytesWriter, FromReader},
|
||||
{"BytesGzipped", Writer.ToBytesGzippedWriter, FromReader},
|
||||
{"Base64StdPadding", Writer.ToBase64StdPaddingWriter, FromReader},
|
||||
{"Base64StdPaddingGzipped", Writer.ToBase64StdPaddingGzippedWriter, FromReader},
|
||||
{"Base64URL", Writer.ToBase64URLWriter, FromReader},
|
||||
{"Base64URLGzip", Writer.ToBase64URLGzipWriter, FromReader},
|
||||
} {
|
||||
writer := NewWriter()
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
_, c, data := randToken()
|
||||
writer.AddSealed(c, data)
|
||||
_, _, data := randToken()
|
||||
writer.AddSealed(data)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
@@ -160,13 +202,12 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
|
||||
|
||||
opts := []delegation.Option{
|
||||
delegation.WithExpiration(time.Now().Add(time.Hour)),
|
||||
delegation.WithSubject(iss),
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
||||
}
|
||||
|
||||
t, err := delegation.New(priv, aud, cmd, pol, opts...)
|
||||
t, err := delegation.Root(iss, aud, cmd, pol, opts...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -176,3 +217,29 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
|
||||
}
|
||||
return t, c, b
|
||||
}
|
||||
|
||||
func FuzzContainerRead(f *testing.F) {
|
||||
// Generate a corpus
|
||||
for tokenCount := 0; tokenCount < 10; tokenCount++ {
|
||||
writer := NewWriter()
|
||||
for i := 0; i < tokenCount; i++ {
|
||||
_, _, data := randToken()
|
||||
writer.AddSealed(data)
|
||||
}
|
||||
data, err := writer.ToBytes()
|
||||
require.NoError(f, err)
|
||||
|
||||
f.Add(data)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
start := time.Now()
|
||||
|
||||
// search for panics
|
||||
_, _ = FromBytes(data)
|
||||
|
||||
if time.Since(start) > 100*time.Millisecond {
|
||||
panic("too long")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,61 +1,139 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/cbor"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
)
|
||||
|
||||
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
|
||||
|
||||
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
||||
type Writer map[cid.Cid][]byte
|
||||
type Writer map[string]struct{}
|
||||
|
||||
func NewWriter() Writer {
|
||||
return make(Writer)
|
||||
}
|
||||
|
||||
// AddSealed includes a "sealed" token (serialized with a ToSealed* function) in the container.
|
||||
func (ctn Writer) AddSealed(cid cid.Cid, data []byte) {
|
||||
ctn[cid] = data
|
||||
func (ctn Writer) AddSealed(data []byte) {
|
||||
ctn[string(data)] = struct{}{}
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCar(w io.Writer) error {
|
||||
return writeCar(w, nil, func(yield func(carBlock, error) bool) {
|
||||
for c, bytes := range ctn {
|
||||
if !yield(carBlock{c: c, data: bytes}, nil) {
|
||||
return
|
||||
// ToBytes encode the container into raw bytes.
|
||||
func (ctn Writer) ToBytes() ([]byte, error) {
|
||||
return ctn.toBytes(headerRawBytes)
|
||||
}
|
||||
|
||||
// ToBytesWriter is the same as ToBytes, but with an io.Writer.
|
||||
func (ctn Writer) ToBytesWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerRawBytes, w)
|
||||
}
|
||||
|
||||
// ToBytesGzipped encode the container into gzipped bytes.
|
||||
func (ctn Writer) ToBytesGzipped() ([]byte, error) {
|
||||
return ctn.toBytes(headerRawBytesGzip)
|
||||
}
|
||||
|
||||
// ToBytesGzippedWriter is the same as ToBytesGzipped, but with an io.Writer.
|
||||
func (ctn Writer) ToBytesGzippedWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerRawBytesGzip, w)
|
||||
}
|
||||
|
||||
// ToBase64StdPadding encode the container into a base64 string, with standard encoding and padding.
|
||||
func (ctn Writer) ToBase64StdPadding() (string, error) {
|
||||
return ctn.toString(headerBase64StdPadding)
|
||||
}
|
||||
|
||||
// ToBase64StdPaddingWriter is the same as ToBase64StdPadding, but with an io.Writer.
|
||||
func (ctn Writer) ToBase64StdPaddingWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerBase64StdPadding, w)
|
||||
}
|
||||
|
||||
// ToBase64StdPaddingGzipped encode the container into a pre-gzipped base64 string, with standard encoding and padding.
|
||||
func (ctn Writer) ToBase64StdPaddingGzipped() (string, error) {
|
||||
return ctn.toString(headerBase64StdPaddingGzip)
|
||||
}
|
||||
|
||||
// ToBase64StdPaddingGzippedWriter is the same as ToBase64StdPaddingGzipped, but with an io.Writer.
|
||||
func (ctn Writer) ToBase64StdPaddingGzippedWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerBase64StdPaddingGzip, w)
|
||||
}
|
||||
|
||||
// ToBase64URL encode the container into base64 string, with URL-safe encoding and no padding.
|
||||
func (ctn Writer) ToBase64URL() (string, error) {
|
||||
return ctn.toString(headerBase64URL)
|
||||
}
|
||||
|
||||
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
|
||||
func (ctn Writer) ToBase64URLWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerBase64URL, w)
|
||||
}
|
||||
|
||||
// ToBase64URL encode the container into pre-gzipped base64 string, with URL-safe encoding and no padding.
|
||||
func (ctn Writer) ToBase64URLGzipped() (string, error) {
|
||||
return ctn.toString(headerBase64URLGzip)
|
||||
}
|
||||
|
||||
// ToBase64URLWriter is the same as ToBase64URL, but with an io.Writer.
|
||||
func (ctn Writer) ToBase64URLGzipWriter(w io.Writer) error {
|
||||
return ctn.toWriter(headerBase64URLGzip, w)
|
||||
}
|
||||
|
||||
func (ctn Writer) toBytes(header header) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
err := ctn.toWriter(header, &buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (ctn Writer) toString(header header) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
err := ctn.toWriter(header, &buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (ctn Writer) toWriter(header header, w io.Writer) (err error) {
|
||||
encoder := header.encoder(w)
|
||||
|
||||
defer func() {
|
||||
err = encoder.Close()
|
||||
}()
|
||||
node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, containerVersionTag, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
|
||||
for data, _ := range ctn {
|
||||
qp.ListEntry(la, qp.Bytes([]byte(data)))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCarBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCar(w2)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCbor(w io.Writer) error {
|
||||
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
|
||||
for _, bytes := range ctn {
|
||||
qp.ListEntry(la, qp.Bytes(bytes))
|
||||
}
|
||||
}))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ipld.EncodeStreaming(w, node, dagcbor.Encode)
|
||||
|
||||
return ipld.EncodeStreaming(encoder, node, cbor.Encode)
|
||||
}
|
||||
|
||||
func (ctn Writer) ToCborBase64(w io.Writer) error {
|
||||
w2 := base64.NewEncoder(base64.StdEncoding, w)
|
||||
defer w2.Close()
|
||||
return ctn.ToCbor(w2)
|
||||
// ToReader convert a container Writer into a Reader.
|
||||
// Most likely, you only want to use this in tests for convenience.
|
||||
// This is not optimized and can panic.
|
||||
func (ctn Writer) ToReader() Reader {
|
||||
data, err := ctn.ToBytes()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
reader, err := FromBytes(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return reader
|
||||
}
|
||||
|
||||
18
pkg/container/writer_test.go
Normal file
18
pkg/container/writer_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWriterDedup(t *testing.T) {
|
||||
ctn := NewWriter()
|
||||
|
||||
_, _, sealed := randToken()
|
||||
ctn.AddSealed(sealed)
|
||||
require.Len(t, ctn, 1)
|
||||
|
||||
ctn.AddSealed(sealed)
|
||||
require.Len(t, ctn, 1)
|
||||
}
|
||||
184
pkg/meta/meta.go
184
pkg/meta/meta.go
@@ -1,24 +1,30 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"iter"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
"github.com/ucan-wg/go-ucan/pkg/secretbox"
|
||||
)
|
||||
|
||||
var ErrUnsupported = fmt.Errorf("failure adding unsupported type to meta")
|
||||
var ErrNotFound = errors.New("key not found in meta")
|
||||
|
||||
var ErrNotFound = fmt.Errorf("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.
|
||||
// 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.
|
||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||
// and transformations, while hiding the IPLD complexity from the caller.
|
||||
type Meta struct {
|
||||
// This type must be compatible with the IPLD type represented by the IPLD
|
||||
// schema { String : Any }.
|
||||
|
||||
Keys []string
|
||||
Values map[string]ipld.Node
|
||||
}
|
||||
@@ -50,6 +56,21 @@ func (m *Meta) GetString(key string) (string, error) {
|
||||
return v.AsString()
|
||||
}
|
||||
|
||||
// GetEncryptedString decorates GetString and decrypt its output with the given symmetric encryption key.
|
||||
func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||
v, err := m.GetBytes(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(decrypted), nil
|
||||
}
|
||||
|
||||
// GetInt64 retrieves a value as an int64.
|
||||
// Returns ErrNotFound if the given key is missing.
|
||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||
@@ -83,9 +104,23 @@ func (m *Meta) GetBytes(key string) ([]byte, error) {
|
||||
return v.AsBytes()
|
||||
}
|
||||
|
||||
// GetEncryptedBytes decorates GetBytes and decrypt its output with the given symmetric encryption key.
|
||||
func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||
v, err := m.GetBytes(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decrypted, err := secretbox.DecryptStringWithKey(v, encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
// GetNode retrieves a value as a raw IPLD node.
|
||||
// Returns ErrNotFound if the given key is missing.
|
||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||
func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
||||
v, ok := m.Values[key]
|
||||
if !ok {
|
||||
@@ -95,35 +130,84 @@ func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
||||
}
|
||||
|
||||
// Add adds a key/value pair in the meta set.
|
||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
||||
// and ipld.Node.
|
||||
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||
func (m *Meta) Add(key string, val any) error {
|
||||
switch val := val.(type) {
|
||||
case bool:
|
||||
m.Values[key] = basicnode.NewBool(val)
|
||||
case string:
|
||||
m.Values[key] = basicnode.NewString(val)
|
||||
case int:
|
||||
m.Values[key] = basicnode.NewInt(int64(val))
|
||||
case int32:
|
||||
m.Values[key] = basicnode.NewInt(int64(val))
|
||||
case int64:
|
||||
m.Values[key] = basicnode.NewInt(val)
|
||||
case float32:
|
||||
m.Values[key] = basicnode.NewFloat(float64(val))
|
||||
case float64:
|
||||
m.Values[key] = basicnode.NewFloat(val)
|
||||
case []byte:
|
||||
m.Values[key] = basicnode.NewBytes(val)
|
||||
case datamodel.Node:
|
||||
m.Values[key] = val
|
||||
default:
|
||||
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
|
||||
if _, ok := m.Values[key]; ok {
|
||||
return fmt.Errorf("duplicate key %q", key)
|
||||
}
|
||||
|
||||
node, err := literal.Any(val)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.Keys = append(m.Keys, key)
|
||||
m.Values[key] = node
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddEncrypted adds a key/value pair in the meta set.
|
||||
// The value is encrypted with the given encryptionKey.
|
||||
// Accepted types for the value are: string, []byte.
|
||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||
func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
||||
var encrypted []byte
|
||||
var err error
|
||||
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
encrypted, err = secretbox.EncryptWithKey([]byte(val), encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case []byte:
|
||||
encrypted, err = secretbox.EncryptWithKey(val, encryptionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return ErrNotEncryptable
|
||||
}
|
||||
|
||||
return m.Add(key, encrypted)
|
||||
}
|
||||
|
||||
type Iterator interface {
|
||||
Iter() iter.Seq2[string, ipld.Node]
|
||||
}
|
||||
|
||||
// Include merges the provided meta into the existing one.
|
||||
//
|
||||
// If duplicate keys are encountered, the new value is silently dropped
|
||||
// without causing an error.
|
||||
func (m *Meta) Include(other Iterator) {
|
||||
for key, value := range other.Iter() {
|
||||
if _, ok := m.Values[key]; ok {
|
||||
// don't overwrite
|
||||
continue
|
||||
}
|
||||
m.Values[key] = value
|
||||
m.Keys = append(m.Keys, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of key/values.
|
||||
func (m *Meta) Len() int {
|
||||
return len(m.Values)
|
||||
}
|
||||
|
||||
// Iter iterates over the meta key/values
|
||||
func (m *Meta) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return func(yield func(string, ipld.Node) bool) {
|
||||
for _, key := range m.Keys {
|
||||
if !yield(key, m.Values[key]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Equals tells if two Meta hold the same key/values.
|
||||
func (m *Meta) Equals(other *Meta) bool {
|
||||
if len(m.Keys) != len(other.Keys) {
|
||||
@@ -141,37 +225,41 @@ func (m *Meta) Equals(other *Meta) bool {
|
||||
}
|
||||
|
||||
func (m *Meta) String() string {
|
||||
sort.Strings(m.Keys)
|
||||
|
||||
buf := strings.Builder{}
|
||||
buf.WriteString("{")
|
||||
|
||||
var i int
|
||||
for key, node := range m.Values {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
i++
|
||||
buf.WriteString("\n\t")
|
||||
buf.WriteString(key)
|
||||
buf.WriteString(":")
|
||||
buf.WriteString(printer.Sprint(node))
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t"))
|
||||
buf.WriteString(",")
|
||||
}
|
||||
|
||||
if len(m.Values) > 0 {
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
buf.WriteString("}")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// ReadOnly returns a read-only version of Meta.
|
||||
func (m *Meta) ReadOnly() ReadOnly {
|
||||
return ReadOnly{m: m}
|
||||
return ReadOnly{meta: m}
|
||||
}
|
||||
|
||||
func fqtn(val any) string {
|
||||
var name string
|
||||
|
||||
t := reflect.TypeOf(val)
|
||||
for t.Kind() == reflect.Pointer {
|
||||
name += "*"
|
||||
t = t.Elem()
|
||||
// Clone makes a deep copy.
|
||||
func (m *Meta) Clone() *Meta {
|
||||
res := &Meta{
|
||||
Keys: make([]string, len(m.Keys)),
|
||||
Values: make(map[string]ipld.Node, len(m.Values)),
|
||||
}
|
||||
|
||||
return name + t.PkgPath() + "." + t.Name()
|
||||
copy(res.Keys, m.Keys)
|
||||
for k, v := range m.Values {
|
||||
res.Values[k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package meta_test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"maps"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/assert"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
)
|
||||
@@ -14,11 +17,114 @@ func TestMeta_Add(t *testing.T) {
|
||||
|
||||
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()
|
||||
|
||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
||||
require.ErrorIs(t, err, meta.ErrUnsupported)
|
||||
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("encrypted meta", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, 32)
|
||||
_, err := rand.Read(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := meta.NewMeta()
|
||||
|
||||
// string encryption
|
||||
err = m.AddEncrypted("secret", "hello world", key)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = m.GetString("secret")
|
||||
require.Error(t, err) // the ciphertext is saved as []byte instead of string
|
||||
|
||||
decrypted, err := m.GetEncryptedString("secret", key)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", decrypted)
|
||||
|
||||
// bytes encryption
|
||||
originalBytes := make([]byte, 128)
|
||||
_, err = rand.Read(originalBytes)
|
||||
require.NoError(t, err)
|
||||
err = m.AddEncrypted("secret-bytes", originalBytes, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
encryptedBytes, err := m.GetBytes("secret-bytes")
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, originalBytes, encryptedBytes)
|
||||
|
||||
decryptedBytes, err := m.GetEncryptedBytes("secret-bytes", key)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, originalBytes, decryptedBytes)
|
||||
|
||||
// error cases
|
||||
t.Run("error on unsupported type", func(t *testing.T) {
|
||||
err := m.AddEncrypted("invalid", 123, key)
|
||||
require.ErrorIs(t, err, meta.ErrNotEncryptable)
|
||||
})
|
||||
|
||||
t.Run("error on invalid key size", func(t *testing.T) {
|
||||
err := m.AddEncrypted("invalid", "test", []byte("short-key"))
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid key size")
|
||||
})
|
||||
|
||||
t.Run("error on nil key", func(t *testing.T) {
|
||||
err := m.AddEncrypted("invalid", "test", nil)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "encryption key is required")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIterCloneEquals(t *testing.T) {
|
||||
m := meta.NewMeta()
|
||||
|
||||
require.NoError(t, m.Add("foo", "bar"))
|
||||
require.NoError(t, m.Add("baz", 1234))
|
||||
|
||||
expected := map[string]ipld.Node{
|
||||
"foo": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
}
|
||||
|
||||
// meta -> iter
|
||||
require.Equal(t, expected, maps.Collect(m.Iter()))
|
||||
|
||||
// readonly -> iter
|
||||
ro := m.ReadOnly()
|
||||
require.Equal(t, expected, maps.Collect(ro.Iter()))
|
||||
|
||||
// meta -> clone -> iter
|
||||
clone := m.Clone()
|
||||
require.Equal(t, expected, maps.Collect(clone.Iter()))
|
||||
|
||||
// readonly -> WriteableClone -> iter
|
||||
wclone := ro.WriteableClone()
|
||||
require.Equal(t, expected, maps.Collect(wclone.Iter()))
|
||||
|
||||
require.True(t, m.Equals(wclone))
|
||||
require.True(t, ro.Equals(wclone.ReadOnly()))
|
||||
}
|
||||
|
||||
func TestInclude(t *testing.T) {
|
||||
m1 := meta.NewMeta()
|
||||
|
||||
require.NoError(t, m1.Add("samekey", "bar"))
|
||||
require.NoError(t, m1.Add("baz", 1234))
|
||||
|
||||
m2 := meta.NewMeta()
|
||||
|
||||
require.NoError(t, m2.Add("samekey", "othervalue")) // check no overwrite
|
||||
require.NoError(t, m2.Add("otherkey", 1234))
|
||||
|
||||
m1.Include(m2)
|
||||
|
||||
require.Equal(t, map[string]ipld.Node{
|
||||
"samekey": basicnode.NewString("bar"),
|
||||
"baz": basicnode.NewInt(1234),
|
||||
"otherkey": basicnode.NewInt(1234),
|
||||
}, maps.Collect(m1.Iter()))
|
||||
}
|
||||
|
||||
@@ -1,42 +1,64 @@
|
||||
package meta
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
)
|
||||
|
||||
// ReadOnly wraps a Meta into a read-only facade.
|
||||
type ReadOnly struct {
|
||||
m *Meta
|
||||
meta *Meta
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetBool(key string) (bool, error) {
|
||||
return r.m.GetBool(key)
|
||||
return r.meta.GetBool(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetString(key string) (string, error) {
|
||||
return r.m.GetString(key)
|
||||
return r.meta.GetString(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||
return r.meta.GetEncryptedString(key, encryptionKey)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||
return r.m.GetInt64(key)
|
||||
return r.meta.GetInt64(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetFloat64(key string) (float64, error) {
|
||||
return r.m.GetFloat64(key)
|
||||
return r.meta.GetFloat64(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
||||
return r.m.GetBytes(key)
|
||||
return r.meta.GetBytes(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||
return r.meta.GetEncryptedBytes(key, encryptionKey)
|
||||
}
|
||||
|
||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||
return r.m.GetNode(key)
|
||||
return r.meta.GetNode(key)
|
||||
}
|
||||
|
||||
func (r ReadOnly) Len() int {
|
||||
return r.meta.Len()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Iter() iter.Seq2[string, ipld.Node] {
|
||||
return r.meta.Iter()
|
||||
}
|
||||
|
||||
func (r ReadOnly) Equals(other ReadOnly) bool {
|
||||
return r.m.Equals(other.m)
|
||||
return r.meta.Equals(other.meta)
|
||||
}
|
||||
|
||||
func (r ReadOnly) String() string {
|
||||
return r.m.String()
|
||||
return r.meta.String()
|
||||
}
|
||||
|
||||
func (r ReadOnly) WriteableClone() *Meta {
|
||||
return r.meta.Clone()
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
)
|
||||
|
||||
func FromIPLD(node datamodel.Node) (Policy, error) {
|
||||
if err := limits.ValidateIntegerBoundsIPLD(node); err != nil {
|
||||
return nil, fmt.Errorf("policy contains integer values outside safe bounds: %w", err)
|
||||
}
|
||||
|
||||
return statementsFromIPLD("/", node)
|
||||
}
|
||||
|
||||
@@ -76,7 +81,7 @@ func statementFromIPLD(path string, node datamodel.Node) (Statement, error) {
|
||||
}
|
||||
case 3:
|
||||
switch op {
|
||||
case KindEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
||||
case KindEqual, KindNotEqual, KindLessThan, KindLessThanOrEqual, KindGreaterThan, KindGreaterThanOrEqual:
|
||||
sel, err := arg2AsSelector(op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -21,10 +21,31 @@ func TestIpldRoundTrip(t *testing.T) {
|
||||
]
|
||||
]`
|
||||
|
||||
// must contain all the operators
|
||||
const allOps = `
|
||||
[
|
||||
["and", [
|
||||
["==", ".foo1", ".bar1"],
|
||||
["!=", ".foo2", ".bar2"]
|
||||
]],
|
||||
["or", [
|
||||
[">", ".foo5", 5.2],
|
||||
[">=", ".foo6", 6.2]
|
||||
]],
|
||||
["not", ["like", ".foo7", "*@example.com"]],
|
||||
["all", ".foo8",
|
||||
["<", ".foo3", 3]
|
||||
],
|
||||
["any", ".foo9",
|
||||
["<=", ".foo4", 4]
|
||||
]
|
||||
]`
|
||||
|
||||
for _, tc := range []struct {
|
||||
name, dagJsonStr string
|
||||
}{
|
||||
{"illustrativeExample", illustrativeExample},
|
||||
{"allOps", allOps},
|
||||
} {
|
||||
nodes, err := ipld.Decode([]byte(tc.dagJsonStr), dagjson.Decode)
|
||||
require.NoError(t, err)
|
||||
|
||||
49
pkg/policy/limits/int.go
Normal file
49
pkg/policy/limits/int.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxInt53 represents the maximum safe integer in JavaScript (2^53 - 1)
|
||||
MaxInt53 int64 = 9007199254740991
|
||||
// MinInt53 represents the minimum safe integer in JavaScript (-2^53 + 1)
|
||||
MinInt53 int64 = -9007199254740991
|
||||
)
|
||||
|
||||
func ValidateIntegerBoundsIPLD(node ipld.Node) error {
|
||||
switch node.Kind() {
|
||||
case ipld.Kind_Int:
|
||||
val := must.Int(node)
|
||||
if val > MaxInt53 || val < MinInt53 {
|
||||
return fmt.Errorf("integer value %d exceeds safe bounds", val)
|
||||
}
|
||||
case ipld.Kind_List:
|
||||
it := node.ListIterator()
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case ipld.Kind_Map:
|
||||
it := node.MapIterator()
|
||||
for !it.Done() {
|
||||
_, v, err := it.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateIntegerBoundsIPLD(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
82
pkg/policy/limits/int_test.go
Normal file
82
pkg/policy/limits/int_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateIntegerBoundsIPLD(t *testing.T) {
|
||||
buildMap := func() datamodel.Node {
|
||||
nb := basicnode.Prototype.Any.NewBuilder()
|
||||
qp.Map(1, func(ma datamodel.MapAssembler) {
|
||||
qp.MapEntry(ma, "foo", qp.Int(MaxInt53+1))
|
||||
})(nb)
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
buildList := func() datamodel.Node {
|
||||
nb := basicnode.Prototype.Any.NewBuilder()
|
||||
qp.List(1, func(la datamodel.ListAssembler) {
|
||||
qp.ListEntry(la, qp.Int(MinInt53-1))
|
||||
})(nb)
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input datamodel.Node
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
input: basicnode.NewInt(42),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe int",
|
||||
input: basicnode.NewInt(MaxInt53),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "min safe int",
|
||||
input: basicnode.NewInt(MinInt53),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "above MaxInt53",
|
||||
input: basicnode.NewInt(MaxInt53 + 1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "below MinInt53",
|
||||
input: basicnode.NewInt(MinInt53 - 1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nested map with invalid int",
|
||||
input: buildMap(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "nested list with invalid int",
|
||||
input: buildList(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateIntegerBoundsIPLD(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds safe bounds")
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package literal
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
var Bool = basicnode.NewBool
|
||||
@@ -33,8 +36,14 @@ func Null() ipld.Node {
|
||||
// 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) {
|
||||
for k, v := range m {
|
||||
qp.MapEntry(ma, k, anyAssemble(v))
|
||||
// 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]))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -48,6 +57,72 @@ func List[T any](l []T) (ipld.Node, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// Any creates an IPLD node from any value
|
||||
// If possible, use another dedicated function for your type for performance.
|
||||
func Any(v any) (res ipld.Node, err error) {
|
||||
// some fast path
|
||||
switch val := v.(type) {
|
||||
case bool:
|
||||
return basicnode.NewBool(val), nil
|
||||
case string:
|
||||
return basicnode.NewString(val), nil
|
||||
case int:
|
||||
i := int64(val)
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", i)
|
||||
}
|
||||
return basicnode.NewInt(i), nil
|
||||
case int8:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case int16:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case int32:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case int64:
|
||||
if val > limits.MaxInt53 || val < limits.MinInt53 {
|
||||
return nil, fmt.Errorf("integer value %d exceeds safe integer bounds", val)
|
||||
}
|
||||
return basicnode.NewInt(val), nil
|
||||
case uint:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case uint8:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case uint16:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case uint32:
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case uint64:
|
||||
if val > uint64(limits.MaxInt53) {
|
||||
return nil, fmt.Errorf("unsigned integer value %d exceeds safe integer bounds", val)
|
||||
}
|
||||
return basicnode.NewInt(int64(val)), nil
|
||||
case float32:
|
||||
return basicnode.NewFloat(float64(val)), nil
|
||||
case float64:
|
||||
return basicnode.NewFloat(val), nil
|
||||
case []byte:
|
||||
return basicnode.NewBytes(val), nil
|
||||
case datamodel.Node:
|
||||
return val, nil
|
||||
case cid.Cid:
|
||||
return LinkCid(val), nil
|
||||
default:
|
||||
}
|
||||
|
||||
builder := basicnode.Prototype__Any{}.NewBuilder()
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%v", r)
|
||||
res = nil
|
||||
}
|
||||
}()
|
||||
|
||||
anyAssemble(v)(builder)
|
||||
|
||||
return builder.Build(), nil
|
||||
}
|
||||
|
||||
func anyAssemble(val any) qp.Assemble {
|
||||
var rt reflect.Type
|
||||
var rv reflect.Value
|
||||
@@ -90,22 +165,39 @@ func anyAssemble(val any) qp.Assemble {
|
||||
if rt.Key().Kind() != reflect.String {
|
||||
break
|
||||
}
|
||||
it := rv.MapRange()
|
||||
// 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 it.Next() {
|
||||
qp.MapEntry(ma, it.Key().String(), anyAssemble(it.Value()))
|
||||
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())
|
||||
i := rv.Int()
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
panic(fmt.Sprintf("integer %d exceeds safe bounds", i))
|
||||
}
|
||||
return qp.Int(i)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return qp.Int(int64(rv.Uint()))
|
||||
u := rv.Uint()
|
||||
if u > uint64(limits.MaxInt53) {
|
||||
panic(fmt.Sprintf("unsigned integer %d exceeds safe bounds", u))
|
||||
}
|
||||
return qp.Int(int64(u))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return qp.Float(rv.Float())
|
||||
case reflect.String:
|
||||
return qp.String(rv.String())
|
||||
case reflect.Struct:
|
||||
if rt == reflect.TypeOf(cid.Cid{}) {
|
||||
c := rv.Interface().(cid.Cid)
|
||||
return qp.Link(cidlink.Link{Cid: c})
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ package literal
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
@@ -53,6 +57,7 @@ func TestMap(t *testing.T) {
|
||||
"barbar": "foo",
|
||||
},
|
||||
},
|
||||
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -115,6 +120,190 @@ func TestMap(t *testing.T) {
|
||||
string{"barbar"}: string{"foo"}
|
||||
}
|
||||
}`, printer.Sprint(v))
|
||||
|
||||
v, err = n.LookupByString("link")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
||||
asLink, err := v.AsLink()
|
||||
require.NoError(t, err)
|
||||
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||
}
|
||||
|
||||
func TestAny(t *testing.T) {
|
||||
data := map[string]any{
|
||||
"bool": true,
|
||||
"string": "foobar",
|
||||
"bytes": []byte{1, 2, 3, 4},
|
||||
"int": 1234,
|
||||
"uint": uint(12345),
|
||||
"float": 1.45,
|
||||
"slice": []int{1, 2, 3},
|
||||
"array": [2]int{1, 2},
|
||||
"map": map[string]any{
|
||||
"foo": "bar",
|
||||
"foofoo": map[string]string{
|
||||
"barbar": "foo",
|
||||
},
|
||||
},
|
||||
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
||||
"func": func() {},
|
||||
}
|
||||
|
||||
v, err := Any(data["bool"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
||||
require.Equal(t, true, must(v.AsBool()))
|
||||
|
||||
v, err = Any(data["string"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_String, v.Kind())
|
||||
require.Equal(t, "foobar", must(v.AsString()))
|
||||
|
||||
v, err = Any(data["bytes"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
||||
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
||||
|
||||
v, err = Any(data["int"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||
require.Equal(t, int64(1234), must(v.AsInt()))
|
||||
|
||||
v, err = Any(data["uint"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||
require.Equal(t, int64(12345), must(v.AsInt()))
|
||||
|
||||
v, err = Any(data["float"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
||||
require.Equal(t, 1.45, must(v.AsFloat()))
|
||||
|
||||
v, err = Any(data["slice"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||
require.Equal(t, int64(3), v.Length())
|
||||
require.Equal(t, `list{
|
||||
0: int{1}
|
||||
1: int{2}
|
||||
2: int{3}
|
||||
}`, printer.Sprint(v))
|
||||
|
||||
v, err = Any(data["array"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||
require.Equal(t, int64(2), v.Length())
|
||||
require.Equal(t, `list{
|
||||
0: int{1}
|
||||
1: int{2}
|
||||
}`, printer.Sprint(v))
|
||||
|
||||
v, err = Any(data["map"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
||||
require.Equal(t, int64(2), v.Length())
|
||||
require.Equal(t, `map{
|
||||
string{"foo"}: string{"bar"}
|
||||
string{"foofoo"}: map{
|
||||
string{"barbar"}: string{"foo"}
|
||||
}
|
||||
}`, printer.Sprint(v))
|
||||
|
||||
v, err = Any(data["link"])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
||||
asLink, err := v.AsLink()
|
||||
require.NoError(t, err)
|
||||
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||
|
||||
_, err = Any(data["func"])
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func BenchmarkAny(b *testing.B) {
|
||||
b.Run("bool", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = Any(true)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("string", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = Any("foobar")
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("bytes", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = Any([]byte{1, 2, 3, 4})
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("map", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = Any(map[string]any{
|
||||
"foo": "bar",
|
||||
"foofoo": map[string]string{
|
||||
"barbar": "foo",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnyAssembleIntegerOverflow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid int",
|
||||
input: 42,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe int",
|
||||
input: limits.MaxInt53,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "min safe int",
|
||||
input: limits.MinInt53,
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "overflow int",
|
||||
input: int64(limits.MaxInt53 + 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "underflow int",
|
||||
input: int64(limits.MinInt53 - 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "overflow uint",
|
||||
input: uint64(limits.MaxInt53 + 1),
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := Any(tt.input)
|
||||
if tt.shouldErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
|
||||
@@ -3,6 +3,7 @@ package policy
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
@@ -10,17 +11,18 @@ import (
|
||||
)
|
||||
|
||||
// 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 {
|
||||
res, _ := matchStatement(stmt, node)
|
||||
res, leaf := matchStatement(stmt, node)
|
||||
switch res {
|
||||
case matchResultNoData, matchResultFalse:
|
||||
return false
|
||||
return false, leaf
|
||||
case matchResultOptionalNoData, matchResultTrue:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
|
||||
@@ -80,6 +82,17 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
||||
}
|
||||
return boolToRes(datamodel.DeepEqual(s.value, res))
|
||||
}
|
||||
case KindNotEqual:
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
if err != nil {
|
||||
return matchResultNoData, cur
|
||||
}
|
||||
if res == nil { // optional selector didn't match
|
||||
return matchResultOptionalNoData, nil
|
||||
}
|
||||
return boolToRes(!datamodel.DeepEqual(s.value, res))
|
||||
}
|
||||
case KindGreaterThan:
|
||||
if s, ok := cur.(equality); ok {
|
||||
res, err := s.selector.Select(node)
|
||||
@@ -131,9 +144,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
||||
case matchResultNoData, matchResultOptionalNoData:
|
||||
return res, leaf
|
||||
case matchResultTrue:
|
||||
return matchResultFalse, leaf
|
||||
return matchResultFalse, cur
|
||||
case matchResultFalse:
|
||||
return matchResultTrue, leaf
|
||||
return matchResultTrue, nil
|
||||
}
|
||||
}
|
||||
case KindAnd:
|
||||
@@ -248,10 +261,22 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
||||
panic(fmt.Errorf("unimplemented statement kind: %s", cur.Kind()))
|
||||
}
|
||||
|
||||
// isOrdered compares two IPLD nodes and returns true if they satisfy the given ordering function.
|
||||
// It supports comparison of integers and floats, returning false for:
|
||||
// - Nodes of different or unsupported kinds
|
||||
// - Integer values outside JavaScript's safe integer bounds (±2^53-1)
|
||||
// - Non-finite floating point values (NaN or ±Inf)
|
||||
//
|
||||
// The satisfies parameter is a function that interprets the comparison result:
|
||||
// - For ">" it returns true when order is 1
|
||||
// - For ">=" it returns true when order is 0 or 1
|
||||
// - For "<" it returns true when order is -1
|
||||
// - For "<=" it returns true when order is -1 or 0
|
||||
func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) bool) bool {
|
||||
if expected.Kind() == ipld.Kind_Int && actual.Kind() == ipld.Kind_Int {
|
||||
a := must.Int(actual)
|
||||
b := must.Int(expected)
|
||||
|
||||
return satisfies(cmp.Compare(a, b))
|
||||
}
|
||||
|
||||
@@ -264,6 +289,11 @@ func isOrdered(expected ipld.Node, actual ipld.Node, satisfies func(order int) b
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("extracting selector float: %w", err))
|
||||
}
|
||||
|
||||
if math.IsInf(a, 0) || math.IsNaN(a) || math.IsInf(b, 0) || math.IsNaN(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
return satisfies(cmp.Compare(a, b))
|
||||
}
|
||||
|
||||
|
||||
@@ -16,229 +16,420 @@ import (
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
t.Run("equality", func(t *testing.T) {
|
||||
t.Run("string", func(t *testing.T) {
|
||||
np := basicnode.Prototype.String
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignString("test")
|
||||
nd := nb.Build()
|
||||
t.Run("eq string", func(t *testing.T) {
|
||||
nd := literal.String("test")
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.String("test")))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("test2")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Int(138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
t.Run("eq int", func(t *testing.T) {
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Int(138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("float", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Float
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignFloat(1.138)
|
||||
nd := nb.Build()
|
||||
t.Run("eq float", func(t *testing.T) {
|
||||
nd := literal.Float(1.138)
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Float(1.138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("IPLD Link", func(t *testing.T) {
|
||||
t.Run("eq IPLD Link", func(t *testing.T) {
|
||||
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
||||
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
||||
|
||||
np := basicnode.Prototype.Link
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignLink(l0)
|
||||
nd := nb.Build()
|
||||
nd := literal.Link(l0)
|
||||
|
||||
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("string in map", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Map
|
||||
nb := np.NewBuilder()
|
||||
ma, _ := nb.BeginMap(1)
|
||||
ma.AssembleKey().AssignString("foo")
|
||||
ma.AssembleValue().AssignString("bar")
|
||||
ma.Finish()
|
||||
nd := nb.Build()
|
||||
t.Run("eq string in map", func(t *testing.T) {
|
||||
nd, _ := literal.Map(map[string]any{
|
||||
"foo": "bar",
|
||||
})
|
||||
|
||||
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("string in list", func(t *testing.T) {
|
||||
np := basicnode.Prototype.List
|
||||
nb := np.NewBuilder()
|
||||
la, _ := nb.BeginList(1)
|
||||
la.AssembleValue().AssignString("foo")
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
t.Run("eq string in list", func(t *testing.T) {
|
||||
nd, _ := literal.List([]any{"foo"})
|
||||
|
||||
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("neq string", func(t *testing.T) {
|
||||
nd := literal.String("test")
|
||||
|
||||
pol := MustConstruct(NotEqual(".", literal.String("test")))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.String("test2")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.Int(138)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("neq int", func(t *testing.T) {
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(NotEqual(".", literal.Int(138)))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.Int(1138)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.String("138")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("neq float", func(t *testing.T) {
|
||||
nd := literal.Float(1.138)
|
||||
|
||||
pol := MustConstruct(NotEqual(".", literal.Float(1.138)))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.Float(11.38)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.String("138")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("neq IPLD Link", func(t *testing.T) {
|
||||
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
||||
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
||||
|
||||
nd := literal.Link(l0)
|
||||
|
||||
pol := MustConstruct(NotEqual(".", literal.Link(l0)))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.Link(l1)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("neq string in map", func(t *testing.T) {
|
||||
nd, _ := literal.Map(map[string]any{
|
||||
"foo": "bar",
|
||||
})
|
||||
|
||||
pol := MustConstruct(NotEqual(".foo", literal.String("bar")))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".[\"foo\"]", literal.String("bar")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".foo", literal.String("baz")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
// missing data will fail, as not optional
|
||||
pol = MustConstruct(NotEqual(".foobar", literal.String("bar")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("neq string in list", func(t *testing.T) {
|
||||
nd, _ := literal.List([]any{"foo"})
|
||||
|
||||
pol := MustConstruct(NotEqual(".[0]", literal.String("foo")))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(NotEqual(".[0]", literal.String("bar")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
// missing data will fail, as not optional
|
||||
pol = MustConstruct(NotEqual(".[1]", literal.String("foo")))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("inequality", func(t *testing.T) {
|
||||
t.Run("gt int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThan(".", literal.Int(138)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThan(".", literal.Int(140)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("gte int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(140)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("gt float", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Float
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignFloat(1.38)
|
||||
nd := nb.Build()
|
||||
nd := literal.Float(1.38)
|
||||
|
||||
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThan(".", literal.Float(2)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("gte float", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Float
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignFloat(1.38)
|
||||
nd := nb.Build()
|
||||
nd := literal.Float(1.38)
|
||||
|
||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(2)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("lt int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThan(".", literal.Int(138)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(LessThan(".", literal.Int(100)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("lte int", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(100)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("lt float", func(t *testing.T) {
|
||||
nd := literal.Float(1.38)
|
||||
|
||||
pol := MustConstruct(LessThan(".", literal.Float(1)))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(LessThan(".", literal.Float(2)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("lte float", func(t *testing.T) {
|
||||
nd := literal.Float(1.38)
|
||||
|
||||
pol := MustConstruct(LessThanOrEqual(".", literal.Float(1)))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(LessThanOrEqual(".", literal.Float(2)))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("negation", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Bool
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignBool(false)
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.Map(map[string]any{
|
||||
"foo": false,
|
||||
})
|
||||
|
||||
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
||||
ok := pol.Match(nd)
|
||||
pol := MustConstruct(Not(Equal(".foo", literal.Bool(true))))
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
||||
ok = pol.Match(nd)
|
||||
pol = MustConstruct(Not(Equal(".foo", literal.Bool(false))))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
// missing data will fail, as not optional
|
||||
pol = MustConstruct(Not(Equal(".foobar", literal.Bool(true))))
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, MustConstruct(Equal(".foobar", literal.Bool(true)))[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("conjunction", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(
|
||||
And(
|
||||
@@ -246,8 +437,9 @@ func TestMatch(t *testing.T) {
|
||||
LessThan(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(
|
||||
And(
|
||||
@@ -255,19 +447,18 @@ func TestMatch(t *testing.T) {
|
||||
Equal(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, MustConstruct(Equal(".", literal.Int(1138)))[0], leaf)
|
||||
|
||||
pol = MustConstruct(And())
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("disjunction", func(t *testing.T) {
|
||||
np := basicnode.Prototype.Int
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignInt(138)
|
||||
nd := nb.Build()
|
||||
nd := literal.Int(138)
|
||||
|
||||
pol := MustConstruct(
|
||||
Or(
|
||||
@@ -275,8 +466,9 @@ func TestMatch(t *testing.T) {
|
||||
LessThan(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(
|
||||
Or(
|
||||
@@ -284,12 +476,14 @@ func TestMatch(t *testing.T) {
|
||||
Equal(".", literal.Int(1138)),
|
||||
),
|
||||
)
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
|
||||
pol = MustConstruct(Or())
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
|
||||
t.Run("wildcard", func(t *testing.T) {
|
||||
@@ -303,14 +497,12 @@ func TestMatch(t *testing.T) {
|
||||
} {
|
||||
func(s string) {
|
||||
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
||||
np := basicnode.Prototype.String
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
nd := literal.String(s)
|
||||
|
||||
pol := MustConstruct(Like(".", pattern))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
})
|
||||
}(s)
|
||||
}
|
||||
@@ -324,70 +516,56 @@ func TestMatch(t *testing.T) {
|
||||
} {
|
||||
func(s string) {
|
||||
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
||||
np := basicnode.Prototype.String
|
||||
nb := np.NewBuilder()
|
||||
nb.AssignString(s)
|
||||
nd := nb.Build()
|
||||
nd := literal.String(s)
|
||||
|
||||
pol := MustConstruct(Like(".", pattern))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
}(s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("quantification", func(t *testing.T) {
|
||||
buildValueNode := func(v int64) ipld.Node {
|
||||
np := basicnode.Prototype.Map
|
||||
nb := np.NewBuilder()
|
||||
ma, _ := nb.BeginMap(1)
|
||||
ma.AssembleKey().AssignString("value")
|
||||
ma.AssembleValue().AssignInt(v)
|
||||
ma.Finish()
|
||||
return nb.Build()
|
||||
}
|
||||
|
||||
t.Run("all", func(t *testing.T) {
|
||||
np := basicnode.Prototype.List
|
||||
nb := np.NewBuilder()
|
||||
la, _ := nb.BeginList(5)
|
||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.List([]any{
|
||||
map[string]int{"value": 5},
|
||||
map[string]int{"value": 10},
|
||||
map[string]int{"value": 20},
|
||||
map[string]int{"value": 50},
|
||||
map[string]int{"value": 100},
|
||||
})
|
||||
|
||||
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, MustConstruct(GreaterThan(".value", literal.Int(20)))[0], leaf)
|
||||
})
|
||||
|
||||
t.Run("any", func(t *testing.T) {
|
||||
np := basicnode.Prototype.List
|
||||
nb := np.NewBuilder()
|
||||
la, _ := nb.BeginList(5)
|
||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
||||
la.Finish()
|
||||
nd := nb.Build()
|
||||
nd, _ := literal.List([]any{
|
||||
map[string]int{"value": 5},
|
||||
map[string]int{"value": 10},
|
||||
map[string]int{"value": 20},
|
||||
map[string]int{"value": 50},
|
||||
map[string]int{"value": 100},
|
||||
})
|
||||
|
||||
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
||||
ok := pol.Match(nd)
|
||||
ok, leaf := pol.Match(nd)
|
||||
require.True(t, ok)
|
||||
require.Nil(t, leaf)
|
||||
|
||||
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
||||
ok = pol.Match(nd)
|
||||
ok, leaf = pol.Match(nd)
|
||||
require.False(t, ok)
|
||||
require.Equal(t, pol[0], leaf)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -405,7 +583,8 @@ func TestPolicyExamples(t *testing.T) {
|
||||
|
||||
pol, err := FromDagJson(policy)
|
||||
require.NoError(t, err)
|
||||
return pol.Match(data)
|
||||
res, _ := pol.Match(data)
|
||||
return res
|
||||
}
|
||||
|
||||
t.Run("And", func(t *testing.T) {
|
||||
@@ -470,6 +649,7 @@ func FuzzMatch(f *testing.F) {
|
||||
f.Add([]byte(`[["all", ".reviewer", ["like", ".email", "*@example.com"]]]`), []byte(`{"reviewer": [{"email": "alice@example.com"}, {"email": "bob@example.com"}]}`))
|
||||
f.Add([]byte(`[["any", ".tags", ["or", [["==", ".", "news"], ["==", ".", "press"]]]]]`), []byte(`{"tags": ["news", "press"]}`))
|
||||
f.Add([]byte(`[["==", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
|
||||
f.Add([]byte(`[["!=", ".name", "Alice"]]`), []byte(`{"name": "Alice"}`))
|
||||
f.Add([]byte(`[[">", ".age", 30]]`), []byte(`{"age": 31}`))
|
||||
f.Add([]byte(`[["<=", ".height", 180]]`), []byte(`{"height": 170}`))
|
||||
f.Add([]byte(`[["not", ["==", ".status", "inactive"]]]`), []byte(`{"status": "active"}`))
|
||||
@@ -509,7 +689,7 @@ func FuzzMatch(f *testing.F) {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
policy.Match(dataNode)
|
||||
_, _ = policy.Match(dataNode)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -584,7 +764,7 @@ func TestOptionalSelectors(t *testing.T) {
|
||||
err = nb.AssignNode(n)
|
||||
require.NoError(t, err)
|
||||
|
||||
result := tt.policy.Match(nb.Build())
|
||||
result, _ := tt.policy.Match(nb.Build())
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
const (
|
||||
KindEqual = "==" // implemented by equality
|
||||
KindNotEqual = "!=" // implemented by equality
|
||||
KindGreaterThan = ">" // implemented by equality
|
||||
KindGreaterThanOrEqual = ">=" // implemented by equality
|
||||
KindLessThan = "<" // implemented by equality
|
||||
@@ -87,6 +88,13 @@ func Equal(selector string, value ipld.Node) Constructor {
|
||||
}
|
||||
}
|
||||
|
||||
func NotEqual(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
return equality{kind: KindNotEqual, selector: sel, value: value}, err
|
||||
}
|
||||
}
|
||||
|
||||
func GreaterThan(selector string, value ipld.Node) Constructor {
|
||||
return func() (Statement, error) {
|
||||
sel, err := selpkg.Parse(selector)
|
||||
@@ -125,7 +133,7 @@ func (n negation) Kind() string {
|
||||
|
||||
func (n negation) String() string {
|
||||
child := n.statement.String()
|
||||
return fmt.Sprintf(`["%s", "%s"]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
|
||||
return fmt.Sprintf(`["%s", %s]`, n.Kind(), strings.ReplaceAll(child, "\n", "\n "))
|
||||
}
|
||||
|
||||
func Not(cstor Constructor) Constructor {
|
||||
@@ -149,7 +157,7 @@ func (c connective) String() string {
|
||||
for i, statement := range c.statements {
|
||||
childs[i] = strings.ReplaceAll(statement.String(), "\n", "\n ")
|
||||
}
|
||||
return fmt.Sprintf("[\"%s\", [\n %s]]\n", c.kind, strings.Join(childs, ",\n "))
|
||||
return fmt.Sprintf("[\"%s\", [\n %s\n]]", c.kind, strings.Join(childs, ",\n "))
|
||||
}
|
||||
|
||||
func And(cstors ...Constructor) Constructor {
|
||||
@@ -208,7 +216,7 @@ func (n quantifier) Kind() string {
|
||||
|
||||
func (n quantifier) String() string {
|
||||
child := n.statement.String()
|
||||
return fmt.Sprintf("[\"%s\", \"%s\",\n %s]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
|
||||
return fmt.Sprintf("[\"%s\", \"%s\",\n %s\n]", n.Kind(), n.selector, strings.ReplaceAll(child, "\n", "\n "))
|
||||
}
|
||||
|
||||
func All(selector string, cstor Constructor) Constructor {
|
||||
|
||||
@@ -28,12 +28,47 @@ func ExamplePolicy() {
|
||||
// [
|
||||
// ["==", ".status", "draft"],
|
||||
// ["all", ".reviewer",
|
||||
// ["like", ".email", "*@example.com"]],
|
||||
// ["like", ".email", "*@example.com"]
|
||||
// ],
|
||||
// ["any", ".tags",
|
||||
// ["or", [
|
||||
// ["==", ".", "news"],
|
||||
// ["==", ".", "press"]]]
|
||||
// ]
|
||||
// ["==", ".", "press"]
|
||||
// ]]
|
||||
// ]
|
||||
// ]
|
||||
}
|
||||
|
||||
func ExamplePolicy_accumulate() {
|
||||
var statements []policy.Constructor
|
||||
|
||||
statements = append(statements, policy.Equal(".status", literal.String("draft")))
|
||||
|
||||
statements = append(statements, policy.All(".reviewer",
|
||||
policy.Like(".email", "*@example.com"),
|
||||
))
|
||||
|
||||
statements = append(statements, policy.Any(".tags", policy.Or(
|
||||
policy.Equal(".", literal.String("news")),
|
||||
policy.Equal(".", literal.String("press")),
|
||||
)))
|
||||
|
||||
pol := policy.MustConstruct(statements...)
|
||||
|
||||
fmt.Println(pol)
|
||||
|
||||
// Output:
|
||||
// [
|
||||
// ["==", ".status", "draft"],
|
||||
// ["all", ".reviewer",
|
||||
// ["like", ".email", "*@example.com"]
|
||||
// ],
|
||||
// ["any", ".tags",
|
||||
// ["or", [
|
||||
// ["==", ".", "news"],
|
||||
// ["==", ".", "press"]
|
||||
// ]]
|
||||
// ]
|
||||
// ]
|
||||
}
|
||||
|
||||
|
||||
67
pkg/policy/policytest/spec.go
Normal file
67
pkg/policy/policytest/spec.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package policytest
|
||||
|
||||
import (
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
)
|
||||
|
||||
// EmptyPolicy provides a Policy with no statements.
|
||||
var EmptyPolicy = policy.Policy{}
|
||||
|
||||
// SpecPolicy provides a valid Policy containing the statements that are included
|
||||
// in the second code block of the [Validation] section of the delegation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
var SpecPolicy = policy.MustConstruct(
|
||||
policy.Equal(".from", literal.String("alice@example.com")),
|
||||
policy.Any(".to", policy.Like(".", "*@example.com")),
|
||||
)
|
||||
|
||||
// TODO: Replace the URL for [Validation] above when the delegation
|
||||
// specification has been finished/merged.
|
||||
|
||||
// SpecValidArguments provides valid, instantiated Arguments containing
|
||||
// the key/value pairs that are included in portion of the second code block
|
||||
// of the [Validation] section of the delegation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
var SpecValidArguments = args.NewBuilder().
|
||||
Add("from", "alice@example.com").
|
||||
Add("to", []string{
|
||||
"bob@example.com",
|
||||
"carol@not.example.com",
|
||||
}).
|
||||
Add("title", "Coffee").
|
||||
Add("body", "Still on for coffee").
|
||||
MustBuild()
|
||||
|
||||
var specValidArgumentsIPLD = mustIPLD(SpecValidArguments)
|
||||
|
||||
// SpecInvalidArguments provides invalid, instantiated Arguments containing
|
||||
// the key/value pairs that are included in portion of the second code block
|
||||
// of the [Validation] section of the delegation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
var SpecInvalidArguments = args.NewBuilder().
|
||||
Add("from", "alice@example.com").
|
||||
Add("to", []string{
|
||||
"bob@null.com",
|
||||
"carol@elsewhere.example.com",
|
||||
}).
|
||||
Add("title", "Coffee").
|
||||
Add("body", "Still on for coffee").
|
||||
MustBuild()
|
||||
|
||||
var specInvalidArgumentsIPLD = mustIPLD(SpecInvalidArguments)
|
||||
|
||||
func mustIPLD(args *args.Args) ipld.Node {
|
||||
node, err := args.ToIPLD()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
32
pkg/policy/policytest/spec_test.go
Normal file
32
pkg/policy/policytest/spec_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package policytest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestInvocationValidation applies the example policy to the second
|
||||
// example arguments as defined in the [Validation] section of the
|
||||
// invocation specification.
|
||||
//
|
||||
// [Validation]: https://github.com/ucan-wg/delegation/tree/v1_ipld#validation
|
||||
func TestInvocationValidationSpecExamples(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("with passing args", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
exec, stmt := SpecPolicy.Match(specValidArgumentsIPLD)
|
||||
assert.True(t, exec)
|
||||
assert.Nil(t, stmt)
|
||||
})
|
||||
|
||||
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
exec, stmt := SpecPolicy.Match(specInvalidArgumentsIPLD)
|
||||
assert.False(t, exec)
|
||||
assert.NotNil(t, stmt)
|
||||
})
|
||||
}
|
||||
@@ -6,12 +6,24 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
var (
|
||||
indexRegex = regexp.MustCompile(`^-?\d+$`)
|
||||
sliceRegex = regexp.MustCompile(`^((\-?\d+:\-?\d*)|(\-?\d*:\-?\d+))$`)
|
||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_]*?$`)
|
||||
|
||||
// Field name requirements:
|
||||
// - Must start with ASCII letter, Unicode letter, or underscore
|
||||
// - Can contain:
|
||||
// - ASCII letters (a-z, A-Z)
|
||||
// - ASCII digits (0-9)
|
||||
// - Unicode letters (\p{L})
|
||||
// - Dollar sign ($)
|
||||
// - Underscore (_)
|
||||
// - Hyphen (-)
|
||||
fieldRegex = regexp.MustCompile(`^\.[a-zA-Z_\p{L}][a-zA-Z0-9$_\p{L}\-]*$`)
|
||||
)
|
||||
|
||||
func Parse(str string) (Selector, error) {
|
||||
@@ -56,6 +68,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid index", str, col, tok)
|
||||
}
|
||||
if int64(idx) > limits.MaxInt53 || int64(idx) < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("index %d exceeds safe integer bounds", idx), str, col, tok)
|
||||
}
|
||||
sel = append(sel, segment{str: tok, optional: opt, index: idx})
|
||||
|
||||
// explicit field, ["abcd"]
|
||||
@@ -77,6 +92,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid slice index", str, col, tok)
|
||||
}
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||
}
|
||||
rng[0] = i
|
||||
}
|
||||
if splt[1] == "" {
|
||||
@@ -86,6 +104,9 @@ func Parse(str string) (Selector, error) {
|
||||
if err != nil {
|
||||
return nil, newParseError("invalid slice index", str, col, tok)
|
||||
}
|
||||
if i > limits.MaxInt53 || i < limits.MinInt53 {
|
||||
return nil, newParseError(fmt.Sprintf("slice index %d exceeds safe integer bounds", i), str, col, tok)
|
||||
}
|
||||
rng[1] = i
|
||||
}
|
||||
sel = append(sel, segment{str: tok, optional: opt, slice: rng[:]})
|
||||
@@ -152,37 +173,37 @@ func tokenize(str string) []string {
|
||||
return toks
|
||||
}
|
||||
|
||||
type parseerr struct {
|
||||
type parseErr struct {
|
||||
msg string
|
||||
src string
|
||||
col int
|
||||
tok string
|
||||
}
|
||||
|
||||
func (p parseerr) Name() string {
|
||||
func (p parseErr) Name() string {
|
||||
return "ParseError"
|
||||
}
|
||||
|
||||
func (p parseerr) Message() string {
|
||||
func (p parseErr) Message() string {
|
||||
return p.msg
|
||||
}
|
||||
|
||||
func (p parseerr) Column() int {
|
||||
func (p parseErr) Column() int {
|
||||
return p.col
|
||||
}
|
||||
|
||||
func (p parseerr) Error() string {
|
||||
func (p parseErr) Error() string {
|
||||
return p.msg
|
||||
}
|
||||
|
||||
func (p parseerr) Source() string {
|
||||
func (p parseErr) Source() string {
|
||||
return p.src
|
||||
}
|
||||
|
||||
func (p parseerr) Token() string {
|
||||
func (p parseErr) Token() string {
|
||||
return p.tok
|
||||
}
|
||||
|
||||
func newParseError(message string, source string, column int, token string) error {
|
||||
return parseerr{message, source, column, token}
|
||||
return parseErr{message, source, column, token}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
@@ -31,6 +33,29 @@ func TestParse(t *testing.T) {
|
||||
require.Empty(t, sel[0].Slice())
|
||||
require.Equal(t, sel[0].Field(), "foo")
|
||||
require.Empty(t, sel[0].Index())
|
||||
|
||||
sel, err = Parse(".foo_bar")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sel))
|
||||
require.False(t, sel[0].Identity())
|
||||
require.False(t, sel[0].Optional())
|
||||
require.False(t, sel[0].Iterator())
|
||||
require.Empty(t, sel[0].Slice())
|
||||
require.Equal(t, sel[0].Field(), "foo_bar")
|
||||
require.Empty(t, sel[0].Index())
|
||||
|
||||
sel, err = Parse(".foo-bar")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(sel))
|
||||
require.False(t, sel[0].Identity())
|
||||
require.False(t, sel[0].Optional())
|
||||
require.False(t, sel[0].Iterator())
|
||||
require.Empty(t, sel[0].Slice())
|
||||
require.Equal(t, sel[0].Field(), "foo-bar")
|
||||
require.Empty(t, sel[0].Index())
|
||||
|
||||
sel, err = Parse(".foo*bar")
|
||||
require.ErrorContains(t, err, "invalid segment")
|
||||
})
|
||||
|
||||
t.Run("explicit field", func(t *testing.T) {
|
||||
@@ -354,7 +379,6 @@ func TestParse(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())
|
||||
@@ -404,13 +428,11 @@ func TestParse(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("slice with negative start and positive end", func(t *testing.T) {
|
||||
@@ -553,10 +575,67 @@ func TestParse(t *testing.T) {
|
||||
_, err := Parse(".[foo]")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func printSegments(s Selector) {
|
||||
for i, seg := range s {
|
||||
fmt.Printf("%d: %s\n", i, seg.String())
|
||||
}
|
||||
t.Run("extended field names", func(t *testing.T) {
|
||||
validFields := []string{
|
||||
".basic",
|
||||
".user_name",
|
||||
".user-name",
|
||||
".userName$special",
|
||||
".αβγ", // Greek letters
|
||||
".użytkownik", // Polish characters
|
||||
".用户", // Chinese characters
|
||||
".사용자", // Korean characters
|
||||
"._private",
|
||||
".number123",
|
||||
".camelCase",
|
||||
".snake_case",
|
||||
".kebab-case",
|
||||
".mixed_kebab-case",
|
||||
".with$dollar",
|
||||
".MIXED_Case_123",
|
||||
".unicodeø",
|
||||
}
|
||||
|
||||
for _, field := range validFields {
|
||||
sel, err := Parse(field)
|
||||
require.NoError(t, err, "field: %s", field)
|
||||
require.NotNil(t, sel)
|
||||
}
|
||||
|
||||
invalidFields := []string{
|
||||
".123number", // Can't start with digit
|
||||
".@special", // @ not allowed
|
||||
".space name", // No spaces
|
||||
".#hashtag", // No #
|
||||
".name!", // No !
|
||||
".{brackets}", // No brackets
|
||||
".name/with/slashes", // No slashes
|
||||
}
|
||||
|
||||
for _, field := range invalidFields {
|
||||
sel, err := Parse(field)
|
||||
require.Error(t, err, "field: %s", field)
|
||||
require.Nil(t, sel)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integer overflow", func(t *testing.T) {
|
||||
sel, err := Parse(fmt.Sprintf(".[%d]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
sel, err = Parse(fmt.Sprintf(".[%d]", limits.MinInt53-1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
// Test slice overflow
|
||||
sel, err = Parse(fmt.Sprintf(".[%d:42]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
|
||||
sel, err = Parse(fmt.Sprintf(".[1:%d]", limits.MaxInt53+1))
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sel)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type Selector []segment
|
||||
// Select perform the selection described by the selector on the input IPLD DAG.
|
||||
// Select can return:
|
||||
// - exactly one matched IPLD node
|
||||
// - a resolutionerr error if not being able to resolve to a node
|
||||
// - 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)
|
||||
@@ -266,19 +266,32 @@ func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||
case slice[0] == math.MinInt:
|
||||
start = 0
|
||||
case slice[0] < 0:
|
||||
start = length + slice[0]
|
||||
// Check for potential overflow before adding
|
||||
if -slice[0] > length {
|
||||
start = 0
|
||||
} else {
|
||||
start = length + slice[0]
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case slice[1] == math.MaxInt:
|
||||
end = length
|
||||
case slice[1] < 0:
|
||||
end = length + slice[1]
|
||||
// Check for potential overflow before adding
|
||||
if -slice[1] > length {
|
||||
end = 0
|
||||
} else {
|
||||
end = length + slice[1]
|
||||
}
|
||||
}
|
||||
|
||||
// backward iteration is not allowed, shortcut to an empty result
|
||||
if start >= end {
|
||||
start, end = 0, 0
|
||||
return
|
||||
}
|
||||
|
||||
// clamp out of bound
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -286,11 +299,14 @@ func resolveSliceIndices(slice []int64, length int64) (start int64, end int64) {
|
||||
if start > length {
|
||||
start = length
|
||||
}
|
||||
if end < 0 {
|
||||
end = 0
|
||||
}
|
||||
if end > length {
|
||||
end = length
|
||||
}
|
||||
|
||||
return start, end
|
||||
return
|
||||
}
|
||||
|
||||
func kindString(n datamodel.Node) string {
|
||||
@@ -300,27 +316,27 @@ func kindString(n datamodel.Node) string {
|
||||
return n.Kind().String()
|
||||
}
|
||||
|
||||
type resolutionerr struct {
|
||||
type resolutionErr struct {
|
||||
msg string
|
||||
at []string
|
||||
}
|
||||
|
||||
func (r resolutionerr) Name() string {
|
||||
func (r resolutionErr) Name() string {
|
||||
return "ResolutionError"
|
||||
}
|
||||
|
||||
func (r resolutionerr) Message() string {
|
||||
func (r resolutionErr) Message() string {
|
||||
return fmt.Sprintf("can not resolve path: .%s", strings.Join(r.at, "."))
|
||||
}
|
||||
|
||||
func (r resolutionerr) At() []string {
|
||||
func (r resolutionErr) At() []string {
|
||||
return r.at
|
||||
}
|
||||
|
||||
func (r resolutionerr) Error() string {
|
||||
func (r resolutionErr) Error() string {
|
||||
return r.Message()
|
||||
}
|
||||
|
||||
func newResolutionError(message string, at []string) error {
|
||||
return resolutionerr{message, at}
|
||||
return resolutionErr{message, at}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package selector
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/ipld/go-ipld-prime/must"
|
||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||
"github.com/ipld/go-ipld-prime/printer"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -87,8 +86,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
age := must.Int(must.Node(res.LookupByString("age")))
|
||||
require.Equal(t, int64(alice.Age), age)
|
||||
})
|
||||
@@ -101,8 +98,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
name := must.String(res)
|
||||
require.Equal(t, alice.Name.First, name)
|
||||
|
||||
@@ -110,8 +105,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
name = must.String(res)
|
||||
require.Equal(t, bob.Name.First, name)
|
||||
})
|
||||
@@ -124,8 +117,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
name := must.String(res)
|
||||
require.Equal(t, *alice.Name.Middle, name)
|
||||
|
||||
@@ -142,9 +133,7 @@ func TestSelect(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
require.Empty(t, res)
|
||||
|
||||
fmt.Println(err)
|
||||
|
||||
require.ErrorAs(t, err, &resolutionerr{}, "error should be a resolution error")
|
||||
require.ErrorAs(t, err, &resolutionErr{}, "error should be a resolution error")
|
||||
})
|
||||
|
||||
t.Run("optional not exists", func(t *testing.T) {
|
||||
@@ -164,8 +153,6 @@ func TestSelect(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, res)
|
||||
|
||||
fmt.Println(printer.Sprint(res))
|
||||
|
||||
iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
|
||||
require.Equal(t, alice.Interests[0].Name, iname)
|
||||
|
||||
@@ -364,9 +351,63 @@ func FuzzParseAndSelect(f *testing.F) {
|
||||
|
||||
// look for panic()
|
||||
_, err = sel.Select(node)
|
||||
if err != nil && !errors.As(err, &resolutionerr{}) {
|
||||
if err != nil && !errors.As(err, &resolutionErr{}) {
|
||||
// not normal, we should only have resolution errors
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResolveSliceIndices(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
slice []int64
|
||||
length int64
|
||||
wantStart int64
|
||||
wantEnd int64
|
||||
}{
|
||||
{
|
||||
name: "normal case",
|
||||
slice: []int64{1, 3},
|
||||
length: 5,
|
||||
wantStart: 1,
|
||||
wantEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "negative indices",
|
||||
slice: []int64{-2, -1},
|
||||
length: 5,
|
||||
wantStart: 3,
|
||||
wantEnd: 4,
|
||||
},
|
||||
{
|
||||
name: "overflow protection negative start",
|
||||
slice: []int64{math.MinInt64, 3},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 3,
|
||||
},
|
||||
{
|
||||
name: "overflow protection negative end",
|
||||
slice: []int64{0, math.MinInt64},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 0,
|
||||
},
|
||||
{
|
||||
name: "max bounds",
|
||||
slice: []int64{0, math.MaxInt64},
|
||||
length: 5,
|
||||
wantStart: 0,
|
||||
wantEnd: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
start, end := resolveSliceIndices(tt.slice, tt.length)
|
||||
require.Equal(t, tt.wantStart, start)
|
||||
require.Equal(t, tt.wantEnd, end)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package selector_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/ipld/go-ipld-prime/datamodel"
|
||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||
@@ -55,7 +52,7 @@ func TestSupportedForms(t *testing.T) {
|
||||
require.NotNil(t, res)
|
||||
|
||||
exp := makeNode(t, tc.Output)
|
||||
equalIPLD(t, exp, res)
|
||||
require.True(t, ipld.DeepEqual(exp, res))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,23 +103,6 @@ func TestSupportedForms(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
t.Helper()
|
||||
|
||||
|
||||
90
pkg/secretbox/secretbox.go
Normal file
90
pkg/secretbox/secretbox.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package secretbox
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
const keySize = 32 // secretbox allows only 32-byte keys
|
||||
|
||||
var ErrShortCipherText = errors.New("ciphertext too short")
|
||||
var ErrNoEncryptionKey = errors.New("encryption key is required")
|
||||
var ErrInvalidKeySize = errors.New("invalid key size: must be 32 bytes")
|
||||
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
|
||||
|
||||
// GenerateKey generates a random 32-byte key to be used by EncryptWithKey and DecryptWithKey
|
||||
func GenerateKey() ([]byte, error) {
|
||||
key := make([]byte, keySize)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate key: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// EncryptWithKey encrypts data using NaCl's secretbox with the provided key.
|
||||
// 40 bytes of overhead (24-byte nonce + 16-byte MAC) are added to the plaintext size.
|
||||
func EncryptWithKey(data, key []byte) ([]byte, error) {
|
||||
if err := validateKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secretKey [keySize]byte
|
||||
copy(secretKey[:], key)
|
||||
|
||||
// Generate 24 bytes of random data as nonce
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encrypt and authenticate data
|
||||
encrypted := secretbox.Seal(nonce[:], data, &nonce, &secretKey)
|
||||
return encrypted, nil
|
||||
}
|
||||
|
||||
// DecryptStringWithKey decrypts data using secretbox with the provided key
|
||||
func DecryptStringWithKey(data, key []byte) ([]byte, error) {
|
||||
if err := validateKey(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(data) < 24 {
|
||||
return nil, ErrShortCipherText
|
||||
}
|
||||
|
||||
var secretKey [keySize]byte
|
||||
copy(secretKey[:], key)
|
||||
|
||||
var nonce [24]byte
|
||||
copy(nonce[:], data[:24])
|
||||
|
||||
decrypted, ok := secretbox.Open(nil, data[24:], &nonce, &secretKey)
|
||||
if !ok {
|
||||
return nil, errors.New("decryption failed")
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
func validateKey(key []byte) error {
|
||||
if key == nil {
|
||||
return ErrNoEncryptionKey
|
||||
}
|
||||
|
||||
if len(key) != keySize {
|
||||
return ErrInvalidKeySize
|
||||
}
|
||||
|
||||
// check if key is all zeros
|
||||
for _, b := range key {
|
||||
if b != 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrZeroKey
|
||||
}
|
||||
144
pkg/secretbox/secretbox_test.go
Normal file
144
pkg/secretbox/secretbox_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package secretbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSecretBoxEncryption(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, keySize) // generate random 32-byte key
|
||||
_, errKey := rand.Read(key)
|
||||
require.NoError(t, errKey)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
key []byte
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "valid encryption/decryption",
|
||||
data: []byte("hello world"),
|
||||
key: key,
|
||||
},
|
||||
{
|
||||
name: "nil key returns error",
|
||||
data: []byte("hello world"),
|
||||
key: nil,
|
||||
wantErr: ErrNoEncryptionKey,
|
||||
},
|
||||
{
|
||||
name: "empty data",
|
||||
data: []byte{},
|
||||
key: key,
|
||||
},
|
||||
{
|
||||
name: "invalid key size",
|
||||
data: []byte("hello world"),
|
||||
key: make([]byte, 16), // Only 32 bytes allowed now
|
||||
wantErr: ErrInvalidKeySize,
|
||||
},
|
||||
{
|
||||
name: "zero key returns error",
|
||||
data: []byte("hello world"),
|
||||
key: make([]byte, keySize),
|
||||
wantErr: ErrZeroKey,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
encrypted, err := EncryptWithKey(tt.data, tt.key)
|
||||
if tt.wantErr != nil {
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify encrypted data is different and includes nonce
|
||||
require.Greater(t, len(encrypted), 24) // At least nonce size
|
||||
if len(tt.data) > 0 {
|
||||
require.NotEqual(t, tt.data, encrypted[24:]) // Ignore nonce prefix
|
||||
}
|
||||
|
||||
decrypted, err := DecryptStringWithKey(encrypted, tt.key)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(tt.data, decrypted))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptionErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key := make([]byte, keySize)
|
||||
_, err := rand.Read(key)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create valid encrypted data for tampering tests
|
||||
validData := []byte("test message")
|
||||
encrypted, err := EncryptWithKey(validData, key)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
key []byte
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "short ciphertext",
|
||||
data: make([]byte, 23), // Less than nonce size
|
||||
key: key,
|
||||
errMsg: "ciphertext too short",
|
||||
},
|
||||
{
|
||||
name: "invalid ciphertext",
|
||||
data: make([]byte, 24), // Just nonce size
|
||||
key: key,
|
||||
errMsg: "decryption failed",
|
||||
},
|
||||
{
|
||||
name: "tampered ciphertext",
|
||||
data: tamperWithBytes(encrypted),
|
||||
key: key,
|
||||
errMsg: "decryption failed",
|
||||
},
|
||||
{
|
||||
name: "missing key",
|
||||
data: encrypted,
|
||||
key: nil,
|
||||
errMsg: "encryption key is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := DecryptStringWithKey(tt.data, tt.key)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.errMsg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// tamperWithBytes modifies a byte in the encrypted data to simulate tampering
|
||||
func tamperWithBytes(data []byte) []byte {
|
||||
if len(data) < 25 { // Need at least nonce + 1 byte
|
||||
return data
|
||||
}
|
||||
tampered := make([]byte, len(data))
|
||||
copy(tampered, data)
|
||||
tampered[24] ^= 0x01 // Modify first byte after nonce
|
||||
return tampered
|
||||
}
|
||||
@@ -10,17 +10,18 @@ package delegation
|
||||
// TODO: change the "delegation" link above when the specification is merged
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"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.
|
||||
@@ -45,21 +46,15 @@ type Token struct {
|
||||
expiration *time.Time
|
||||
}
|
||||
|
||||
// New creates a validated Token from the provided parameters and options.
|
||||
// New creates a validated delegation Token from the provided parameters and options.
|
||||
// This is typically used to delegate a given power to another agent.
|
||||
//
|
||||
// When creating a delegated token, the Issuer's (iss) DID is assembed
|
||||
// using the public key associated with the private key sent as the first
|
||||
// parameter.
|
||||
func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
iss, err := did.FromPrivKey(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on (subject)".
|
||||
func New(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, sub did.DID, opts ...Option) (*Token, error) {
|
||||
tkn := &Token{
|
||||
issuer: iss,
|
||||
audience: aud,
|
||||
subject: did.Undef,
|
||||
subject: sub,
|
||||
command: cmd,
|
||||
policy: pol,
|
||||
meta: meta.NewMeta(),
|
||||
@@ -72,8 +67,9 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(tkn.nonce) == 0 {
|
||||
tkn.nonce, err = generateNonce()
|
||||
tkn.nonce, err = nonce.Generate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -86,21 +82,27 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
||||
return tkn, nil
|
||||
}
|
||||
|
||||
// Root creates a validated UCAN delegation Token from the provided
|
||||
// parameters and options.
|
||||
// Root creates a validated UCAN delegation Token from the provided parameters and options.
|
||||
// This is typically used to create and give power to an agent.
|
||||
//
|
||||
// When creating a root token, both the Issuer's (iss) and Subject's
|
||||
// (sub) DIDs are assembled from the public key associated with the
|
||||
// private key passed as the first argument.
|
||||
func Root(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
sub, err := did.FromPrivKey(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on itself".
|
||||
func Root(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
return New(iss, aud, cmd, pol, iss, opts...)
|
||||
}
|
||||
|
||||
opts = append(opts, WithSubject(sub))
|
||||
|
||||
return New(privKey, aud, cmd, pol, opts...)
|
||||
// Powerline creates a validated UCAN delegation Token from the provided parameters and options.
|
||||
//
|
||||
// Powerline is a pattern for automatically delegating all future delegations to another agent regardless of Subject.
|
||||
// This is a very powerful pattern, use it only if you understand it.
|
||||
// Powerline delegations MUST NOT be used as the root delegation to a resource
|
||||
//
|
||||
// A very common use case for Powerline is providing a stable DID across multiple agents (e.g. representing a user with
|
||||
// multiple devices). This enables the automatic sharing of authority across their devices without needing to share keys
|
||||
// or set up a threshold scheme. It is also flexible, since a Powerline delegation MAY be revoked.
|
||||
//
|
||||
// You can read it as "(issuer) allows (audience) to perform (cmd+pol) on anything".
|
||||
func Powerline(iss did.DID, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||
return New(iss, aud, cmd, pol, did.Undef, opts...)
|
||||
}
|
||||
|
||||
// Issuer returns the did.DID representing the Token's issuer.
|
||||
@@ -152,6 +154,60 @@ func (t *Token) Expiration() *time.Time {
|
||||
return t.expiration
|
||||
}
|
||||
|
||||
// IsRoot tells if the token is a root delegation.
|
||||
func (t *Token) IsRoot() bool {
|
||||
return t.issuer == t.subject
|
||||
}
|
||||
|
||||
// IsPowerline tells if the token is a powerline delegation.
|
||||
func (t *Token) IsPowerline() bool {
|
||||
return t.subject == did.Undef
|
||||
}
|
||||
|
||||
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
||||
// This does NOT do any other kind of verifications.
|
||||
func (t *Token) IsValidNow() bool {
|
||||
return t.IsValidAt(time.Now())
|
||||
}
|
||||
|
||||
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
||||
// This does NOT do any other kind of verifications.
|
||||
func (t *Token) IsValidAt(ti time.Time) bool {
|
||||
if t.expiration != nil && ti.After(*t.expiration) {
|
||||
return false
|
||||
}
|
||||
if t.notBefore != nil && ti.Before(*t.notBefore) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Token) String() string {
|
||||
var res strings.Builder
|
||||
|
||||
var kind string
|
||||
switch {
|
||||
case t.issuer == t.subject:
|
||||
kind = " (root delegation)"
|
||||
case t.subject == did.Undef:
|
||||
kind = " (powerline delegation)"
|
||||
default:
|
||||
kind = " (normal delegation)"
|
||||
}
|
||||
|
||||
res.WriteString(fmt.Sprintf("Issuer: %s\n", t.Issuer()))
|
||||
res.WriteString(fmt.Sprintf("Audience: %s\n", t.Audience()))
|
||||
res.WriteString(fmt.Sprintf("Subject: %s%s\n", t.Subject(), kind))
|
||||
res.WriteString(fmt.Sprintf("Command: %s\n", t.Command()))
|
||||
res.WriteString(fmt.Sprintf("Policy: %s\n", t.Policy()))
|
||||
res.WriteString(fmt.Sprintf("Nonce: %s\n", base64.StdEncoding.EncodeToString(t.Nonce())))
|
||||
res.WriteString(fmt.Sprintf("Meta: %s\n", t.Meta()))
|
||||
res.WriteString(fmt.Sprintf("NotBefore: %v\n", t.NotBefore()))
|
||||
res.WriteString(fmt.Sprintf("Expiration: %v", t.Expiration()))
|
||||
|
||||
return res.String()
|
||||
}
|
||||
|
||||
func (t *Token) validate() error {
|
||||
var errs error
|
||||
|
||||
@@ -184,27 +240,19 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
return nil, fmt.Errorf("parse iss: %w", err)
|
||||
}
|
||||
|
||||
tkn.audience, err = did.Parse(m.Aud)
|
||||
if err != nil {
|
||||
if tkn.audience, err = did.Parse(m.Aud); err != nil {
|
||||
return nil, fmt.Errorf("parse audience: %w", err)
|
||||
}
|
||||
|
||||
if m.Sub != nil {
|
||||
tkn.subject, err = did.Parse(*m.Sub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse subject: %w", err)
|
||||
}
|
||||
} else {
|
||||
tkn.subject = did.Undef
|
||||
if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
|
||||
return nil, fmt.Errorf("parse subject: %w", err)
|
||||
}
|
||||
|
||||
tkn.command, err = command.Parse(m.Cmd)
|
||||
if err != nil {
|
||||
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
||||
return nil, fmt.Errorf("parse command: %w", err)
|
||||
}
|
||||
|
||||
tkn.policy, err = policy.FromIPLD(m.Pol)
|
||||
if err != nil {
|
||||
if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
|
||||
return nil, fmt.Errorf("parse policy: %w", err)
|
||||
}
|
||||
|
||||
@@ -214,15 +262,18 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
tkn.nonce = m.Nonce
|
||||
|
||||
tkn.meta = m.Meta
|
||||
|
||||
if m.Nbf != nil {
|
||||
t := time.Unix(*m.Nbf, 0)
|
||||
tkn.notBefore = &t
|
||||
if tkn.meta == nil {
|
||||
tkn.meta = meta.NewMeta()
|
||||
}
|
||||
|
||||
if m.Exp != nil {
|
||||
t := time.Unix(*m.Exp, 0)
|
||||
tkn.expiration = &t
|
||||
tkn.notBefore, err = parse.OptionalTimestamp(m.Nbf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse notBefore: %w", err)
|
||||
}
|
||||
|
||||
tkn.expiration, err = parse.OptionalTimestamp(m.Exp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse expiration: %w", err)
|
||||
}
|
||||
|
||||
if err := tkn.validate(); err != nil {
|
||||
@@ -231,14 +282,3 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||
|
||||
return &tkn, nil
|
||||
}
|
||||
|
||||
// generateNonce creates a 12-byte random nonce.
|
||||
// TODO: some crypto scheme require more, is that our case?
|
||||
func generateNonce() ([]byte, error) {
|
||||
res := make([]byte, 12)
|
||||
_, err := rand.Read(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package delegation_test
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/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"
|
||||
@@ -17,67 +17,28 @@ import (
|
||||
const (
|
||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||
|
||||
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
||||
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
|
||||
|
||||
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
|
||||
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
||||
|
||||
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
|
||||
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||
subJectCmd = "/foo/bar"
|
||||
subjectPol = `
|
||||
subJectCmd = "/foo/bar"
|
||||
subjectPol = `
|
||||
[
|
||||
[
|
||||
"==",
|
||||
".status",
|
||||
"draft"
|
||||
],
|
||||
[
|
||||
"all",
|
||||
".reviewer",
|
||||
[
|
||||
"like",
|
||||
".email",
|
||||
"*@example.com"
|
||||
]
|
||||
],
|
||||
[
|
||||
"any",
|
||||
".tags",
|
||||
[
|
||||
"or",
|
||||
[
|
||||
[
|
||||
"==",
|
||||
".",
|
||||
"news"
|
||||
],
|
||||
[
|
||||
"==",
|
||||
".",
|
||||
"press"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
["==", ".status", "draft"],
|
||||
["all", ".reviewer",
|
||||
["like", ".email", "*@example.com"]
|
||||
],
|
||||
["any", ".tags",
|
||||
["or", [
|
||||
["==", ".", "news"],
|
||||
["==", ".", "press"]
|
||||
]]
|
||||
]
|
||||
]
|
||||
`
|
||||
|
||||
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
||||
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
||||
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
|
||||
)
|
||||
|
||||
func TestConstructors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
privKey := privKey(t, issuerPrivKeyCfg)
|
||||
|
||||
aud, err := did.Parse(AudienceDID)
|
||||
|
||||
sub, err := did.Parse(subjectDID)
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd, err := command.Parse(subJectCmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -88,19 +49,19 @@ func TestConstructors(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
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, didtest.PersonaCarol.DID(),
|
||||
delegation.WithNonce([]byte(nonce)),
|
||||
delegation.WithSubject(sub),
|
||||
delegation.WithExpiration(exp),
|
||||
delegation.WithMeta("foo", "fooo"),
|
||||
delegation.WithMeta("bar", "barr"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := tkn.ToDagJson(privKey)
|
||||
require.NoError(t, err)
|
||||
require.False(t, tkn.IsRoot())
|
||||
require.False(t, tkn.IsPowerline())
|
||||
|
||||
t.Log(string(data))
|
||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
golden.Assert(t, string(data), "new.dagjson")
|
||||
})
|
||||
@@ -108,7 +69,7 @@ func TestConstructors(t *testing.T) {
|
||||
t.Run("Root", func(t *testing.T) {
|
||||
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.WithExpiration(exp),
|
||||
delegation.WithMeta("foo", "fooo"),
|
||||
@@ -116,21 +77,132 @@ func TestConstructors(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := tkn.ToDagJson(privKey)
|
||||
require.NoError(t, err)
|
||||
require.True(t, tkn.IsRoot())
|
||||
require.False(t, tkn.IsPowerline())
|
||||
|
||||
t.Log(string(data))
|
||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
golden.Assert(t, string(data), "root.dagjson")
|
||||
})
|
||||
|
||||
t.Run("Powerline", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tkn, err := delegation.Powerline(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||
delegation.WithNonce([]byte(nonce)),
|
||||
delegation.WithExpiration(exp),
|
||||
delegation.WithMeta("foo", "fooo"),
|
||||
delegation.WithMeta("bar", "barr"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.False(t, tkn.IsRoot())
|
||||
require.True(t, tkn.IsPowerline())
|
||||
|
||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
golden.Assert(t, string(data), "powerline.dagjson")
|
||||
})
|
||||
}
|
||||
|
||||
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||
func TestEncryptedMeta(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cmd, err := command.Parse(subJectCmd)
|
||||
require.NoError(t, err)
|
||||
pol, err := policy.FromDagJson(subjectPol)
|
||||
require.NoError(t, err)
|
||||
|
||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||
encryptionKey, err := base64.StdEncoding.DecodeString(aesKey)
|
||||
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.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
decodedTkn, _, err := delegation.FromSealed(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = decodedTkn.Meta().GetString(tt.key)
|
||||
require.Error(t, err)
|
||||
|
||||
decrypted, err := decodedTkn.Meta().GetEncryptedString(tt.key, encryptionKey)
|
||||
require.NoError(t, err)
|
||||
// Verify the decrypted value is equal to the original
|
||||
require.Equal(t, tt.value, decrypted)
|
||||
|
||||
// Try to decrypt with wrong key
|
||||
wrongKey := make([]byte, 32)
|
||||
_, err = decodedTkn.Meta().GetEncryptedString(tt.key, wrongKey)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("multiple encrypted values in the same token", func(t *testing.T) {
|
||||
values := map[string]string{
|
||||
"secret1": "value1",
|
||||
"secret2": "value2",
|
||||
"secret3": "value3",
|
||||
}
|
||||
var opts []delegation.Option
|
||||
for k, v := range values {
|
||||
opts = append(opts, delegation.WithEncryptedMetaString(k, v, encryptionKey))
|
||||
}
|
||||
|
||||
// Create token with multiple encrypted values
|
||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
decodedTkn, _, err := delegation.FromSealed(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
for k, v := range values {
|
||||
decrypted, err := decodedTkn.Meta().GetEncryptedString(k, encryptionKey)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, v, decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
5
token/delegation/delegationtest/README.md
Normal file
5
token/delegation/delegationtest/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# delegationtest
|
||||
|
||||
See the package documentation for instructions on how to use the generated
|
||||
tokens as well as information on how to regenerate the code if changes have
|
||||
been made.
|
||||
BIN
token/delegation/delegationtest/data/TokenAliceBob.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenAliceBob.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenBobCarol.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenBobCarol.dagcbor
Normal file
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenCarolDan.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenCarolDan.dagcbor
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenDanErin.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenDanErin.dagcbor
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
token/delegation/delegationtest/data/TokenErinFrank.dagcbor
Normal file
BIN
token/delegation/delegationtest/data/TokenErinFrank.dagcbor
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
33
token/delegation/delegationtest/doc.go
Normal file
33
token/delegation/delegationtest/doc.go
Normal 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
|
||||
275
token/delegation/delegationtest/generator/generator.go
Normal file
275
token/delegation/delegationtest/generator/generator.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/policytest"
|
||||
"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 // iss
|
||||
aud did.DID
|
||||
cmd command.Command
|
||||
pol policy.Policy
|
||||
sub did.DID
|
||||
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: policytest.EmptyPolicy,
|
||||
sub: didtest.PersonaAlice.DID(),
|
||||
opts: []delegation.Option{
|
||||
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.sub = 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))
|
||||
}},
|
||||
{name: "ValidExamplePolicy", variant: func(p *newDelegationParams) {
|
||||
p.pol = policytest.SpecPolicy
|
||||
}},
|
||||
}
|
||||
|
||||
// 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(¶ms)
|
||||
|
||||
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.sub, 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 {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
|
||||
Println := func(a ...any) { _, _ = fmt.Fprintln(buf, a...) }
|
||||
Printf := func(format string, a ...any) { _, _ = fmt.Fprintf(buf, format, a...) }
|
||||
|
||||
Println("// Code generated by delegationtest - DO NOT EDIT.")
|
||||
Println()
|
||||
Println("package delegationtest")
|
||||
Println()
|
||||
Println("import (")
|
||||
Println("\t\"github.com/ipfs/go-cid\"")
|
||||
Println()
|
||||
Println("\t\"github.com/ucan-wg/go-ucan/token/delegation\"")
|
||||
Println(")")
|
||||
|
||||
refs := make(map[cid.Cid]string, len(g.dlgs))
|
||||
|
||||
for _, d := range g.dlgs {
|
||||
refs[d.id] = d.name + "CID"
|
||||
|
||||
Println()
|
||||
Println("var (")
|
||||
Printf("\t%sCID = cid.MustParse(\"%s\")\n", d.name, d.id.String())
|
||||
Printf("\t%sSealed = mustGetBundle(%s).Sealed\n", d.name, d.name+"CID")
|
||||
Printf("\t%sBundle = mustGetBundle(%s)\n", d.name, d.name+"CID")
|
||||
Printf("\t%s = mustGetBundle(%s).Decoded\n", d.name, d.name+"CID")
|
||||
Println(")")
|
||||
}
|
||||
|
||||
Println()
|
||||
Println("var AllTokens = []*delegation.Token{")
|
||||
for _, d := range g.dlgs {
|
||||
Printf("\t%s,\n", d.name)
|
||||
}
|
||||
Println("}")
|
||||
|
||||
Println()
|
||||
Println("var AllBundles = []delegation.Bundle{")
|
||||
for _, d := range g.dlgs {
|
||||
Printf("\t%sBundle,\n", d.name)
|
||||
}
|
||||
Println("}")
|
||||
|
||||
Println()
|
||||
Println("var cidToName = map[cid.Cid]string{")
|
||||
for _, d := range g.dlgs {
|
||||
Printf("\t%sCID: \"%s\",\n", d.name, d.name)
|
||||
}
|
||||
Println("}")
|
||||
|
||||
for _, c := range g.chains {
|
||||
Println()
|
||||
Printf("var %s = []cid.Cid{\n", c.name)
|
||||
|
||||
for _, d := range slices.Backward(c.prf) {
|
||||
Printf("\t%s,\n", refs[d])
|
||||
}
|
||||
|
||||
Println("}")
|
||||
}
|
||||
|
||||
out, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile("../token_gen.go", out, 0666)
|
||||
}
|
||||
17
token/delegation/delegationtest/generator/main.go
Normal file
17
token/delegation/delegationtest/generator/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
115
token/delegation/delegationtest/token.go
Normal file
115
token/delegation/delegationtest/token.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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 {
|
||||
bundles map[cid.Cid]delegation.Bundle
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ldr *DelegationLoader
|
||||
)
|
||||
|
||||
// GetDelegationLoader returns a singleton instance of a test
|
||||
// DelegationLoader containing all the tokens present in the data/
|
||||
// directory.
|
||||
func GetDelegationLoader() *DelegationLoader {
|
||||
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) {
|
||||
bundle, ok := l.bundles[id]
|
||||
if !ok {
|
||||
return nil, delegation.ErrDelegationNotFound
|
||||
}
|
||||
return bundle.Decoded, nil
|
||||
}
|
||||
|
||||
func loadDelegations() (*DelegationLoader, error) {
|
||||
dirEntries, err := fs.ReadDir(TokenDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bundles := make(map[cid.Cid]delegation.Bundle, 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
|
||||
}
|
||||
|
||||
bundles[id] = delegation.Bundle{Cid: id, Decoded: tkn, Sealed: data}
|
||||
}
|
||||
|
||||
return &DelegationLoader{
|
||||
bundles: bundles,
|
||||
}, 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 CidToName(id cid.Cid) string {
|
||||
return cidToName[id]
|
||||
}
|
||||
|
||||
func mustGetBundle(id cid.Cid) delegation.Bundle {
|
||||
bundle, ok := GetDelegationLoader().bundles[id]
|
||||
if !ok {
|
||||
panic(delegation.ErrDelegationNotFound)
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
404
token/delegation/delegationtest/token_gen.go
Normal file
404
token/delegation/delegationtest/token_gen.go
Normal file
@@ -0,0 +1,404 @@
|
||||
// Code generated by delegationtest - DO NOT EDIT.
|
||||
|
||||
package delegationtest
|
||||
|
||||
import (
|
||||
"github.com/ipfs/go-cid"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||
)
|
||||
|
||||
var (
|
||||
TokenAliceBobCID = cid.MustParse("bafyreicidrwvmac5lvjypucgityrtjsknojraio7ujjli4r5eyby66wjzm")
|
||||
TokenAliceBobSealed = mustGetBundle(TokenAliceBobCID).Sealed
|
||||
TokenAliceBobBundle = mustGetBundle(TokenAliceBobCID)
|
||||
TokenAliceBob = mustGetBundle(TokenAliceBobCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenBobCarolCID = cid.MustParse("bafyreihxv2uhq43oxllzs2xfvxst7wtvvvl7pohb2chcz6hjvfv2ntea5u")
|
||||
TokenBobCarolSealed = mustGetBundle(TokenBobCarolCID).Sealed
|
||||
TokenBobCarolBundle = mustGetBundle(TokenBobCarolCID)
|
||||
TokenBobCarol = mustGetBundle(TokenBobCarolCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDanCID = cid.MustParse("bafyreihclsgiroazq3heqdswvj2cafwqbpboicq7immo65scl7ahktpsdq")
|
||||
TokenCarolDanSealed = mustGetBundle(TokenCarolDanCID).Sealed
|
||||
TokenCarolDanBundle = mustGetBundle(TokenCarolDanCID)
|
||||
TokenCarolDan = mustGetBundle(TokenCarolDanCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErinCID = cid.MustParse("bafyreicja6ihewy64p3ake56xukotafjlkh4uqep2qhj52en46zzfwby3e")
|
||||
TokenDanErinSealed = mustGetBundle(TokenDanErinCID).Sealed
|
||||
TokenDanErinBundle = mustGetBundle(TokenDanErinCID)
|
||||
TokenDanErin = mustGetBundle(TokenDanErinCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrankCID = cid.MustParse("bafyreicjlx3lobxm6hl5s4htd4ydwkkqeiou6rft4rnvulfdyoew565vka")
|
||||
TokenErinFrankSealed = mustGetBundle(TokenErinFrankCID).Sealed
|
||||
TokenErinFrankBundle = mustGetBundle(TokenErinFrankCID)
|
||||
TokenErinFrank = mustGetBundle(TokenErinFrankCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidExpandedCommandCID = cid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4")
|
||||
TokenCarolDan_InvalidExpandedCommandSealed = mustGetBundle(TokenCarolDan_InvalidExpandedCommandCID).Sealed
|
||||
TokenCarolDan_InvalidExpandedCommandBundle = mustGetBundle(TokenCarolDan_InvalidExpandedCommandCID)
|
||||
TokenCarolDan_InvalidExpandedCommand = mustGetBundle(TokenCarolDan_InvalidExpandedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidExpandedCommandCID = cid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm")
|
||||
TokenDanErin_InvalidExpandedCommandSealed = mustGetBundle(TokenDanErin_InvalidExpandedCommandCID).Sealed
|
||||
TokenDanErin_InvalidExpandedCommandBundle = mustGetBundle(TokenDanErin_InvalidExpandedCommandCID)
|
||||
TokenDanErin_InvalidExpandedCommand = mustGetBundle(TokenDanErin_InvalidExpandedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidExpandedCommandCID = cid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe")
|
||||
TokenErinFrank_InvalidExpandedCommandSealed = mustGetBundle(TokenErinFrank_InvalidExpandedCommandCID).Sealed
|
||||
TokenErinFrank_InvalidExpandedCommandBundle = mustGetBundle(TokenErinFrank_InvalidExpandedCommandCID)
|
||||
TokenErinFrank_InvalidExpandedCommand = mustGetBundle(TokenErinFrank_InvalidExpandedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_ValidAttenuatedCommandCID = cid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe")
|
||||
TokenCarolDan_ValidAttenuatedCommandSealed = mustGetBundle(TokenCarolDan_ValidAttenuatedCommandCID).Sealed
|
||||
TokenCarolDan_ValidAttenuatedCommandBundle = mustGetBundle(TokenCarolDan_ValidAttenuatedCommandCID)
|
||||
TokenCarolDan_ValidAttenuatedCommand = mustGetBundle(TokenCarolDan_ValidAttenuatedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_ValidAttenuatedCommandCID = cid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y")
|
||||
TokenDanErin_ValidAttenuatedCommandSealed = mustGetBundle(TokenDanErin_ValidAttenuatedCommandCID).Sealed
|
||||
TokenDanErin_ValidAttenuatedCommandBundle = mustGetBundle(TokenDanErin_ValidAttenuatedCommandCID)
|
||||
TokenDanErin_ValidAttenuatedCommand = mustGetBundle(TokenDanErin_ValidAttenuatedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_ValidAttenuatedCommandCID = cid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q")
|
||||
TokenErinFrank_ValidAttenuatedCommandSealed = mustGetBundle(TokenErinFrank_ValidAttenuatedCommandCID).Sealed
|
||||
TokenErinFrank_ValidAttenuatedCommandBundle = mustGetBundle(TokenErinFrank_ValidAttenuatedCommandCID)
|
||||
TokenErinFrank_ValidAttenuatedCommand = mustGetBundle(TokenErinFrank_ValidAttenuatedCommandCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidSubjectCID = cid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u")
|
||||
TokenCarolDan_InvalidSubjectSealed = mustGetBundle(TokenCarolDan_InvalidSubjectCID).Sealed
|
||||
TokenCarolDan_InvalidSubjectBundle = mustGetBundle(TokenCarolDan_InvalidSubjectCID)
|
||||
TokenCarolDan_InvalidSubject = mustGetBundle(TokenCarolDan_InvalidSubjectCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidSubjectCID = cid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty")
|
||||
TokenDanErin_InvalidSubjectSealed = mustGetBundle(TokenDanErin_InvalidSubjectCID).Sealed
|
||||
TokenDanErin_InvalidSubjectBundle = mustGetBundle(TokenDanErin_InvalidSubjectCID)
|
||||
TokenDanErin_InvalidSubject = mustGetBundle(TokenDanErin_InvalidSubjectCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidSubjectCID = cid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4")
|
||||
TokenErinFrank_InvalidSubjectSealed = mustGetBundle(TokenErinFrank_InvalidSubjectCID).Sealed
|
||||
TokenErinFrank_InvalidSubjectBundle = mustGetBundle(TokenErinFrank_InvalidSubjectCID)
|
||||
TokenErinFrank_InvalidSubject = mustGetBundle(TokenErinFrank_InvalidSubjectCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidExpiredCID = cid.MustParse("bafyreifyzm5jkx2sfu5awyndg3dn5zlg7sq5hssgfatafk62kiiilapnqe")
|
||||
TokenCarolDan_InvalidExpiredSealed = mustGetBundle(TokenCarolDan_InvalidExpiredCID).Sealed
|
||||
TokenCarolDan_InvalidExpiredBundle = mustGetBundle(TokenCarolDan_InvalidExpiredCID)
|
||||
TokenCarolDan_InvalidExpired = mustGetBundle(TokenCarolDan_InvalidExpiredCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidExpiredCID = cid.MustParse("bafyreihhnisabmkofuk3qaw37leijxqjaz5or6v2cufjxwzdkvuvv2dzbq")
|
||||
TokenDanErin_InvalidExpiredSealed = mustGetBundle(TokenDanErin_InvalidExpiredCID).Sealed
|
||||
TokenDanErin_InvalidExpiredBundle = mustGetBundle(TokenDanErin_InvalidExpiredCID)
|
||||
TokenDanErin_InvalidExpired = mustGetBundle(TokenDanErin_InvalidExpiredCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidExpiredCID = cid.MustParse("bafyreigeokaziviwm5kzmkpwesj3gta5k7zrd62x4a746fnrnkhvatwbna")
|
||||
TokenErinFrank_InvalidExpiredSealed = mustGetBundle(TokenErinFrank_InvalidExpiredCID).Sealed
|
||||
TokenErinFrank_InvalidExpiredBundle = mustGetBundle(TokenErinFrank_InvalidExpiredCID)
|
||||
TokenErinFrank_InvalidExpired = mustGetBundle(TokenErinFrank_InvalidExpiredCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_InvalidInactiveCID = cid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u")
|
||||
TokenCarolDan_InvalidInactiveSealed = mustGetBundle(TokenCarolDan_InvalidInactiveCID).Sealed
|
||||
TokenCarolDan_InvalidInactiveBundle = mustGetBundle(TokenCarolDan_InvalidInactiveCID)
|
||||
TokenCarolDan_InvalidInactive = mustGetBundle(TokenCarolDan_InvalidInactiveCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_InvalidInactiveCID = cid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq")
|
||||
TokenDanErin_InvalidInactiveSealed = mustGetBundle(TokenDanErin_InvalidInactiveCID).Sealed
|
||||
TokenDanErin_InvalidInactiveBundle = mustGetBundle(TokenDanErin_InvalidInactiveCID)
|
||||
TokenDanErin_InvalidInactive = mustGetBundle(TokenDanErin_InvalidInactiveCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_InvalidInactiveCID = cid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre")
|
||||
TokenErinFrank_InvalidInactiveSealed = mustGetBundle(TokenErinFrank_InvalidInactiveCID).Sealed
|
||||
TokenErinFrank_InvalidInactiveBundle = mustGetBundle(TokenErinFrank_InvalidInactiveCID)
|
||||
TokenErinFrank_InvalidInactive = mustGetBundle(TokenErinFrank_InvalidInactiveCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenCarolDan_ValidExamplePolicyCID = cid.MustParse("bafyreibtfrp2njnkjrcuhxd4ebaecmpcql5knek2h2j2fjzu2sij2tv6ei")
|
||||
TokenCarolDan_ValidExamplePolicySealed = mustGetBundle(TokenCarolDan_ValidExamplePolicyCID).Sealed
|
||||
TokenCarolDan_ValidExamplePolicyBundle = mustGetBundle(TokenCarolDan_ValidExamplePolicyCID)
|
||||
TokenCarolDan_ValidExamplePolicy = mustGetBundle(TokenCarolDan_ValidExamplePolicyCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenDanErin_ValidExamplePolicyCID = cid.MustParse("bafyreidxfwbkzujpu7ivulkc7b6ff4cpbzrkeklmxqvyhhmkmym5b45e2e")
|
||||
TokenDanErin_ValidExamplePolicySealed = mustGetBundle(TokenDanErin_ValidExamplePolicyCID).Sealed
|
||||
TokenDanErin_ValidExamplePolicyBundle = mustGetBundle(TokenDanErin_ValidExamplePolicyCID)
|
||||
TokenDanErin_ValidExamplePolicy = mustGetBundle(TokenDanErin_ValidExamplePolicyCID).Decoded
|
||||
)
|
||||
|
||||
var (
|
||||
TokenErinFrank_ValidExamplePolicyCID = cid.MustParse("bafyreiatkvtvgakqcrdk6vgrv7tbq5rbeiqct52ep4plcftp2agffjyvp4")
|
||||
TokenErinFrank_ValidExamplePolicySealed = mustGetBundle(TokenErinFrank_ValidExamplePolicyCID).Sealed
|
||||
TokenErinFrank_ValidExamplePolicyBundle = mustGetBundle(TokenErinFrank_ValidExamplePolicyCID)
|
||||
TokenErinFrank_ValidExamplePolicy = mustGetBundle(TokenErinFrank_ValidExamplePolicyCID).Decoded
|
||||
)
|
||||
|
||||
var AllTokens = []*delegation.Token{
|
||||
TokenAliceBob,
|
||||
TokenBobCarol,
|
||||
TokenCarolDan,
|
||||
TokenDanErin,
|
||||
TokenErinFrank,
|
||||
TokenCarolDan_InvalidExpandedCommand,
|
||||
TokenDanErin_InvalidExpandedCommand,
|
||||
TokenErinFrank_InvalidExpandedCommand,
|
||||
TokenCarolDan_ValidAttenuatedCommand,
|
||||
TokenDanErin_ValidAttenuatedCommand,
|
||||
TokenErinFrank_ValidAttenuatedCommand,
|
||||
TokenCarolDan_InvalidSubject,
|
||||
TokenDanErin_InvalidSubject,
|
||||
TokenErinFrank_InvalidSubject,
|
||||
TokenCarolDan_InvalidExpired,
|
||||
TokenDanErin_InvalidExpired,
|
||||
TokenErinFrank_InvalidExpired,
|
||||
TokenCarolDan_InvalidInactive,
|
||||
TokenDanErin_InvalidInactive,
|
||||
TokenErinFrank_InvalidInactive,
|
||||
TokenCarolDan_ValidExamplePolicy,
|
||||
TokenDanErin_ValidExamplePolicy,
|
||||
TokenErinFrank_ValidExamplePolicy,
|
||||
}
|
||||
|
||||
var AllBundles = []delegation.Bundle{
|
||||
TokenAliceBobBundle,
|
||||
TokenBobCarolBundle,
|
||||
TokenCarolDanBundle,
|
||||
TokenDanErinBundle,
|
||||
TokenErinFrankBundle,
|
||||
TokenCarolDan_InvalidExpandedCommandBundle,
|
||||
TokenDanErin_InvalidExpandedCommandBundle,
|
||||
TokenErinFrank_InvalidExpandedCommandBundle,
|
||||
TokenCarolDan_ValidAttenuatedCommandBundle,
|
||||
TokenDanErin_ValidAttenuatedCommandBundle,
|
||||
TokenErinFrank_ValidAttenuatedCommandBundle,
|
||||
TokenCarolDan_InvalidSubjectBundle,
|
||||
TokenDanErin_InvalidSubjectBundle,
|
||||
TokenErinFrank_InvalidSubjectBundle,
|
||||
TokenCarolDan_InvalidExpiredBundle,
|
||||
TokenDanErin_InvalidExpiredBundle,
|
||||
TokenErinFrank_InvalidExpiredBundle,
|
||||
TokenCarolDan_InvalidInactiveBundle,
|
||||
TokenDanErin_InvalidInactiveBundle,
|
||||
TokenErinFrank_InvalidInactiveBundle,
|
||||
TokenCarolDan_ValidExamplePolicyBundle,
|
||||
TokenDanErin_ValidExamplePolicyBundle,
|
||||
TokenErinFrank_ValidExamplePolicyBundle,
|
||||
}
|
||||
|
||||
var cidToName = map[cid.Cid]string{
|
||||
TokenAliceBobCID: "TokenAliceBob",
|
||||
TokenBobCarolCID: "TokenBobCarol",
|
||||
TokenCarolDanCID: "TokenCarolDan",
|
||||
TokenDanErinCID: "TokenDanErin",
|
||||
TokenErinFrankCID: "TokenErinFrank",
|
||||
TokenCarolDan_InvalidExpandedCommandCID: "TokenCarolDan_InvalidExpandedCommand",
|
||||
TokenDanErin_InvalidExpandedCommandCID: "TokenDanErin_InvalidExpandedCommand",
|
||||
TokenErinFrank_InvalidExpandedCommandCID: "TokenErinFrank_InvalidExpandedCommand",
|
||||
TokenCarolDan_ValidAttenuatedCommandCID: "TokenCarolDan_ValidAttenuatedCommand",
|
||||
TokenDanErin_ValidAttenuatedCommandCID: "TokenDanErin_ValidAttenuatedCommand",
|
||||
TokenErinFrank_ValidAttenuatedCommandCID: "TokenErinFrank_ValidAttenuatedCommand",
|
||||
TokenCarolDan_InvalidSubjectCID: "TokenCarolDan_InvalidSubject",
|
||||
TokenDanErin_InvalidSubjectCID: "TokenDanErin_InvalidSubject",
|
||||
TokenErinFrank_InvalidSubjectCID: "TokenErinFrank_InvalidSubject",
|
||||
TokenCarolDan_InvalidExpiredCID: "TokenCarolDan_InvalidExpired",
|
||||
TokenDanErin_InvalidExpiredCID: "TokenDanErin_InvalidExpired",
|
||||
TokenErinFrank_InvalidExpiredCID: "TokenErinFrank_InvalidExpired",
|
||||
TokenCarolDan_InvalidInactiveCID: "TokenCarolDan_InvalidInactive",
|
||||
TokenDanErin_InvalidInactiveCID: "TokenDanErin_InvalidInactive",
|
||||
TokenErinFrank_InvalidInactiveCID: "TokenErinFrank_InvalidInactive",
|
||||
TokenCarolDan_ValidExamplePolicyCID: "TokenCarolDan_ValidExamplePolicy",
|
||||
TokenDanErin_ValidExamplePolicyCID: "TokenDanErin_ValidExamplePolicy",
|
||||
TokenErinFrank_ValidExamplePolicyCID: "TokenErinFrank_ValidExamplePolicy",
|
||||
}
|
||||
|
||||
var ProofAliceBob = []cid.Cid{
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarol = []cid.Cid{
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan = []cid.Cid{
|
||||
TokenCarolDanCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin = []cid.Cid{
|
||||
TokenDanErinCID,
|
||||
TokenCarolDanCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank = []cid.Cid{
|
||||
TokenErinFrankCID,
|
||||
TokenDanErinCID,
|
||||
TokenCarolDanCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidExpandedCommand = []cid.Cid{
|
||||
TokenCarolDan_InvalidExpandedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidExpandedCommand = []cid.Cid{
|
||||
TokenDanErin_InvalidExpandedCommandCID,
|
||||
TokenCarolDan_InvalidExpandedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []cid.Cid{
|
||||
TokenErinFrank_InvalidExpandedCommandCID,
|
||||
TokenDanErin_InvalidExpandedCommandCID,
|
||||
TokenCarolDan_InvalidExpandedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_ValidAttenuatedCommand = []cid.Cid{
|
||||
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_ValidAttenuatedCommand = []cid.Cid{
|
||||
TokenDanErin_ValidAttenuatedCommandCID,
|
||||
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []cid.Cid{
|
||||
TokenErinFrank_ValidAttenuatedCommandCID,
|
||||
TokenDanErin_ValidAttenuatedCommandCID,
|
||||
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidSubject = []cid.Cid{
|
||||
TokenCarolDan_InvalidSubjectCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidSubject = []cid.Cid{
|
||||
TokenDanErin_InvalidSubjectCID,
|
||||
TokenCarolDan_InvalidSubjectCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidSubject = []cid.Cid{
|
||||
TokenErinFrank_InvalidSubjectCID,
|
||||
TokenDanErin_InvalidSubjectCID,
|
||||
TokenCarolDan_InvalidSubjectCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidExpired = []cid.Cid{
|
||||
TokenCarolDan_InvalidExpiredCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidExpired = []cid.Cid{
|
||||
TokenDanErin_InvalidExpiredCID,
|
||||
TokenCarolDan_InvalidExpiredCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidExpired = []cid.Cid{
|
||||
TokenErinFrank_InvalidExpiredCID,
|
||||
TokenDanErin_InvalidExpiredCID,
|
||||
TokenCarolDan_InvalidExpiredCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_InvalidInactive = []cid.Cid{
|
||||
TokenCarolDan_InvalidInactiveCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_InvalidInactive = []cid.Cid{
|
||||
TokenDanErin_InvalidInactiveCID,
|
||||
TokenCarolDan_InvalidInactiveCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_InvalidInactive = []cid.Cid{
|
||||
TokenErinFrank_InvalidInactiveCID,
|
||||
TokenDanErin_InvalidInactiveCID,
|
||||
TokenCarolDan_InvalidInactiveCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDan_ValidExamplePolicy = []cid.Cid{
|
||||
TokenCarolDan_ValidExamplePolicyCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErin_ValidExamplePolicy = []cid.Cid{
|
||||
TokenDanErin_ValidExamplePolicyCID,
|
||||
TokenCarolDan_ValidExamplePolicyCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
|
||||
var ProofAliceBobCarolDanErinFrank_ValidExamplePolicy = []cid.Cid{
|
||||
TokenErinFrank_ValidExamplePolicyCID,
|
||||
TokenDanErin_ValidExamplePolicyCID,
|
||||
TokenCarolDan_ValidExamplePolicyCID,
|
||||
TokenBobCarolCID,
|
||||
TokenAliceBobCID,
|
||||
}
|
||||
30
token/delegation/delegationtest/token_test.go
Normal file
30
token/delegation/delegationtest/token_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package delegation_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -13,9 +12,8 @@ import (
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||
@@ -26,15 +24,7 @@ import (
|
||||
// The following example shows how to create a delegation.Token with
|
||||
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
|
||||
func ExampleNew() {
|
||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
issDid, err := did.FromPubKey(issPub)
|
||||
printThenPanicOnErr(err)
|
||||
fmt.Println("issDid:", issDid)
|
||||
|
||||
audDid := did.MustParse(AudienceDID)
|
||||
subDid := did.MustParse(subjectDID)
|
||||
fmt.Println("issDid:", didtest.PersonaBob.DID().String())
|
||||
|
||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
||||
cmd := command.MustParse("/foo/bar")
|
||||
@@ -51,8 +41,7 @@ func ExampleNew() {
|
||||
)),
|
||||
)
|
||||
|
||||
tkn, err := delegation.New(issPriv, audDid, cmd, pol,
|
||||
delegation.WithSubject(subDid),
|
||||
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol, didtest.PersonaAlice.DID(),
|
||||
delegation.WithExpirationIn(time.Hour),
|
||||
delegation.WithNotBeforeIn(time.Minute),
|
||||
delegation.WithMeta("foo", "bar"),
|
||||
@@ -61,101 +50,91 @@ func ExampleNew() {
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
// "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)
|
||||
|
||||
printCIDAndSealed(id, data)
|
||||
|
||||
// Example output:
|
||||
//
|
||||
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe
|
||||
// issDid: did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU
|
||||
//
|
||||
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW
|
||||
//
|
||||
// DAG-CBOR (base64) out: glhA5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDWzqY2lzc3g4ZGlkOmtleTp6Nk1raFZGem5QZVI1NzJyVEs1MVVqb1ROcG5GOGN4dVdmUG05b0JNUHI3eThBQmVjbmJmGmcNXxZjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3RBMXVCZENwcTR1SkJxRTlqak1pTHl4WkJnOWE2eGdQUEtKak1xc3M2WmMyZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMu0HMgJ5Y+M84I/66
|
||||
// CID (base58BTC): zdpuAsqfZkgg2jgZyob23sq1J9xwtf9PHgt1PsskVCMq7Vvxk
|
||||
//
|
||||
// DAG-CBOR (base64) out: lhAOnjc0bPptlI5MxRBrIK3YmAP1CxKfXOPkz6MHt/UJCx2gCN+6gXZX2N+BIJvmy8XmAO5sT2GYimiV7HlJH1AA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rZ3VwY2hoNUh3dUhhaFM3WXN5RThiTHVhMU1yOHAyaUtOUmh5dlN2UkFzOW5jY21kaC9mb28vYmFyY2V4cBpnROP/Y2lzc3g4ZGlkOmtleTp6Nk1rdkpQbUVaWlliZ2l3MW91VDFvb3VUc1RGQkhKU3RzOW9waFZzTmdjUm1ZeFVjbmJmGmdE1itjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3V1a2syc2tEWExRbjdOSzNFaDlqTW5kWWZ2REJ4eGt0Z3BpZEpBcWI3TTNwZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMv+Diy6GExIuM1eX4
|
||||
// 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",
|
||||
// "cmd": "/foo/bar",
|
||||
// "exp": 1728933098,
|
||||
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
||||
// "meta": {
|
||||
// "baz": 123,
|
||||
// "foo": "bar"
|
||||
// },
|
||||
// "nbf": 1728929558,
|
||||
// "nonce": {
|
||||
// {
|
||||
// "h": {
|
||||
// "/": {
|
||||
// "bytes": "u0HMgJ5Y+M84I/66"
|
||||
// "bytes": "NO0BcQ"
|
||||
// }
|
||||
// },
|
||||
// "pol": [
|
||||
// [
|
||||
// "==",
|
||||
// ".status",
|
||||
// "draft"
|
||||
// ],
|
||||
// [
|
||||
// "all",
|
||||
// ".reviewer",
|
||||
// "ucan/dlg@1.0.0-rc.1": {
|
||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||
// "cmd": "/foo/bar",
|
||||
// "exp": 1728933098,
|
||||
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
||||
// "meta": {
|
||||
// "baz": 123,
|
||||
// "foo": "bar"
|
||||
// },
|
||||
// "nbf": 1728929558,
|
||||
// "nonce": {
|
||||
// "/": {
|
||||
// "bytes": "u0HMgJ5Y+M84I/66"
|
||||
// }
|
||||
// },
|
||||
// "pol": [
|
||||
// [
|
||||
// "like",
|
||||
// ".email",
|
||||
// "*@example.com"
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// "any",
|
||||
// ".tags",
|
||||
// "==",
|
||||
// ".status",
|
||||
// "draft"
|
||||
// ],
|
||||
// [
|
||||
// "or",
|
||||
// "all",
|
||||
// ".reviewer",
|
||||
// [
|
||||
// "like",
|
||||
// ".email",
|
||||
// "*@example.com"
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// "any",
|
||||
// ".tags",
|
||||
// [
|
||||
// "or",
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "news"
|
||||
// ],
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "press"
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "news"
|
||||
// ],
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "press"
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ],
|
||||
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||
// ],
|
||||
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// ]
|
||||
}
|
||||
|
||||
// The following example shows how to create a UCAN root delegation.Token
|
||||
// - a delegation.Token with the subject (sub) set to the value of issuer
|
||||
// (iss).
|
||||
func ExampleRoot() {
|
||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
issDid, err := did.FromPubKey(issPub)
|
||||
printThenPanicOnErr(err)
|
||||
fmt.Println("issDid:", issDid)
|
||||
|
||||
audDid := did.MustParse(AudienceDID)
|
||||
|
||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
||||
cmd := command.MustParse("/foo/bar")
|
||||
|
||||
@@ -171,7 +150,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.WithNotBeforeIn(time.Minute),
|
||||
delegation.WithMeta("foo", "bar"),
|
||||
@@ -180,7 +159,7 @@ func ExampleRoot() {
|
||||
printThenPanicOnErr(err)
|
||||
|
||||
// "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)
|
||||
|
||||
printCIDAndSealed(id, data)
|
||||
@@ -189,82 +168,82 @@ func ExampleRoot() {
|
||||
//
|
||||
// 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:
|
||||
// [
|
||||
// {
|
||||
// "/": {
|
||||
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// "h": {
|
||||
// [
|
||||
// {
|
||||
// "/": {
|
||||
// "bytes": "NO0BcQ"
|
||||
// "bytes": "VpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSAw"
|
||||
// }
|
||||
// },
|
||||
// "ucan/dlg@1.0.0-rc.1": {
|
||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||
// "cmd": "/foo/bar",
|
||||
// "exp": 1728933168,
|
||||
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
|
||||
// "meta": {
|
||||
// "baz": 123,
|
||||
// "foo": "bar"
|
||||
// },
|
||||
// "nbf": 1728929628,
|
||||
// "nonce": {
|
||||
// {
|
||||
// "h": {
|
||||
// "/": {
|
||||
// "bytes": "JOsjYi1Pq3OIB0La"
|
||||
// "bytes": "NO0BcQ"
|
||||
// }
|
||||
// },
|
||||
// "pol": [
|
||||
// [
|
||||
// "==",
|
||||
// ".status",
|
||||
// "draft"
|
||||
// ],
|
||||
// [
|
||||
// "all",
|
||||
// ".reviewer",
|
||||
// "ucan/dlg@1.0.0-rc.1": {
|
||||
// "aud": "did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU",
|
||||
// "cmd": "/foo/bar",
|
||||
// "exp": 1732568424,
|
||||
// "iss": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p",
|
||||
// "meta": {
|
||||
// "baz": 123,
|
||||
// "foo": "bar"
|
||||
// },
|
||||
// "nbf": 1732564884,
|
||||
// "nonce": {
|
||||
// "/": {
|
||||
// "bytes": "wzDc03WBciJIGPWG"
|
||||
// }
|
||||
// },
|
||||
// "pol": [
|
||||
// [
|
||||
// "like",
|
||||
// ".email",
|
||||
// "*@example.com"
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// "any",
|
||||
// ".tags",
|
||||
// "==",
|
||||
// ".status",
|
||||
// "draft"
|
||||
// ],
|
||||
// [
|
||||
// "or",
|
||||
// "all",
|
||||
// ".reviewer",
|
||||
// [
|
||||
// "like",
|
||||
// ".email",
|
||||
// "*@example.com"
|
||||
// ]
|
||||
// ],
|
||||
// [
|
||||
// "any",
|
||||
// ".tags",
|
||||
// [
|
||||
// "or",
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "news"
|
||||
// ],
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "press"
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "news"
|
||||
// ],
|
||||
// [
|
||||
// "==",
|
||||
// ".",
|
||||
// "press"
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ]
|
||||
// ],
|
||||
// "sub": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF"
|
||||
// ],
|
||||
// "sub": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// ]
|
||||
}
|
||||
|
||||
// The following example demonstrates how to get a delegation.Token from
|
||||
// a DAG-CBOR []byte.
|
||||
func ExampleToken_FromSealed() {
|
||||
func ExampleFromSealed() {
|
||||
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
||||
|
||||
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
||||
@@ -293,12 +272,14 @@ func ExampleToken_FromSealed() {
|
||||
// Policy (pol): [
|
||||
// ["==", ".status", "draft"],
|
||||
// ["all", ".reviewer",
|
||||
// ["like", ".email", "*@example.com"]],
|
||||
// ["like", ".email", "*@example.com"]
|
||||
// ],
|
||||
// ["any", ".tags",
|
||||
// ["or", [
|
||||
// ["==", ".", "news"],
|
||||
// ["==", ".", "press"]]]
|
||||
// ]
|
||||
// ["==", ".", "press"]
|
||||
// ]]
|
||||
// ]
|
||||
// ]
|
||||
// Nonce (nonce): 000102030405060708090a0b
|
||||
// Meta (meta): {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package delegation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"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) {
|
||||
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 {
|
||||
s := t.subject.String()
|
||||
sub = &s
|
||||
|
||||
@@ -3,8 +3,6 @@ package delegation
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
)
|
||||
|
||||
// Option is a type that allows optional fields to be set during the
|
||||
@@ -44,6 +42,24 @@ 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 key.
|
||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||
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 key.
|
||||
// The ciphertext will be 40 bytes larger than the plaintext due to encryption overhead.
|
||||
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
|
||||
// of the provided time.Time.
|
||||
func WithNotBefore(nbf time.Time) Option {
|
||||
@@ -67,20 +83,6 @@ func WithNotBeforeIn(nbf time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithSubject sets the Tokens's optional "subject" field to the value of
|
||||
// provided did.DID.
|
||||
//
|
||||
// This Option should only be used with the New constructor - since
|
||||
// Subject is a required parameter when creating a Token via the Root
|
||||
// constructor, any value provided via this Option will be silently
|
||||
// overwritten.
|
||||
func WithSubject(sub did.DID) Option {
|
||||
return func(t *Token) error {
|
||||
t.subject = sub
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNonce sets the Token's nonce with the given value.
|
||||
// If this option is not used, a random 12-byte nonce is generated for this required field.
|
||||
func WithNonce(nonce []byte) Option {
|
||||
|
||||
@@ -26,17 +26,17 @@ const Tag = "ucan/dlg@1.0.0-rc.1"
|
||||
var schemaBytes []byte
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
errSchema error
|
||||
)
|
||||
|
||||
func mustLoadSchema() *schema.TypeSystem {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||
if errSchema != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package delegation_test
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ipld/go-ipld-prime"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"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/internal/envelope"
|
||||
)
|
||||
@@ -22,7 +22,7 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
delegationJson := golden.Get(t, "new.dagjson")
|
||||
privKey := privKey(t, issuerPrivKeyCfg)
|
||||
privKey := didtest.PersonaAlice.PrivKey()
|
||||
|
||||
t.Run("via buffers", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -33,21 +33,19 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
p1, err := delegation.FromDagJson(delegationJson)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, newCID, err := p1.ToSealed(privKey)
|
||||
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))
|
||||
assert.Equal(t, envelope.CIDToBase58BTC(newCID), envelope.CIDToBase58BTC(id))
|
||||
|
||||
p2, c2, err := delegation.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(delegationJson), string(readJson))
|
||||
})
|
||||
@@ -63,11 +61,13 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
p1, err := delegation.FromDagJsonReader(buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, newCID, err := p1.ToSealed(privKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
cborBytes := &bytes.Buffer{}
|
||||
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
||||
t.Log(len(id.Bytes()), id.Bytes())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||
assert.Equal(t, envelope.CIDToBase58BTC(newCID), envelope.CIDToBase58BTC(id))
|
||||
|
||||
// buf = bytes.NewBuffer(cborBytes.Bytes())
|
||||
p2, c2, err := delegation.FromSealedReader(cborBytes)
|
||||
@@ -79,6 +79,16 @@ func TestSchemaRoundTrip(t *testing.T) {
|
||||
|
||||
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) {
|
||||
@@ -90,7 +100,7 @@ func BenchmarkSchemaLoad(b *testing.B) {
|
||||
|
||||
func BenchmarkRoundTrip(b *testing.B) {
|
||||
delegationJson := golden.Get(b, "new.dagjson")
|
||||
privKey := privKey(b, issuerPrivKeyCfg)
|
||||
privKey := didtest.PersonaAlice.PrivKey()
|
||||
|
||||
b.Run("via buffers", func(b *testing.B) {
|
||||
p1, _ := delegation.FromDagJson(delegationJson)
|
||||
|
||||
2
token/delegation/testdata/new.dagjson
vendored
2
token/delegation/testdata/new.dagjson
vendored
@@ -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":"YJsl8EMLnXSFE/nKKjMxz9bHHo+Y7QeLEzukEzW1TB+m53TTiY1aOt+qUO8JaTcOKsOHt/a4Vn+YiOd5CkLdAQ"}},{"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:z6Mkgupchh5HwuHahS7YsyE8bLua1Mr8p2iKNRhyvSvRAs9n"}}]
|
||||
1
token/delegation/testdata/powerline.dagjson
vendored
Normal file
1
token/delegation/testdata/powerline.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
||||
[{"/":{"bytes":"i3YkPDvNSU4V8XYEluZhLH0b+NDcW/6+PtPSUHC17cmXXqgelG0K4EzWQQkS9UsYCHfkZSCn9NjGSXYMMFhaAQ"}},{"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"]]]]]}}]
|
||||
2
token/delegation/testdata/root.dagjson
vendored
2
token/delegation/testdata/root.dagjson
vendored
@@ -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"}}]
|
||||
24
token/delegation/utilities.go
Normal file
24
token/delegation/utilities.go
Normal file
@@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// Bundle carries together a decoded token with its Cid and raw signed data.
|
||||
type Bundle struct {
|
||||
Cid cid.Cid
|
||||
Decoded *Token
|
||||
Sealed []byte
|
||||
}
|
||||
@@ -2,22 +2,22 @@ package token
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"github.com/ipld/go-ipld-prime/codec"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||
)
|
||||
|
||||
type Token interface {
|
||||
Marshaller
|
||||
|
||||
// Issuer returns the did.DID representing the Token's issuer.
|
||||
Issuer() did.DID
|
||||
// Meta returns the Token's metadata.
|
||||
Meta() meta.ReadOnly
|
||||
// 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.
|
||||
IsValidNow() bool
|
||||
// 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 {
|
||||
@@ -39,3 +39,10 @@ type Marshaller interface {
|
||||
// ToDagJsonWriter is the same as ToDagJson, but it accepts an io.Writer.
|
||||
ToDagJsonWriter(w io.Writer, privKey crypto.PrivKey) error
|
||||
}
|
||||
|
||||
// Bundle carries together a decoded token with its Cid and raw signed data.
|
||||
type Bundle struct {
|
||||
Cid cid.Cid
|
||||
Decoded Token
|
||||
Sealed []byte
|
||||
}
|
||||
|
||||
@@ -41,18 +41,18 @@ const (
|
||||
var schemaBytes []byte
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
err error
|
||||
once sync.Once
|
||||
ts *schema.TypeSystem
|
||||
errSchema error
|
||||
)
|
||||
|
||||
func mustLoadSchema() *schema.TypeSystem {
|
||||
once.Do(func() {
|
||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||
if errSchema != nil {
|
||||
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
||||
}
|
||||
|
||||
return ts
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
// TODO: can we use the already serialized CBOR data here, instead of encoding again the payload?
|
||||
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
|
||||
31
token/internal/nonce/nonce.go
Normal file
31
token/internal/nonce/nonce.go
Normal 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
|
||||
}
|
||||
29
token/internal/parse/parse.go
Normal file
29
token/internal/parse/parse.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ucan-wg/go-ucan/did"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func OptionalDID(s *string) (did.DID, error) {
|
||||
if s == nil {
|
||||
return did.Undef, nil
|
||||
}
|
||||
return did.Parse(*s)
|
||||
}
|
||||
|
||||
func OptionalTimestamp(sec *int64) (*time.Time, error) {
|
||||
if sec == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if *sec > limits.MaxInt53 || *sec < limits.MinInt53 {
|
||||
return nil, fmt.Errorf("timestamp value %d exceeds safe integer bounds", *sec)
|
||||
}
|
||||
|
||||
t := time.Unix(*sec, 0)
|
||||
return &t, nil
|
||||
}
|
||||
64
token/internal/parse/parse_test.go
Normal file
64
token/internal/parse/parse_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package parse
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/ucan-wg/go-ucan/pkg/policy/limits"
|
||||
)
|
||||
|
||||
func TestOptionalTimestamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *int64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil timestamp",
|
||||
input: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid timestamp",
|
||||
input: int64Ptr(1625097600),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "max safe integer",
|
||||
input: int64Ptr(limits.MaxInt53),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "exceeds max safe integer",
|
||||
input: int64Ptr(limits.MaxInt53 + 1),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "below min safe integer",
|
||||
input: int64Ptr(limits.MinInt53 - 1),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := OptionalTimestamp(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "exceeds safe integer bounds")
|
||||
require.Nil(t, result)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
if tt.input == nil {
|
||||
require.Nil(t, result)
|
||||
} else {
|
||||
require.NotNil(t, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func int64Ptr(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
41
token/invocation/errors.go
Normal file
41
token/invocation/errors.go
Normal file
@@ -0,0 +1,41 @@
|
||||
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")
|
||||
)
|
||||
|
||||
// ErrPolicyNotSatisfied is returned when the provided Arguments don't
|
||||
// satisfy one or more of the aggregated Policy Statements
|
||||
var ErrPolicyNotSatisfied = errors.New("the following UCAN policy is not satisfied")
|
||||
201
token/invocation/examples_test.go
Normal file
201
token/invocation/examples_test.go
Normal 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, cmd, sub, 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user