Compare commits
82 Commits
match-trac
...
v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dddb67a2b7 | ||
|
|
60bdc8873b | ||
|
|
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 |
77
Readme.md
Normal file
77
Readme.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan" target="_blank">
|
||||||
|
<img src="https://raw.githubusercontent.com/ucan-wg/go-ucan/v1/assets/logo.png" alt="go-ucan Logo" height="250"></img>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<h1 align="center">go-ucan</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/UCAN-v1.0.0--rc.1-blue" alt="UCAN v1.0.0-rc.1">
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/tags">
|
||||||
|
<img alt="GitHub Tag" src="https://img.shields.io/github/v/tag/ucan-wg/go-ucan">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/actions?query=">
|
||||||
|
<img src="https://github.com/ucan-wg/go-ucan/actions/workflows/gotest.yml/badge.svg" alt="Build Status">
|
||||||
|
</a>
|
||||||
|
<a href="https://ucan-wg.github.io/go-ucan/dev/bench/">
|
||||||
|
<img alt="Go benchmarks" src="https://img.shields.io/badge/Benchmarks-go-blue">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md">
|
||||||
|
<img alt="Apache 2.0 + MIT License" src="https://img.shields.io/badge/License-Apache--2.0+MIT-green">
|
||||||
|
</a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/ucan-wg/go-ucan">
|
||||||
|
<img src="https://img.shields.io/badge/Docs-godoc-blue" alt="Docs">
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/JSyFG6XgVM">
|
||||||
|
<img src="https://img.shields.io/static/v1?label=Discord&message=join%20us!&color=mediumslateblue" alt="Discord">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
This is a go library to help the next generation of web and decentralized applications make use
|
||||||
|
of UCANs in their authorization flows.
|
||||||
|
|
||||||
|
User Controlled Authorization Networks (UCANs) are a way of doing authorization where users are fully in control. OAuth is designed for a centralized world, UCAN is the distributed user controlled version.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
### Specifications
|
||||||
|
|
||||||
|
The UCAN specification is separated in multiple sub-spec:
|
||||||
|
- [Main specification](https://github.com/ucan-wg/spec)
|
||||||
|
- [Delegation](https://github.com/ucan-wg/delegation/tree/v1_ipld)
|
||||||
|
- [Invocation](https://github.com/ucan-wg/invocation)
|
||||||
|
|
||||||
|
Not implemented yet:
|
||||||
|
- [Revocation](https://github.com/ucan-wg/revocation/tree/first-draft)
|
||||||
|
- [Promise](https://github.com/ucan-wg/promise/tree/v1-rc1)
|
||||||
|
|
||||||
|
### Talks
|
||||||
|
|
||||||
|
- [Decentralizing Auth, and UCAN Too - Brooklyn Zelenka (2023)](https://www.youtube.com/watch?v=MuHfrqw9gQA)
|
||||||
|
- [What's New in UCAN 1.0 - Brooklyn Zelenka (2024)](https://www.youtube.com/watch?v=-uohQzZcwF4)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
`go-ucan` currently support the required parts of the UCAN specification: the main specification, delegation and invocation.
|
||||||
|
|
||||||
|
Besides that, `go-ucan` also includes:
|
||||||
|
- a simplified [DID](https://www.w3.org/TR/did-core/) and [did-key](https://w3c-ccg.github.io/did-method-key/) implementation
|
||||||
|
- a [token container](https://github.com/ucan-wg/go-ucan/tree/v1/pkg/container) with CBOR and CAR format, to package and carry tokens together
|
||||||
|
- support for encrypted values in token's metadata
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
For usage questions, usecases, or issues reach out to us in our `go-ucan`
|
||||||
|
[Discord channel](https://discord.gg/3EHEQ6M8BC).
|
||||||
|
|
||||||
|
We would be happy to try to answer your question or try opening a new issue on
|
||||||
|
Github.
|
||||||
|
|
||||||
|
## UCAN Gopher
|
||||||
|
|
||||||
|
Artwork by [Bruno Monts](https://www.instagram.com/bruno_monts). Thank you [Renee French](http://reneefrench.blogspot.com/) for creating the [Go Gopher](https://blog.golang.org/gopher)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the double license [Apache 2.0 + MIT](https://github.com/ucan-wg/go-ucan/blob/v1/LICENSE.md).
|
||||||
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -78,7 +78,7 @@ func MustParse(str string) DID {
|
|||||||
|
|
||||||
// Defined tells if the DID is defined, not equal to Undef.
|
// Defined tells if the DID is defined, not equal to Undef.
|
||||||
func (d DID) Defined() bool {
|
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.
|
// PubKey returns the public key encapsulated by the did:key.
|
||||||
|
|||||||
127
did/didtest/crypto.go
Normal file
127
did/didtest/crypto.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Package didtest provides Personas that can be used for testing. Each
|
||||||
|
// Persona has a name, crypto.PrivKey and associated crypto.PubKey and
|
||||||
|
// did.DID.
|
||||||
|
package didtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alicePrivKeyB64 = "CAESQHdNJLBBiuc1AdwPHBkubB2KS1p0cv2JEF7m8tfwtrcm5ajaYPm+XmVCmtcHOF2lGDlmaiDA7emfwD3IrcyES0M="
|
||||||
|
bobPrivKeyB64 = "CAESQHBz+AIop1g+9iBDj+ufUc/zm9/ry7c6kDFO8Wl/D0+H63V9hC6s9l4npf3pYEFCjBtlR0AMNWMoFQKSlYNKo20="
|
||||||
|
carolPrivKeyB64 = "CAESQPrCgkcHnYFXDT9AlAydhPECBEivEuuVx9dJxLjVvDTmJIVNivfzg6H4mAiPfYS+5ryVVUZTHZBzvMuvvvG/Ks0="
|
||||||
|
danPrivKeyB64 = "CAESQCgNhzofKhC+7hW6x+fNd7iMPtQHeEmKRhhlduf/I7/TeOEFYAEflbJ0sAhMeDJ/HQXaAvsWgHEbJ3ZLhP8q2B0="
|
||||||
|
erinPrivKeyB64 = "CAESQKhCJo5UBpQcthko8DKMFsbdZ+qqQ5oc01CtLCqrE90dF2GfRlrMmot3WPHiHGCmEYi5ZMEHuiSI095e/6O4Bpw="
|
||||||
|
frankPrivKeyB64 = "CAESQDlXPKsy3jHh7OWTWQqyZF95Ueac5DKo7xD0NOBE5F2BNr1ZVxRmJ2dBELbOt8KP9sOACcO9qlCB7uMA1UQc7sk="
|
||||||
|
)
|
||||||
|
|
||||||
|
// Persona is a generic participant used for cryptographic testing.
|
||||||
|
type Persona int
|
||||||
|
|
||||||
|
// The provided Personas were selected from the first few generic
|
||||||
|
// participants listed in this [table].
|
||||||
|
//
|
||||||
|
// [table]: https://en.wikipedia.org/wiki/Alice_and_Bob#Cryptographic_systems
|
||||||
|
const (
|
||||||
|
PersonaAlice Persona = iota
|
||||||
|
PersonaBob
|
||||||
|
PersonaCarol
|
||||||
|
PersonaDan
|
||||||
|
PersonaErin
|
||||||
|
PersonaFrank
|
||||||
|
)
|
||||||
|
|
||||||
|
var privKeys map[Persona]crypto.PrivKey
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
privKeys = make(map[Persona]crypto.PrivKey, 6)
|
||||||
|
for persona, privKeyCfg := range privKeyB64() {
|
||||||
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
privKeys[persona] = privKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DID returns a did.DID based on the Persona's Ed25519 public key.
|
||||||
|
func (p Persona) DID() did.DID {
|
||||||
|
d, err := did.FromPrivKey(p.PrivKey())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the username of the Persona.
|
||||||
|
func (p Persona) Name() string {
|
||||||
|
name, ok := map[Persona]string{
|
||||||
|
PersonaAlice: "Alice",
|
||||||
|
PersonaBob: "Bob",
|
||||||
|
PersonaCarol: "Carol",
|
||||||
|
PersonaDan: "Dan",
|
||||||
|
PersonaErin: "Erin",
|
||||||
|
PersonaFrank: "Frank",
|
||||||
|
}[p]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("Unknown persona: %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivKey returns the Ed25519 private key for the Persona.
|
||||||
|
func (p Persona) PrivKey() crypto.PrivKey {
|
||||||
|
return privKeys[p]
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubKey returns the Ed25519 public key for the Persona.
|
||||||
|
func (p Persona) PubKey() crypto.PubKey {
|
||||||
|
return p.PrivKey().GetPublic()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubKeyConfig returns the marshaled and encoded Ed25519 public key
|
||||||
|
// for the Persona.
|
||||||
|
func (p Persona) PubKeyConfig(t *testing.T) string {
|
||||||
|
pubKeyMar, err := crypto.MarshalPublicKey(p.PrivKey().GetPublic())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return crypto.ConfigEncodeKey(pubKeyMar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func privKeyB64() map[Persona]string {
|
||||||
|
return map[Persona]string{
|
||||||
|
PersonaAlice: alicePrivKeyB64,
|
||||||
|
PersonaBob: bobPrivKeyB64,
|
||||||
|
PersonaCarol: carolPrivKeyB64,
|
||||||
|
PersonaDan: danPrivKeyB64,
|
||||||
|
PersonaErin: erinPrivKeyB64,
|
||||||
|
PersonaFrank: frankPrivKeyB64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Personas returns an (alphabetically) ordered list of the defined
|
||||||
|
// Persona values.
|
||||||
|
func Personas() []Persona {
|
||||||
|
return []Persona{
|
||||||
|
PersonaAlice,
|
||||||
|
PersonaBob,
|
||||||
|
PersonaCarol,
|
||||||
|
PersonaDan,
|
||||||
|
PersonaErin,
|
||||||
|
PersonaFrank,
|
||||||
|
}
|
||||||
|
}
|
||||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module github.com/ucan-wg/go-ucan
|
|||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/dave/jennifer v1.7.1
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||||
github.com/ipfs/go-cid v0.4.1
|
github.com/ipfs/go-cid v0.4.1
|
||||||
github.com/ipld/go-ipld-prime v0.21.0
|
github.com/ipld/go-ipld-prime v0.21.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
|
||||||
|
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
137
pkg/args/args.go
Normal file
137
pkg/args/args.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Package args provides the type that represents the Arguments passed to
|
||||||
|
// a command within an invocation.Token as well as a convenient Add method
|
||||||
|
// to incrementally build the underlying map.
|
||||||
|
package args
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Args are the Command's arguments when an invocation Token is processed by the executor.
|
||||||
|
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||||
|
// and transformations, while hiding the IPLD complexity from the caller.
|
||||||
|
type Args struct {
|
||||||
|
// This type must be compatible with the IPLD type represented by the IPLD
|
||||||
|
// schema { String : Any }.
|
||||||
|
|
||||||
|
Keys []string
|
||||||
|
Values map[string]ipld.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a pointer to an initialized Args value.
|
||||||
|
func New() *Args {
|
||||||
|
return &Args{
|
||||||
|
Values: map[string]ipld.Node{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inserts a key/value pair in the Args set.
|
||||||
|
//
|
||||||
|
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||||
|
func (a *Args) Add(key string, val any) error {
|
||||||
|
if _, ok := a.Values[key]; ok {
|
||||||
|
return fmt.Errorf("duplicate key %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := literal.Any(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Values[key] = node
|
||||||
|
a.Keys = append(a.Keys, key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include merges the provided arguments into the existing arguments.
|
||||||
|
//
|
||||||
|
// If duplicate keys are encountered, the new value is silently dropped
|
||||||
|
// without causing an error.
|
||||||
|
func (a *Args) Include(other *Args) {
|
||||||
|
for _, key := range other.Keys {
|
||||||
|
if _, ok := a.Values[key]; ok {
|
||||||
|
// don't overwrite
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.Values[key] = other.Values[key]
|
||||||
|
a.Keys = append(a.Keys, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToIPLD wraps an instance of an Args with an ipld.Node.
|
||||||
|
func (a *Args) ToIPLD() (ipld.Node, error) {
|
||||||
|
sort.Strings(a.Keys)
|
||||||
|
|
||||||
|
return qp.BuildMap(basicnode.Prototype.Any, int64(len(a.Keys)), func(ma datamodel.MapAssembler) {
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
qp.MapEntry(ma, key, qp.Node(a.Values[key]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals tells if two Args hold the same values.
|
||||||
|
func (a *Args) Equals(other *Args) bool {
|
||||||
|
if len(a.Keys) != len(other.Keys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(a.Values) != len(other.Values) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
if !ipld.DeepEqual(a.Values[key], other.Values[key]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Args) String() string {
|
||||||
|
sort.Strings(a.Keys)
|
||||||
|
|
||||||
|
buf := strings.Builder{}
|
||||||
|
buf.WriteString("{")
|
||||||
|
|
||||||
|
for _, key := range a.Keys {
|
||||||
|
buf.WriteString("\n\t")
|
||||||
|
buf.WriteString(key)
|
||||||
|
buf.WriteString(": ")
|
||||||
|
buf.WriteString(strings.ReplaceAll(printer.Sprint(a.Values[key]), "\n", "\n\t"))
|
||||||
|
buf.WriteString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.Keys) > 0 {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
buf.WriteString("}")
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadOnly returns a read-only version of Args.
|
||||||
|
func (a *Args) ReadOnly() ReadOnly {
|
||||||
|
return ReadOnly{args: a}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone makes a deep copy.
|
||||||
|
func (a *Args) Clone() *Args {
|
||||||
|
res := &Args{
|
||||||
|
Keys: make([]string, len(a.Keys)),
|
||||||
|
Values: make(map[string]ipld.Node, len(a.Values)),
|
||||||
|
}
|
||||||
|
copy(res.Keys, a.Keys)
|
||||||
|
for k, v := range a.Values {
|
||||||
|
res.Values[k] = v
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
179
pkg/args/args_test.go
Normal file
179
pkg/args/args_test.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package args_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/ipld/go-ipld-prime"
|
||||||
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArgs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const (
|
||||||
|
intKey = "intKey"
|
||||||
|
mapKey = "mapKey"
|
||||||
|
nilKey = "nilKey"
|
||||||
|
boolKey = "boolKey"
|
||||||
|
linkKey = "linkKey"
|
||||||
|
listKey = "listKey"
|
||||||
|
nodeKey = "nodeKey"
|
||||||
|
uintKey = "uintKey"
|
||||||
|
bytesKey = "bytesKey"
|
||||||
|
floatKey = "floatKey"
|
||||||
|
stringKey = "stringKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
expIntVal = int64(-42)
|
||||||
|
expBoolVal = true
|
||||||
|
expUintVal = uint(42)
|
||||||
|
expStringVal = "stringVal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
expMapVal = map[string]string{"keyOne": "valOne", "keyTwo": "valTwo"}
|
||||||
|
// expNilVal = (map[string]string)(nil)
|
||||||
|
expLinkVal = cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")
|
||||||
|
expListVal = []string{"elem1", "elem2", "elem3"}
|
||||||
|
expNodeVal = literal.String("nodeVal")
|
||||||
|
expBytesVal = []byte{0xde, 0xad, 0xbe, 0xef}
|
||||||
|
expFloatVal = 42.0
|
||||||
|
)
|
||||||
|
|
||||||
|
argsIn := args.New()
|
||||||
|
|
||||||
|
for _, a := range []struct {
|
||||||
|
key string
|
||||||
|
val any
|
||||||
|
}{
|
||||||
|
{key: intKey, val: expIntVal},
|
||||||
|
{key: mapKey, val: expMapVal},
|
||||||
|
// {key: nilKey, val: expNilVal},
|
||||||
|
{key: boolKey, val: expBoolVal},
|
||||||
|
{key: linkKey, val: expLinkVal},
|
||||||
|
{key: listKey, val: expListVal},
|
||||||
|
{key: uintKey, val: expUintVal},
|
||||||
|
{key: nodeKey, val: expNodeVal},
|
||||||
|
{key: bytesKey, val: expBytesVal},
|
||||||
|
{key: floatKey, val: expFloatVal},
|
||||||
|
{key: stringKey, val: expStringVal},
|
||||||
|
} {
|
||||||
|
require.NoError(t, argsIn.Add(a.key, a.val))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round-trip to DAG-CBOR
|
||||||
|
argsOut := roundTripThroughDAGCBOR(t, argsIn)
|
||||||
|
assert.ElementsMatch(t, argsIn.Keys, argsOut.Keys)
|
||||||
|
assert.Equal(t, argsIn.Values, argsOut.Values)
|
||||||
|
|
||||||
|
actMapVal := map[string]string{}
|
||||||
|
mit := argsOut.Values[mapKey].MapIterator()
|
||||||
|
|
||||||
|
for !mit.Done() {
|
||||||
|
k, v, err := mit.Next()
|
||||||
|
require.NoError(t, err)
|
||||||
|
ks := must(k.AsString())
|
||||||
|
vs := must(v.AsString())
|
||||||
|
actMapVal[ks] = vs
|
||||||
|
}
|
||||||
|
|
||||||
|
actListVal := []string{}
|
||||||
|
lit := argsOut.Values[listKey].ListIterator()
|
||||||
|
|
||||||
|
for !lit.Done() {
|
||||||
|
_, v, err := lit.Next()
|
||||||
|
require.NoError(t, err)
|
||||||
|
vs := must(v.AsString())
|
||||||
|
|
||||||
|
actListVal = append(actListVal, vs)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expIntVal, must(argsOut.Values[intKey].AsInt()))
|
||||||
|
assert.Equal(t, expMapVal, actMapVal) // TODO: special accessor
|
||||||
|
// TODO: the nil map comes back empty (but the right type)
|
||||||
|
// assert.Equal(t, expNilVal, actNilVal)
|
||||||
|
assert.Equal(t, expBoolVal, must(argsOut.Values[boolKey].AsBool()))
|
||||||
|
assert.Equal(t, expLinkVal.String(), must(argsOut.Values[linkKey].AsLink()).(datamodel.Link).String()) // TODO: special accessor
|
||||||
|
assert.Equal(t, expListVal, actListVal) // TODO: special accessor
|
||||||
|
assert.Equal(t, expNodeVal, argsOut.Values[nodeKey])
|
||||||
|
assert.Equal(t, expUintVal, uint(must(argsOut.Values[uintKey].AsInt())))
|
||||||
|
assert.Equal(t, expBytesVal, must(argsOut.Values[bytesKey].AsBytes()))
|
||||||
|
assert.Equal(t, expFloatVal, must(argsOut.Values[floatKey].AsFloat()))
|
||||||
|
assert.Equal(t, expStringVal, must(argsOut.Values[stringKey].AsString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgs_Include(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
argsIn := args.New()
|
||||||
|
require.NoError(t, argsIn.Add("key1", "val1"))
|
||||||
|
require.NoError(t, argsIn.Add("key2", "val2"))
|
||||||
|
|
||||||
|
argsOther := args.New()
|
||||||
|
require.NoError(t, argsOther.Add("key2", "valOther")) // This should not overwrite key2 above
|
||||||
|
require.NoError(t, argsOther.Add("key3", "val3"))
|
||||||
|
require.NoError(t, argsOther.Add("key4", "val4"))
|
||||||
|
|
||||||
|
argsIn.Include(argsOther)
|
||||||
|
|
||||||
|
assert.Len(t, argsIn.Values, 4)
|
||||||
|
assert.Equal(t, "val1", must(argsIn.Values["key1"].AsString()))
|
||||||
|
assert.Equal(t, "val2", must(argsIn.Values["key2"].AsString()))
|
||||||
|
assert.Equal(t, "val3", must(argsIn.Values["key3"].AsString()))
|
||||||
|
assert.Equal(t, "val4", must(argsIn.Values["key4"].AsString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
argsSchema = "type Args { String : Any }"
|
||||||
|
argsName = "Args"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
ts *schema.TypeSystem
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
func argsType() schema.Type {
|
||||||
|
once.Do(func() {
|
||||||
|
ts, err = ipld.LoadSchemaBytes([]byte(argsSchema))
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.TypeByName(argsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundTripThroughDAGCBOR(t *testing.T, argsIn *args.Args) *args.Args {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
node, err := argsIn.ToIPLD()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := ipld.Encode(node, dagcbor.Encode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var argsOut args.Args
|
||||||
|
_, err = ipld.Unmarshal(data, dagcbor.Decode, &argsOut, argsType())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return &argsOut
|
||||||
|
}
|
||||||
|
|
||||||
|
func must[T any](t T, err error) T {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
23
pkg/args/readonly.go
Normal file
23
pkg/args/readonly.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package args
|
||||||
|
|
||||||
|
import "github.com/ipld/go-ipld-prime"
|
||||||
|
|
||||||
|
type ReadOnly struct {
|
||||||
|
args *Args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) ToIPLD() (ipld.Node, error) {
|
||||||
|
return r.args.ToIPLD()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) Equals(other *Args) bool {
|
||||||
|
return r.args.Equals(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) String() string {
|
||||||
|
return r.args.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) WriteableClone() *Args {
|
||||||
|
return r.args.Clone()
|
||||||
|
}
|
||||||
@@ -98,7 +98,34 @@ func (c Command) Join(segments ...string) Command {
|
|||||||
// Segments returns the ordered segments that comprise the Command as a
|
// Segments returns the ordered segments that comprise the Command as a
|
||||||
// slice of strings.
|
// slice of strings.
|
||||||
func (c Command) Segments() []string {
|
func (c Command) Segments() []string {
|
||||||
return strings.Split(string(c), separator)
|
if c == separator {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return strings.Split(string(c), separator)[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Covers returns true if the command is identical or a parent of the given other command.
|
||||||
|
func (c Command) Covers(other Command) bool {
|
||||||
|
// fast-path, equivalent to the code below (verified with fuzzing)
|
||||||
|
if !strings.HasPrefix(string(other), string(c)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c == separator || len(c) == len(other) || other[len(c)] == separator[0]
|
||||||
|
|
||||||
|
/* -------
|
||||||
|
|
||||||
|
otherSegments := other.Segments()
|
||||||
|
if len(otherSegments) < len(c.Segments()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, s := range c.Segments() {
|
||||||
|
if otherSegments[i] != s {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the composed representation the command. This is also
|
// String returns the composed representation the command. This is also
|
||||||
|
|||||||
@@ -73,6 +73,21 @@ func TestJoin(t *testing.T) {
|
|||||||
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String())
|
require.Equal(t, "/faz/boz/foo/bar", command.MustParse("/faz/boz").Join("foo", "bar").String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSegments(t *testing.T) {
|
||||||
|
require.Empty(t, command.Top().Segments())
|
||||||
|
require.Equal(t, []string{"foo", "bar", "baz"}, command.MustParse("/foo/bar/baz").Segments())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCovers(t *testing.T) {
|
||||||
|
require.True(t, command.MustParse("/foo/bar/baz").Covers(command.MustParse("/foo/bar/baz")))
|
||||||
|
require.True(t, command.MustParse("/foo/bar").Covers(command.MustParse("/foo/bar/baz")))
|
||||||
|
require.False(t, command.MustParse("/foo/bar/baz").Covers(command.MustParse("/foo/bar")))
|
||||||
|
require.True(t, command.MustParse("/").Covers(command.MustParse("/foo")))
|
||||||
|
require.True(t, command.MustParse("/").Covers(command.MustParse("/foo/bar/baz")))
|
||||||
|
require.False(t, command.MustParse("/foo").Covers(command.MustParse("/foo00")))
|
||||||
|
require.False(t, command.MustParse("/foo/bar").Covers(command.MustParse("/foo/bar00")))
|
||||||
|
}
|
||||||
|
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
inp string
|
inp string
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ func TestCarRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FuzzCarRoundTrip(f *testing.F) {
|
func FuzzCarRoundTrip(f *testing.F) {
|
||||||
|
// Note: this fuzzing is somewhat broken.
|
||||||
|
// After some time, the fuzzer discover that a varint can be serialized in different
|
||||||
|
// ways that lead to the same integer value. This means that the CAR format can have
|
||||||
|
// multiple legal binary representation for the exact same data, which is what we are
|
||||||
|
// trying to detect here. Ideally, the format would be stricter, but that's how things
|
||||||
|
// are.
|
||||||
|
|
||||||
example, err := os.ReadFile("testdata/sample-v1.car")
|
example, err := os.ReadFile("testdata/sample-v1.car")
|
||||||
require.NoError(f, err)
|
require.NoError(f, err)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"iter"
|
"iter"
|
||||||
@@ -34,13 +35,16 @@ func (ctn Reader) GetToken(cid cid.Cid) (token.Token, error) {
|
|||||||
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
// GetDelegation is the same as GetToken but only return a delegation.Token, with the right type.
|
||||||
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
func (ctn Reader) GetDelegation(cid cid.Cid) (*delegation.Token, error) {
|
||||||
tkn, err := ctn.GetToken(cid)
|
tkn, err := ctn.GetToken(cid)
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
return nil, delegation.ErrDelegationNotFound
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if tkn, ok := tkn.(*delegation.Token); ok {
|
if tkn, ok := tkn.(*delegation.Token); ok {
|
||||||
return tkn, nil
|
return tkn, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("not a delegation token")
|
return nil, delegation.ErrDelegationNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllDelegations returns all the delegation.Token in the container.
|
// GetAllDelegations returns all the delegation.Token in the container.
|
||||||
@@ -98,15 +102,36 @@ func FromCbor(r io.Reader) (Reader, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if n.Kind() != datamodel.Kind_List {
|
if n.Kind() != datamodel.Kind_Map {
|
||||||
return nil, fmt.Errorf("not a list")
|
return nil, fmt.Errorf("invalid container format: expected map")
|
||||||
|
}
|
||||||
|
if n.Length() != 1 {
|
||||||
|
return nil, fmt.Errorf("invalid container format: expected single version key")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctn := make(Reader, n.Length())
|
// get the first (and only) key-value pair
|
||||||
|
it := n.MapIterator()
|
||||||
|
key, tokensNode, err := it.Next()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
it := n.ListIterator()
|
version, err := key.AsString()
|
||||||
for !it.Done() {
|
if err != nil {
|
||||||
_, val, err := it.Next()
|
return nil, fmt.Errorf("invalid container format: version must be string")
|
||||||
|
}
|
||||||
|
if version != currentContainerVersion {
|
||||||
|
return nil, fmt.Errorf("unsupported container version: %s", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokensNode.Kind() != datamodel.Kind_List {
|
||||||
|
return nil, fmt.Errorf("invalid container format: tokens must be a list")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctn := make(Reader, tokensNode.Length())
|
||||||
|
it2 := tokensNode.ListIterator()
|
||||||
|
for !it2.Done() {
|
||||||
|
_, val, err := it2.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
|
|||||||
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
opts = append(opts, delegation.WithMeta(randomString(8), randomString(10)))
|
||||||
}
|
}
|
||||||
|
|
||||||
t, err := delegation.New(priv, aud, cmd, pol, opts...)
|
t, err := delegation.New(iss, aud, cmd, pol, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -176,3 +176,30 @@ func randToken() (*delegation.Token, cid.Cid, []byte) {
|
|||||||
}
|
}
|
||||||
return t, c, b
|
return t, c, b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FuzzContainerRead(f *testing.F) {
|
||||||
|
// Generate a corpus
|
||||||
|
for tokenCount := 0; tokenCount < 10; tokenCount++ {
|
||||||
|
writer := NewWriter()
|
||||||
|
for i := 0; i < tokenCount; i++ {
|
||||||
|
_, c, data := randToken()
|
||||||
|
writer.AddSealed(c, data)
|
||||||
|
}
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
err := writer.ToCbor(buf)
|
||||||
|
require.NoError(f, err)
|
||||||
|
|
||||||
|
f.Add(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Fuzz(func(t *testing.T, data []byte) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// search for panics
|
||||||
|
_, _ = FromCbor(bytes.NewReader(data))
|
||||||
|
|
||||||
|
if time.Since(start) > 100*time.Millisecond {
|
||||||
|
panic("too long")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
|
|
||||||
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
|
// TODO: should we have a multibase to wrap the cbor? but there is no reader/write in go-multibase :-(
|
||||||
|
|
||||||
|
const currentContainerVersion = "ctn-v1"
|
||||||
|
|
||||||
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
// Writer is a token container writer. It provides a convenient way to aggregate and serialize tokens together.
|
||||||
type Writer map[cid.Cid][]byte
|
type Writer map[cid.Cid][]byte
|
||||||
|
|
||||||
@@ -43,10 +45,12 @@ func (ctn Writer) ToCarBase64(w io.Writer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ctn Writer) ToCbor(w io.Writer) error {
|
func (ctn Writer) ToCbor(w io.Writer) error {
|
||||||
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
|
node, err := qp.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
|
||||||
for _, bytes := range ctn {
|
qp.MapEntry(ma, currentContainerVersion, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
|
||||||
qp.ListEntry(la, qp.Bytes(bytes))
|
for _, bytes := range ctn {
|
||||||
}
|
qp.ListEntry(la, qp.Bytes(bytes))
|
||||||
|
}
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
132
pkg/meta/internal/crypto/aes.go
Normal file
132
pkg/meta/internal/crypto/aes.go
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeySize represents valid AES key sizes
|
||||||
|
type KeySize int
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeySize128 KeySize = 16 // AES-128
|
||||||
|
KeySize192 KeySize = 24 // AES-192
|
||||||
|
KeySize256 KeySize = 32 // AES-256 (recommended)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsValid returns true if the key size is valid for AES
|
||||||
|
func (ks KeySize) IsValid() bool {
|
||||||
|
switch ks {
|
||||||
|
case KeySize128, KeySize192, KeySize256:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrShortCipherText = errors.New("ciphertext too short")
|
||||||
|
var ErrNoEncryptionKey = errors.New("encryption key is required")
|
||||||
|
var ErrInvalidKeySize = errors.New("invalid key size: must be 16, 24, or 32 bytes")
|
||||||
|
var ErrZeroKey = errors.New("encryption key cannot be all zeros")
|
||||||
|
|
||||||
|
// GenerateKey generates a random AES key of default size KeySize256 (32 bytes).
|
||||||
|
// Returns an error if the specified size is invalid or if key generation fails.
|
||||||
|
func GenerateKey() ([]byte, error) {
|
||||||
|
return GenerateKeyWithSize(KeySize256)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeyWithSize generates a random AES key of the specified size.
|
||||||
|
// Returns an error if the specified size is invalid or if key generation fails.
|
||||||
|
func GenerateKeyWithSize(size KeySize) ([]byte, error) {
|
||||||
|
if !size.IsValid() {
|
||||||
|
return nil, ErrInvalidKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
key := make([]byte, size)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate AES key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptWithAESKey encrypts data using AES-GCM with the provided key.
|
||||||
|
// The key must be 16, 24, or 32 bytes long (for AES-128, AES-192, or AES-256).
|
||||||
|
// Returns the encrypted data with the nonce prepended, or an error if encryption fails.
|
||||||
|
func EncryptWithAESKey(data, key []byte) ([]byte, error) {
|
||||||
|
if err := validateAESKey(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptStringWithAESKey decrypts data that was encrypted with EncryptWithAESKey.
|
||||||
|
// The key must match the one used for encryption.
|
||||||
|
// Expects the input to have a prepended nonce.
|
||||||
|
// Returns the decrypted data or an error if decryption fails.
|
||||||
|
func DecryptStringWithAESKey(data, key []byte) ([]byte, error) {
|
||||||
|
if err := validateAESKey(key); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < gcm.NonceSize() {
|
||||||
|
return nil, ErrShortCipherText
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := data[:gcm.NonceSize()], data[gcm.NonceSize():]
|
||||||
|
decrypted, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAESKey(key []byte) error {
|
||||||
|
if key == nil {
|
||||||
|
return ErrNoEncryptionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if !KeySize(len(key)).IsValid() {
|
||||||
|
return ErrInvalidKeySize
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if key is all zeros
|
||||||
|
for _, b := range key {
|
||||||
|
if b != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrZeroKey
|
||||||
|
}
|
||||||
124
pkg/meta/internal/crypto/aes_test.go
Normal file
124
pkg/meta/internal/crypto/aes_test.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAESEncryption(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, 32) // generated random 32-byte key
|
||||||
|
_, errKey := rand.Read(key)
|
||||||
|
require.NoError(t, errKey)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
key []byte
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid encryption/decryption",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil key returns error",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: nil,
|
||||||
|
wantErr: ErrNoEncryptionKey,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty data",
|
||||||
|
data: []byte{},
|
||||||
|
key: key,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid key size",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: make([]byte, 31),
|
||||||
|
wantErr: ErrInvalidKeySize,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero key returns error",
|
||||||
|
data: []byte("hello world"),
|
||||||
|
key: make([]byte, 32),
|
||||||
|
wantErr: ErrZeroKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encrypted, err := EncryptWithAESKey(tt.data, tt.key)
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := DecryptStringWithAESKey(encrypted, tt.key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if tt.key == nil {
|
||||||
|
require.Equal(t, tt.data, encrypted)
|
||||||
|
require.Equal(t, tt.data, decrypted)
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, tt.data, encrypted)
|
||||||
|
require.True(t, bytes.Equal(tt.data, decrypted))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptionErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
key []byte
|
||||||
|
errMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "short ciphertext",
|
||||||
|
data: []byte("short"),
|
||||||
|
key: key,
|
||||||
|
errMsg: "ciphertext too short",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid ciphertext",
|
||||||
|
data: make([]byte, 16), // just nonce size
|
||||||
|
key: key,
|
||||||
|
errMsg: "message authentication failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing key",
|
||||||
|
data: []byte("<22>`M<><4D><EFBFBD>l\u001AIF<49>\u0012<31><32><EFBFBD>=h<>?<3F>c<EFBFBD> <20><>\u0012<31><32><EFBFBD><EFBFBD>\u001C<31>\u0018Ƽ(g"),
|
||||||
|
key: nil,
|
||||||
|
errMsg: "encryption key is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := DecryptStringWithAESKey(tt.data, tt.key)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
134
pkg/meta/meta.go
134
pkg/meta/meta.go
@@ -1,24 +1,28 @@
|
|||||||
package meta
|
package meta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
"github.com/ipld/go-ipld-prime/printer"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/meta/internal/crypto"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrUnsupported = 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.
|
// Meta is a container for meta key-value pairs in a UCAN token.
|
||||||
// This also serves as a way to construct the underlying IPLD data with minimum allocations and transformations,
|
// This also serves as a way to construct the underlying IPLD data with minimum allocations
|
||||||
// while hiding the IPLD complexity from the caller.
|
// and transformations, while hiding the IPLD complexity from the caller.
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
|
// This type must be compatible with the IPLD type represented by the IPLD
|
||||||
|
// schema { String : Any }.
|
||||||
|
|
||||||
Keys []string
|
Keys []string
|
||||||
Values map[string]ipld.Node
|
Values map[string]ipld.Node
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,21 @@ func (m *Meta) GetString(key string) (string, error) {
|
|||||||
return v.AsString()
|
return v.AsString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEncryptedString decorates GetString and decrypt its output with the given symmetric encryption key.
|
||||||
|
func (m *Meta) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||||
|
v, err := m.GetBytes(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(decrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetInt64 retrieves a value as an int64.
|
// GetInt64 retrieves a value as an int64.
|
||||||
// Returns ErrNotFound if the given key is missing.
|
// Returns ErrNotFound if the given key is missing.
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
@@ -83,6 +102,21 @@ func (m *Meta) GetBytes(key string) ([]byte, error) {
|
|||||||
return v.AsBytes()
|
return v.AsBytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetEncryptedBytes decorates GetBytes and decrypt its output with the given symmetric encryption key.
|
||||||
|
func (m *Meta) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||||
|
v, err := m.GetBytes(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := crypto.DecryptStringWithAESKey(v, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetNode retrieves a value as a raw IPLD node.
|
// GetNode retrieves a value as a raw IPLD node.
|
||||||
// Returns ErrNotFound if the given key is missing.
|
// Returns ErrNotFound if the given key is missing.
|
||||||
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
// Returns datamodel.ErrWrongKind if the value has the wrong type.
|
||||||
@@ -95,35 +129,48 @@ func (m *Meta) GetNode(key string) (ipld.Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add adds a key/value pair in the meta set.
|
// Add adds a key/value pair in the meta set.
|
||||||
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
// Accepted types for val are any CBOR compatible type, or directly IPLD values.
|
||||||
// and ipld.Node.
|
|
||||||
func (m *Meta) Add(key string, val any) error {
|
func (m *Meta) Add(key string, val any) error {
|
||||||
switch val := val.(type) {
|
if _, ok := m.Values[key]; ok {
|
||||||
case bool:
|
return fmt.Errorf("duplicate key %q", key)
|
||||||
m.Values[key] = basicnode.NewBool(val)
|
|
||||||
case string:
|
|
||||||
m.Values[key] = basicnode.NewString(val)
|
|
||||||
case int:
|
|
||||||
m.Values[key] = basicnode.NewInt(int64(val))
|
|
||||||
case int32:
|
|
||||||
m.Values[key] = basicnode.NewInt(int64(val))
|
|
||||||
case int64:
|
|
||||||
m.Values[key] = basicnode.NewInt(val)
|
|
||||||
case float32:
|
|
||||||
m.Values[key] = basicnode.NewFloat(float64(val))
|
|
||||||
case float64:
|
|
||||||
m.Values[key] = basicnode.NewFloat(val)
|
|
||||||
case []byte:
|
|
||||||
m.Values[key] = basicnode.NewBytes(val)
|
|
||||||
case datamodel.Node:
|
|
||||||
m.Values[key] = val
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node, err := literal.Any(val)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
m.Keys = append(m.Keys, key)
|
m.Keys = append(m.Keys, key)
|
||||||
|
m.Values[key] = node
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddEncrypted adds a key/value pair in the meta set.
|
||||||
|
// The value is encrypted with the given encryptionKey.
|
||||||
|
// Accepted types for the value are: string, []byte.
|
||||||
|
func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
||||||
|
var encrypted []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case string:
|
||||||
|
encrypted, err = crypto.EncryptWithAESKey([]byte(val), encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case []byte:
|
||||||
|
encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ErrNotEncryptable
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Add(key, encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
// Equals tells if two Meta hold the same key/values.
|
// Equals tells if two Meta hold the same key/values.
|
||||||
func (m *Meta) Equals(other *Meta) bool {
|
func (m *Meta) Equals(other *Meta) bool {
|
||||||
if len(m.Keys) != len(other.Keys) {
|
if len(m.Keys) != len(other.Keys) {
|
||||||
@@ -144,18 +191,19 @@ func (m *Meta) String() string {
|
|||||||
buf := strings.Builder{}
|
buf := strings.Builder{}
|
||||||
buf.WriteString("{")
|
buf.WriteString("{")
|
||||||
|
|
||||||
var i int
|
|
||||||
for key, node := range m.Values {
|
for key, node := range m.Values {
|
||||||
if i > 0 {
|
buf.WriteString("\n\t")
|
||||||
buf.WriteString(", ")
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
buf.WriteString(key)
|
buf.WriteString(key)
|
||||||
buf.WriteString(":")
|
buf.WriteString(": ")
|
||||||
buf.WriteString(printer.Sprint(node))
|
buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t"))
|
||||||
|
buf.WriteString(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(m.Values) > 0 {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
buf.WriteString("}")
|
buf.WriteString("}")
|
||||||
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,15 +211,3 @@ func (m *Meta) String() string {
|
|||||||
func (m *Meta) ReadOnly() ReadOnly {
|
func (m *Meta) ReadOnly() ReadOnly {
|
||||||
return ReadOnly{m: m}
|
return ReadOnly{m: m}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fqtn(val any) string {
|
|
||||||
var name string
|
|
||||||
|
|
||||||
t := reflect.TypeOf(val)
|
|
||||||
for t.Kind() == reflect.Pointer {
|
|
||||||
name += "*"
|
|
||||||
t = t.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
return name + t.PkgPath() + "." + t.Name()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package meta_test
|
package meta_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
)
|
)
|
||||||
@@ -14,11 +14,64 @@ func TestMeta_Add(t *testing.T) {
|
|||||||
|
|
||||||
type Unsupported struct{}
|
type Unsupported struct{}
|
||||||
|
|
||||||
t.Run("error if not primative or Node", func(t *testing.T) {
|
t.Run("error if not primitive or Node", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
||||||
require.ErrorIs(t, err, meta.ErrUnsupported)
|
require.Error(t, err)
|
||||||
assert.ErrorContains(t, err, "*github.com/ucan-wg/go-ucan/pkg/meta_test.Unsupported")
|
})
|
||||||
|
|
||||||
|
t.Run("encrypted meta", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := meta.NewMeta()
|
||||||
|
|
||||||
|
// string encryption
|
||||||
|
err = m.AddEncrypted("secret", "hello world", key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = m.GetString("secret")
|
||||||
|
require.Error(t, err) // the ciphertext is saved as []byte instead of string
|
||||||
|
|
||||||
|
decrypted, err := m.GetEncryptedString("secret", key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "hello world", decrypted)
|
||||||
|
|
||||||
|
// bytes encryption
|
||||||
|
originalBytes := make([]byte, 128)
|
||||||
|
_, err = rand.Read(originalBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = m.AddEncrypted("secret-bytes", originalBytes, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encryptedBytes, err := m.GetBytes("secret-bytes")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, originalBytes, encryptedBytes)
|
||||||
|
|
||||||
|
decryptedBytes, err := m.GetEncryptedBytes("secret-bytes", key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, originalBytes, decryptedBytes)
|
||||||
|
|
||||||
|
// error cases
|
||||||
|
t.Run("error on unsupported type", func(t *testing.T) {
|
||||||
|
err := m.AddEncrypted("invalid", 123, key)
|
||||||
|
require.ErrorIs(t, err, meta.ErrNotEncryptable)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on invalid key size", func(t *testing.T) {
|
||||||
|
err := m.AddEncrypted("invalid", "test", []byte("short-key"))
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "invalid key size")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("error on nil key", func(t *testing.T) {
|
||||||
|
err := m.AddEncrypted("invalid", "test", nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "encryption key is required")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ func (r ReadOnly) GetString(key string) (string, error) {
|
|||||||
return r.m.GetString(key)
|
return r.m.GetString(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetEncryptedString(key string, encryptionKey []byte) (string, error) {
|
||||||
|
return r.m.GetEncryptedString(key, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
func (r ReadOnly) GetInt64(key string) (int64, error) {
|
||||||
return r.m.GetInt64(key)
|
return r.m.GetInt64(key)
|
||||||
}
|
}
|
||||||
@@ -29,6 +33,10 @@ func (r ReadOnly) GetBytes(key string) ([]byte, error) {
|
|||||||
return r.m.GetBytes(key)
|
return r.m.GetBytes(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r ReadOnly) GetEncryptedBytes(key string, encryptionKey []byte) ([]byte, error) {
|
||||||
|
return r.m.GetEncryptedBytes(key, encryptionKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
func (r ReadOnly) GetNode(key string) (ipld.Node, error) {
|
||||||
return r.m.GetNode(key)
|
return r.m.GetNode(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package literal
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
@@ -33,8 +34,14 @@ func Null() ipld.Node {
|
|||||||
// Map creates an IPLD node from a map[string]any
|
// Map creates an IPLD node from a map[string]any
|
||||||
func Map[T any](m map[string]T) (ipld.Node, error) {
|
func Map[T any](m map[string]T) (ipld.Node, error) {
|
||||||
return qp.BuildMap(basicnode.Prototype.Any, int64(len(m)), func(ma datamodel.MapAssembler) {
|
return qp.BuildMap(basicnode.Prototype.Any, int64(len(m)), func(ma datamodel.MapAssembler) {
|
||||||
for k, v := range m {
|
// deterministic iteration
|
||||||
qp.MapEntry(ma, k, anyAssemble(v))
|
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 +55,64 @@ 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) {
|
||||||
|
// TODO: handle uint overflow below
|
||||||
|
|
||||||
|
// some fast path
|
||||||
|
switch val := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return basicnode.NewBool(val), nil
|
||||||
|
case string:
|
||||||
|
return basicnode.NewString(val), nil
|
||||||
|
case int:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int8:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int16:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int32:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case int64:
|
||||||
|
return basicnode.NewInt(val), nil
|
||||||
|
case uint:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint8:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint16:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint32:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case uint64:
|
||||||
|
return basicnode.NewInt(int64(val)), nil
|
||||||
|
case float32:
|
||||||
|
return basicnode.NewFloat(float64(val)), nil
|
||||||
|
case float64:
|
||||||
|
return basicnode.NewFloat(val), nil
|
||||||
|
case []byte:
|
||||||
|
return basicnode.NewBytes(val), nil
|
||||||
|
case datamodel.Node:
|
||||||
|
return val, nil
|
||||||
|
case cid.Cid:
|
||||||
|
return LinkCid(val), nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := basicnode.Prototype__Any{}.NewBuilder()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%v", r)
|
||||||
|
res = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
anyAssemble(v)(builder)
|
||||||
|
|
||||||
|
return builder.Build(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func anyAssemble(val any) qp.Assemble {
|
func anyAssemble(val any) qp.Assemble {
|
||||||
var rt reflect.Type
|
var rt reflect.Type
|
||||||
var rv reflect.Value
|
var rv reflect.Value
|
||||||
@@ -90,10 +155,14 @@ func anyAssemble(val any) qp.Assemble {
|
|||||||
if rt.Key().Kind() != reflect.String {
|
if rt.Key().Kind() != reflect.String {
|
||||||
break
|
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) {
|
return qp.Map(int64(rv.Len()), func(ma datamodel.MapAssembler) {
|
||||||
for it.Next() {
|
for _, key := range keys {
|
||||||
qp.MapEntry(ma, it.Key().String(), anyAssemble(it.Value()))
|
qp.MapEntry(ma, key.String(), anyAssemble(rv.MapIndex(key)))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
@@ -106,6 +175,11 @@ func anyAssemble(val any) qp.Assemble {
|
|||||||
return qp.Float(rv.Float())
|
return qp.Float(rv.Float())
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
return qp.String(rv.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:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package literal
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
"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/ipld/go-ipld-prime/printer"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -53,6 +55,7 @@ func TestMap(t *testing.T) {
|
|||||||
"barbar": "foo",
|
"barbar": "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -115,6 +118,140 @@ func TestMap(t *testing.T) {
|
|||||||
string{"barbar"}: string{"foo"}
|
string{"barbar"}: string{"foo"}
|
||||||
}
|
}
|
||||||
}`, printer.Sprint(v))
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = n.LookupByString("link")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
||||||
|
asLink, err := v.AsLink()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAny(t *testing.T) {
|
||||||
|
data := map[string]any{
|
||||||
|
"bool": true,
|
||||||
|
"string": "foobar",
|
||||||
|
"bytes": []byte{1, 2, 3, 4},
|
||||||
|
"int": 1234,
|
||||||
|
"uint": uint(12345),
|
||||||
|
"float": 1.45,
|
||||||
|
"slice": []int{1, 2, 3},
|
||||||
|
"array": [2]int{1, 2},
|
||||||
|
"map": map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"foofoo": map[string]string{
|
||||||
|
"barbar": "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
||||||
|
"func": func() {},
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := Any(data["bool"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Bool, v.Kind())
|
||||||
|
require.Equal(t, true, must(v.AsBool()))
|
||||||
|
|
||||||
|
v, err = Any(data["string"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_String, v.Kind())
|
||||||
|
require.Equal(t, "foobar", must(v.AsString()))
|
||||||
|
|
||||||
|
v, err = Any(data["bytes"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Bytes, v.Kind())
|
||||||
|
require.Equal(t, []byte{1, 2, 3, 4}, must(v.AsBytes()))
|
||||||
|
|
||||||
|
v, err = Any(data["int"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||||
|
require.Equal(t, int64(1234), must(v.AsInt()))
|
||||||
|
|
||||||
|
v, err = Any(data["uint"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Int, v.Kind())
|
||||||
|
require.Equal(t, int64(12345), must(v.AsInt()))
|
||||||
|
|
||||||
|
v, err = Any(data["float"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Float, v.Kind())
|
||||||
|
require.Equal(t, 1.45, must(v.AsFloat()))
|
||||||
|
|
||||||
|
v, err = Any(data["slice"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||||
|
require.Equal(t, int64(3), v.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
2: int{3}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = Any(data["array"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_List, v.Kind())
|
||||||
|
require.Equal(t, int64(2), v.Length())
|
||||||
|
require.Equal(t, `list{
|
||||||
|
0: int{1}
|
||||||
|
1: int{2}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = Any(data["map"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Map, v.Kind())
|
||||||
|
require.Equal(t, int64(2), v.Length())
|
||||||
|
require.Equal(t, `map{
|
||||||
|
string{"foo"}: string{"bar"}
|
||||||
|
string{"foofoo"}: map{
|
||||||
|
string{"barbar"}: string{"foo"}
|
||||||
|
}
|
||||||
|
}`, printer.Sprint(v))
|
||||||
|
|
||||||
|
v, err = Any(data["link"])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, datamodel.Kind_Link, v.Kind())
|
||||||
|
asLink, err := v.AsLink()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, asLink.(cidlink.Link).Equals(cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm")))
|
||||||
|
|
||||||
|
v, err = Any(data["func"])
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkAny(b *testing.B) {
|
||||||
|
b.Run("bool", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("string", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any("foobar")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("bytes", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any([]byte{1, 2, 3, 4})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("map", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
_, _ = Any(map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
"foofoo": map[string]string{
|
||||||
|
"barbar": "foo",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func must[T any](t T, err error) T {
|
func must[T any](t T, err error) T {
|
||||||
|
|||||||
@@ -9,21 +9,19 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MatchTrace, if set, will print tracing statements to stdout of the policy matching resolution.
|
|
||||||
var MatchTrace = false
|
|
||||||
|
|
||||||
// Match determines if the IPLD node satisfies the policy.
|
// Match determines if the IPLD node satisfies the policy.
|
||||||
func (p Policy) Match(node datamodel.Node) bool {
|
// The first Statement failing to match is returned as well.
|
||||||
|
func (p Policy) Match(node datamodel.Node) (bool, Statement) {
|
||||||
for _, stmt := range p {
|
for _, stmt := range p {
|
||||||
res, _ := matchStatement(stmt, node)
|
res, leaf := matchStatement(stmt, node)
|
||||||
switch res {
|
switch res {
|
||||||
case matchResultNoData, matchResultFalse:
|
case matchResultNoData, matchResultFalse:
|
||||||
return false
|
return false, leaf
|
||||||
case matchResultOptionalNoData, matchResultTrue:
|
case matchResultOptionalNoData, matchResultTrue:
|
||||||
// continue
|
// continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
|
// PartialMatch returns false IIF one non-optional Statement has the corresponding data and doesn't match.
|
||||||
@@ -62,7 +60,7 @@ const (
|
|||||||
// - matchResultNoData: if the selector didn't match the expected data.
|
// - matchResultNoData: if the selector didn't match the expected data.
|
||||||
// For matchResultTrue and matchResultNoData, the leaf-most (innermost) statement failing to be true is returned,
|
// For matchResultTrue and matchResultNoData, the leaf-most (innermost) statement failing to be true is returned,
|
||||||
// as well as the corresponding root-most encompassing statement.
|
// as well as the corresponding root-most encompassing statement.
|
||||||
func matchStatement(cur Statement, node ipld.Node) (output matchResult, leafMost Statement) {
|
func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Statement) {
|
||||||
var boolToRes = func(v bool) (matchResult, Statement) {
|
var boolToRes = func(v bool) (matchResult, Statement) {
|
||||||
if v {
|
if v {
|
||||||
return matchResultTrue, nil
|
return matchResultTrue, nil
|
||||||
@@ -70,11 +68,6 @@ func matchStatement(cur Statement, node ipld.Node) (output matchResult, leafMost
|
|||||||
return matchResultFalse, cur
|
return matchResultFalse, cur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if MatchTrace {
|
|
||||||
defer func() {
|
|
||||||
fmt.Printf("match %v --> %v\n", cur, matchResToStr(output))
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cur.Kind() {
|
switch cur.Kind() {
|
||||||
case KindEqual:
|
case KindEqual:
|
||||||
@@ -139,9 +132,9 @@ func matchStatement(cur Statement, node ipld.Node) (output matchResult, leafMost
|
|||||||
case matchResultNoData, matchResultOptionalNoData:
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
return res, leaf
|
return res, leaf
|
||||||
case matchResultTrue:
|
case matchResultTrue:
|
||||||
return matchResultFalse, leaf
|
return matchResultFalse, cur
|
||||||
case matchResultFalse:
|
case matchResultFalse:
|
||||||
return matchResultTrue, leaf
|
return matchResultTrue, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case KindAnd:
|
case KindAnd:
|
||||||
@@ -282,18 +275,3 @@ func gt(order int) bool { return order == 1 }
|
|||||||
func gte(order int) bool { return order == 0 || order == 1 }
|
func gte(order int) bool { return order == 0 || order == 1 }
|
||||||
func lt(order int) bool { return order == -1 }
|
func lt(order int) bool { return order == -1 }
|
||||||
func lte(order int) bool { return order == 0 || order == -1 }
|
func lte(order int) bool { return order == 0 || order == -1 }
|
||||||
|
|
||||||
func matchResToStr(res matchResult) string {
|
|
||||||
switch res {
|
|
||||||
case matchResultTrue:
|
|
||||||
return "True"
|
|
||||||
case matchResultFalse:
|
|
||||||
return "False"
|
|
||||||
case matchResultNoData:
|
|
||||||
return "NoData"
|
|
||||||
case matchResultOptionalNoData:
|
|
||||||
return "OptionalNoData"
|
|
||||||
default:
|
|
||||||
panic("invalid matchResult")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import (
|
|||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
|
"github.com/ipld/go-ipld-prime/fluent/qp"
|
||||||
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
||||||
"github.com/ipld/go-ipld-prime/node/basicnode"
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
@@ -17,228 +20,252 @@ import (
|
|||||||
func TestMatch(t *testing.T) {
|
func TestMatch(t *testing.T) {
|
||||||
t.Run("equality", func(t *testing.T) {
|
t.Run("equality", func(t *testing.T) {
|
||||||
t.Run("string", func(t *testing.T) {
|
t.Run("string", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.String
|
nd := literal.String("test")
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignString("test")
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.String("test")))
|
pol := MustConstruct(Equal(".", literal.String("test")))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("test2")))
|
pol = MustConstruct(Equal(".", literal.String("test2")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Int(138)))
|
pol = MustConstruct(Equal(".", literal.Int(138)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("int", func(t *testing.T) {
|
t.Run("int", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Int(138)))
|
pol := MustConstruct(Equal(".", literal.Int(138)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
pol = MustConstruct(Equal(".", literal.Int(1138)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("float", func(t *testing.T) {
|
t.Run("float", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Float
|
nd := literal.Float(1.138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignFloat(1.138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", 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.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
pol = MustConstruct(Equal(".", literal.Float(11.38)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("138")))
|
pol = MustConstruct(Equal(".", literal.String("138")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("IPLD Link", func(t *testing.T) {
|
t.Run("IPLD Link", func(t *testing.T) {
|
||||||
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
l0 := cidlink.Link{Cid: cid.MustParse("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")}
|
||||||
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
l1 := cidlink.Link{Cid: cid.MustParse("bafkreifau35r7vi37tvbvfy3hdwvgb4tlflqf7zcdzeujqcjk3rsphiwte")}
|
||||||
|
|
||||||
np := basicnode.Prototype.Link
|
nd := literal.Link(l0)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignLink(l0)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
pol = MustConstruct(Equal(".", literal.Link(l1)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
pol = MustConstruct(Equal(".", literal.String("bafybeif4owy5gno5lwnixqm52rwqfodklf76hsetxdhffuxnplvijskzqq")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("string in map", func(t *testing.T) {
|
t.Run("string in map", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Map
|
nd, _ := literal.Map(map[string]any{
|
||||||
nb := np.NewBuilder()
|
"foo": "bar",
|
||||||
ma, _ := nb.BeginMap(1)
|
})
|
||||||
ma.AssembleKey().AssignString("foo")
|
|
||||||
ma.AssembleValue().AssignString("bar")
|
|
||||||
ma.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
pol := MustConstruct(Equal(".foo", literal.String("bar")))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
pol = MustConstruct(Equal(".[\"foo\"]", literal.String("bar")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
pol = MustConstruct(Equal(".foo", literal.String("baz")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
pol = MustConstruct(Equal(".foobar", literal.String("bar")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("string in list", func(t *testing.T) {
|
t.Run("string in list", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.List
|
nd, _ := literal.List([]any{"foo"})
|
||||||
nb := np.NewBuilder()
|
|
||||||
la, _ := nb.BeginList(1)
|
|
||||||
la.AssembleValue().AssignString("foo")
|
|
||||||
la.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
pol := MustConstruct(Equal(".[0]", literal.String("foo")))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
pol = MustConstruct(Equal(".[1]", literal.String("foo")))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("inequality", func(t *testing.T) {
|
t.Run("inequality", func(t *testing.T) {
|
||||||
t.Run("gt int", func(t *testing.T) {
|
t.Run("gt int", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
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) {
|
t.Run("gte int", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Int(138)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.True(t, ok)
|
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) {
|
t.Run("gt float", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Float
|
nd := literal.Float(1.38)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignFloat(1.38)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
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) {
|
t.Run("gte float", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Float
|
nd := literal.Float(1.38)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignFloat(1.38)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
pol = MustConstruct(GreaterThanOrEqual(".", literal.Float(1.38)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.True(t, ok)
|
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) {
|
t.Run("lt int", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
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) {
|
t.Run("lte int", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
pol = MustConstruct(LessThanOrEqual(".", literal.Int(138)))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
|
pol = MustConstruct(LessThanOrEqual(".", literal.Int(100)))
|
||||||
|
ok, leaf = pol.Match(nd)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("negation", func(t *testing.T) {
|
t.Run("negation", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Bool
|
nd := literal.Bool(false)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignBool(false)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
pol = MustConstruct(Not(Equal(".", literal.Bool(false))))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("conjunction", func(t *testing.T) {
|
t.Run("conjunction", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(
|
pol := MustConstruct(
|
||||||
And(
|
And(
|
||||||
@@ -246,8 +273,9 @@ func TestMatch(t *testing.T) {
|
|||||||
LessThan(".", literal.Int(1138)),
|
LessThan(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(
|
pol = MustConstruct(
|
||||||
And(
|
And(
|
||||||
@@ -255,19 +283,18 @@ func TestMatch(t *testing.T) {
|
|||||||
Equal(".", literal.Int(1138)),
|
Equal(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, MustConstruct(Equal(".", literal.Int(1138)))[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(And())
|
pol = MustConstruct(And())
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("disjunction", func(t *testing.T) {
|
t.Run("disjunction", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.Int
|
nd := literal.Int(138)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignInt(138)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(
|
pol := MustConstruct(
|
||||||
Or(
|
Or(
|
||||||
@@ -275,8 +302,9 @@ func TestMatch(t *testing.T) {
|
|||||||
LessThan(".", literal.Int(1138)),
|
LessThan(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(
|
pol = MustConstruct(
|
||||||
Or(
|
Or(
|
||||||
@@ -284,12 +312,14 @@ func TestMatch(t *testing.T) {
|
|||||||
Equal(".", literal.Int(1138)),
|
Equal(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Or())
|
pol = MustConstruct(Or())
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("wildcard", func(t *testing.T) {
|
t.Run("wildcard", func(t *testing.T) {
|
||||||
@@ -303,14 +333,12 @@ func TestMatch(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
func(s string) {
|
func(s string) {
|
||||||
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
t.Run(fmt.Sprintf("pass %s", s), func(t *testing.T) {
|
||||||
np := basicnode.Prototype.String
|
nd := literal.String(s)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignString(s)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Like(".", pattern))
|
pol := MustConstruct(Like(".", pattern))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
})
|
})
|
||||||
}(s)
|
}(s)
|
||||||
}
|
}
|
||||||
@@ -324,70 +352,56 @@ func TestMatch(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
func(s string) {
|
func(s string) {
|
||||||
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
t.Run(fmt.Sprintf("fail %s", s), func(t *testing.T) {
|
||||||
np := basicnode.Prototype.String
|
nd := literal.String(s)
|
||||||
nb := np.NewBuilder()
|
|
||||||
nb.AssignString(s)
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Like(".", pattern))
|
pol := MustConstruct(Like(".", pattern))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
}(s)
|
}(s)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("quantification", func(t *testing.T) {
|
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) {
|
t.Run("all", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.List
|
nd, _ := literal.List([]any{
|
||||||
nb := np.NewBuilder()
|
map[string]int{"value": 5},
|
||||||
la, _ := nb.BeginList(5)
|
map[string]int{"value": 10},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
map[string]int{"value": 20},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
map[string]int{"value": 50},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
map[string]int{"value": 100},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
})
|
||||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
|
||||||
la.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
pol := MustConstruct(All(".[]", GreaterThan(".value", literal.Int(2))))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
pol = MustConstruct(All(".[]", GreaterThan(".value", literal.Int(20))))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, MustConstruct(GreaterThan(".value", literal.Int(20)))[0], leaf)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("any", func(t *testing.T) {
|
t.Run("any", func(t *testing.T) {
|
||||||
np := basicnode.Prototype.List
|
nd, _ := literal.List([]any{
|
||||||
nb := np.NewBuilder()
|
map[string]int{"value": 5},
|
||||||
la, _ := nb.BeginList(5)
|
map[string]int{"value": 10},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(5))
|
map[string]int{"value": 20},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(10))
|
map[string]int{"value": 50},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(20))
|
map[string]int{"value": 100},
|
||||||
la.AssembleValue().AssignNode(buildValueNode(50))
|
})
|
||||||
la.AssembleValue().AssignNode(buildValueNode(100))
|
|
||||||
la.Finish()
|
|
||||||
nd := nb.Build()
|
|
||||||
|
|
||||||
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
pol := MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(60))))
|
||||||
ok := pol.Match(nd)
|
ok, leaf := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
require.Nil(t, leaf)
|
||||||
|
|
||||||
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
pol = MustConstruct(Any(".[]", GreaterThan(".value", literal.Int(100))))
|
||||||
ok = pol.Match(nd)
|
ok, leaf = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
|
require.Equal(t, pol[0], leaf)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -405,7 +419,8 @@ func TestPolicyExamples(t *testing.T) {
|
|||||||
|
|
||||||
pol, err := FromDagJson(policy)
|
pol, err := FromDagJson(policy)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
return pol.Match(data)
|
res, _ := pol.Match(data)
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("And", func(t *testing.T) {
|
t.Run("And", func(t *testing.T) {
|
||||||
@@ -509,7 +524,7 @@ func FuzzMatch(f *testing.F) {
|
|||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
policy.Match(dataNode)
|
_, _ = policy.Match(dataNode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +599,7 @@ func TestOptionalSelectors(t *testing.T) {
|
|||||||
err = nb.AssignNode(n)
|
err = nb.AssignNode(n)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result := tt.policy.Match(nb.Build())
|
result, _ := tt.policy.Match(nb.Build())
|
||||||
require.Equal(t, tt.expected, result)
|
require.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -889,3 +904,55 @@ func TestPartialMatch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
pol := MustConstruct(
|
||||||
|
Equal(".from", literal.String("alice@example.com")),
|
||||||
|
Any(".to", Like(".", "*@example.com")),
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("with passing args", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||||
|
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
||||||
|
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.String("bob@example.com"))
|
||||||
|
qp.ListEntry(la, qp.String("carol@not.example.com"))
|
||||||
|
}))
|
||||||
|
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
||||||
|
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
exec, stmt := pol.Match(argsNode)
|
||||||
|
assert.True(t, exec)
|
||||||
|
assert.Nil(t, stmt)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails on recipients (second statement)", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
argsNode, err := qp.BuildMap(basicnode.Prototype.Any, 2, func(ma datamodel.MapAssembler) {
|
||||||
|
qp.MapEntry(ma, "from", qp.String("alice@example.com"))
|
||||||
|
qp.MapEntry(ma, "to", qp.List(2, func(la datamodel.ListAssembler) {
|
||||||
|
qp.ListEntry(la, qp.String("bob@null.com"))
|
||||||
|
qp.ListEntry(la, qp.String("carol@elsewhere.example.com"))
|
||||||
|
}))
|
||||||
|
qp.MapEntry(ma, "title", qp.String("Coffee"))
|
||||||
|
qp.MapEntry(ma, "body", qp.String("Still on for coffee"))
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
exec, stmt := pol.Match(argsNode)
|
||||||
|
assert.False(t, exec)
|
||||||
|
assert.NotNil(t, stmt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,37 @@ func ExamplePolicy() {
|
|||||||
// ]
|
// ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExamplePolicy_accumulate() {
|
||||||
|
var statements []policy.Constructor
|
||||||
|
|
||||||
|
statements = append(statements, policy.Equal(".status", literal.String("draft")))
|
||||||
|
|
||||||
|
statements = append(statements, policy.All(".reviewer",
|
||||||
|
policy.Like(".email", "*@example.com"),
|
||||||
|
))
|
||||||
|
|
||||||
|
statements = append(statements, policy.Any(".tags", policy.Or(
|
||||||
|
policy.Equal(".", literal.String("news")),
|
||||||
|
policy.Equal(".", literal.String("press")),
|
||||||
|
)))
|
||||||
|
|
||||||
|
pol := policy.MustConstruct(statements...)
|
||||||
|
|
||||||
|
fmt.Println(pol)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// [
|
||||||
|
// ["==", ".status", "draft"],
|
||||||
|
// ["all", ".reviewer",
|
||||||
|
// ["like", ".email", "*@example.com"]],
|
||||||
|
// ["any", ".tags",
|
||||||
|
// ["or", [
|
||||||
|
// ["==", ".", "news"],
|
||||||
|
// ["==", ".", "press"]]]
|
||||||
|
// ]
|
||||||
|
// ]
|
||||||
|
}
|
||||||
|
|
||||||
func TestConstruct(t *testing.T) {
|
func TestConstruct(t *testing.T) {
|
||||||
pol, err := policy.Construct(
|
pol, err := policy.Construct(
|
||||||
policy.Equal(".status", literal.String("draft")),
|
policy.Equal(".status", literal.String("draft")),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package selector
|
package selector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -354,7 +353,6 @@ func TestParse(t *testing.T) {
|
|||||||
str := `.foo.["bar"].[138]?.baz[1:]`
|
str := `.foo.["bar"].[138]?.baz[1:]`
|
||||||
sel, err := Parse(str)
|
sel, err := Parse(str)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
printSegments(sel)
|
|
||||||
require.Equal(t, str, sel.String())
|
require.Equal(t, str, sel.String())
|
||||||
require.Equal(t, 7, len(sel))
|
require.Equal(t, 7, len(sel))
|
||||||
require.False(t, sel[0].Identity())
|
require.False(t, sel[0].Identity())
|
||||||
@@ -404,13 +402,11 @@ func TestParse(t *testing.T) {
|
|||||||
t.Run("non dotted", func(t *testing.T) {
|
t.Run("non dotted", func(t *testing.T) {
|
||||||
_, err := Parse("foo")
|
_, err := Parse("foo")
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
fmt.Println(err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("non quoted", func(t *testing.T) {
|
t.Run("non quoted", func(t *testing.T) {
|
||||||
_, err := Parse(".[foo]")
|
_, err := Parse(".[foo]")
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
fmt.Println(err)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("slice with negative start and positive end", func(t *testing.T) {
|
t.Run("slice with negative start and positive end", func(t *testing.T) {
|
||||||
@@ -554,9 +550,3 @@ func TestParse(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSegments(s Selector) {
|
|
||||||
for i, seg := range s {
|
|
||||||
fmt.Printf("%d: %s\n", i, seg.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package selector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime/must"
|
"github.com/ipld/go-ipld-prime/must"
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
"github.com/ipld/go-ipld-prime/printer"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -87,8 +85,6 @@ func TestSelect(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, res)
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(res))
|
|
||||||
|
|
||||||
age := must.Int(must.Node(res.LookupByString("age")))
|
age := must.Int(must.Node(res.LookupByString("age")))
|
||||||
require.Equal(t, int64(alice.Age), age)
|
require.Equal(t, int64(alice.Age), age)
|
||||||
})
|
})
|
||||||
@@ -101,8 +97,6 @@ func TestSelect(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, res)
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(res))
|
|
||||||
|
|
||||||
name := must.String(res)
|
name := must.String(res)
|
||||||
require.Equal(t, alice.Name.First, name)
|
require.Equal(t, alice.Name.First, name)
|
||||||
|
|
||||||
@@ -110,8 +104,6 @@ func TestSelect(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, res)
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(res))
|
|
||||||
|
|
||||||
name = must.String(res)
|
name = must.String(res)
|
||||||
require.Equal(t, bob.Name.First, name)
|
require.Equal(t, bob.Name.First, name)
|
||||||
})
|
})
|
||||||
@@ -124,8 +116,6 @@ func TestSelect(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, res)
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(res))
|
|
||||||
|
|
||||||
name := must.String(res)
|
name := must.String(res)
|
||||||
require.Equal(t, *alice.Name.Middle, name)
|
require.Equal(t, *alice.Name.Middle, name)
|
||||||
|
|
||||||
@@ -142,8 +132,6 @@ func TestSelect(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Empty(t, res)
|
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")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -164,8 +152,6 @@ func TestSelect(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotEmpty(t, res)
|
require.NotEmpty(t, res)
|
||||||
|
|
||||||
fmt.Println(printer.Sprint(res))
|
|
||||||
|
|
||||||
iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
|
iname := must.String(must.Node(must.Node(res.LookupByIndex(0)).LookupByString("name")))
|
||||||
require.Equal(t, alice.Interests[0].Name, iname)
|
require.Equal(t, alice.Interests[0].Name, iname)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
package selector_test
|
package selector_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
"github.com/ipld/go-ipld-prime/datamodel"
|
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
@@ -55,7 +52,7 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NotNil(t, res)
|
require.NotNil(t, res)
|
||||||
|
|
||||||
exp := makeNode(t, tc.Output)
|
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 {
|
func makeNode(t *testing.T, dagJsonInput string) ipld.Node {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,16 @@ package delegation
|
|||||||
// TODO: change the "delegation" link above when the specification is merged
|
// TODO: change the "delegation" link above when the specification is merged
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/nonce"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/parse"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Token is an immutable type that holds the fields of a UCAN delegation.
|
// Token is an immutable type that holds the fields of a UCAN delegation.
|
||||||
@@ -47,15 +46,10 @@ type Token struct {
|
|||||||
|
|
||||||
// New creates a validated Token from the provided parameters and options.
|
// New creates a validated Token from the provided parameters and options.
|
||||||
//
|
//
|
||||||
// When creating a delegated token, the Issuer's (iss) DID is assembed
|
// When creating a delegated token, the Issuer's (iss) DID is assembled
|
||||||
// using the public key associated with the private key sent as the first
|
// using the public key associated with the private key sent as the first
|
||||||
// parameter.
|
// parameter.
|
||||||
func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
iss, err := did.FromPrivKey(privKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tkn := &Token{
|
tkn := &Token{
|
||||||
issuer: iss,
|
issuer: iss,
|
||||||
audience: aud,
|
audience: aud,
|
||||||
@@ -72,8 +66,9 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if len(tkn.nonce) == 0 {
|
if len(tkn.nonce) == 0 {
|
||||||
tkn.nonce, err = generateNonce()
|
tkn.nonce, err = nonce.Generate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -92,15 +87,10 @@ func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Po
|
|||||||
// When creating a root token, both the Issuer's (iss) and Subject's
|
// When creating a root token, both the Issuer's (iss) and Subject's
|
||||||
// (sub) DIDs are assembled from the public key associated with the
|
// (sub) DIDs are assembled from the public key associated with the
|
||||||
// private key passed as the first argument.
|
// private key passed as the first argument.
|
||||||
func Root(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
func Root(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
sub, err := did.FromPrivKey(privKey)
|
opts = append(opts, WithSubject(iss))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, WithSubject(sub))
|
return New(iss, aud, cmd, pol, opts...)
|
||||||
|
|
||||||
return New(privKey, aud, cmd, pol, opts...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
// Issuer returns the did.DID representing the Token's issuer.
|
||||||
@@ -152,6 +142,24 @@ func (t *Token) Expiration() *time.Time {
|
|||||||
return t.expiration
|
return t.expiration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
func (t *Token) IsValidNow() bool {
|
||||||
|
return t.IsValidAt(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
func (t *Token) IsValidAt(ti time.Time) bool {
|
||||||
|
if t.expiration != nil && ti.After(*t.expiration) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if t.notBefore != nil && ti.Before(*t.notBefore) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Token) validate() error {
|
func (t *Token) validate() error {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
@@ -184,27 +192,19 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|||||||
return nil, fmt.Errorf("parse iss: %w", err)
|
return nil, fmt.Errorf("parse iss: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tkn.audience, err = did.Parse(m.Aud)
|
if tkn.audience, err = did.Parse(m.Aud); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse audience: %w", err)
|
return nil, fmt.Errorf("parse audience: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Sub != nil {
|
if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
|
||||||
tkn.subject, err = did.Parse(*m.Sub)
|
return nil, fmt.Errorf("parse subject: %w", err)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse subject: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tkn.subject = did.Undef
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tkn.command, err = command.Parse(m.Cmd)
|
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse command: %w", err)
|
return nil, fmt.Errorf("parse command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tkn.policy, err = policy.FromIPLD(m.Pol)
|
if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("parse policy: %w", err)
|
return nil, fmt.Errorf("parse policy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,15 +215,8 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|||||||
|
|
||||||
tkn.meta = m.Meta
|
tkn.meta = m.Meta
|
||||||
|
|
||||||
if m.Nbf != nil {
|
tkn.notBefore = parse.OptionalTimestamp(m.Nbf)
|
||||||
t := time.Unix(*m.Nbf, 0)
|
tkn.expiration = parse.OptionalTimestamp(m.Exp)
|
||||||
tkn.notBefore = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Exp != nil {
|
|
||||||
t := time.Unix(*m.Exp, 0)
|
|
||||||
tkn.expiration = &t
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tkn.validate(); err != nil {
|
if err := tkn.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -231,14 +224,3 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|||||||
|
|
||||||
return &tkn, nil
|
return &tkn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateNonce creates a 12-byte random nonce.
|
|
||||||
// TODO: some crypto scheme require more, is that our case?
|
|
||||||
func generateNonce() ([]byte, error) {
|
|
||||||
res := make([]byte, 12)
|
|
||||||
_, err := rand.Read(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package delegation_test
|
package delegation_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
@@ -17,16 +17,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||||
|
|
||||||
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
subJectCmd = "/foo/bar"
|
||||||
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
|
subjectPol = `
|
||||||
|
|
||||||
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
|
|
||||||
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
|
||||||
|
|
||||||
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
|
|
||||||
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
|
||||||
subJectCmd = "/foo/bar"
|
|
||||||
subjectPol = `
|
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"==",
|
"==",
|
||||||
@@ -64,20 +56,15 @@ const (
|
|||||||
]
|
]
|
||||||
`
|
`
|
||||||
|
|
||||||
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
newCID = "zdpuAwa4qv3ncMDPeDoqVxjZy3JoyWsbqUzm94rdA1AvRFkkw"
|
||||||
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
rootCID = "zdpuAkgGmUp5JrXvehGuuw9JA8DLQKDaxtK3R8brDQQVC2i5X"
|
||||||
|
|
||||||
|
aesKey = "xQklMmNTnVrmaPBq/0pwV5fEwuv/iClF5HWak9MsgI8="
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConstructors(t *testing.T) {
|
func TestConstructors(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
privKey := privKey(t, issuerPrivKeyCfg)
|
|
||||||
|
|
||||||
aud, err := did.Parse(AudienceDID)
|
|
||||||
|
|
||||||
sub, err := did.Parse(subjectDID)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cmd, err := command.Parse(subJectCmd)
|
cmd, err := command.Parse(subJectCmd)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -88,27 +75,25 @@ func TestConstructors(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("New", func(t *testing.T) {
|
t.Run("New", func(t *testing.T) {
|
||||||
tkn, err := delegation.New(privKey, aud, cmd, pol,
|
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
delegation.WithNonce([]byte(nonce)),
|
delegation.WithNonce([]byte(nonce)),
|
||||||
delegation.WithSubject(sub),
|
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
||||||
delegation.WithExpiration(exp),
|
delegation.WithExpiration(exp),
|
||||||
delegation.WithMeta("foo", "fooo"),
|
delegation.WithMeta("foo", "fooo"),
|
||||||
delegation.WithMeta("bar", "barr"),
|
delegation.WithMeta("bar", "barr"),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(privKey)
|
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Log(string(data))
|
|
||||||
|
|
||||||
golden.Assert(t, string(data), "new.dagjson")
|
golden.Assert(t, string(data), "new.dagjson")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Root", func(t *testing.T) {
|
t.Run("Root", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
tkn, err := delegation.Root(privKey, aud, cmd, pol,
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
delegation.WithNonce([]byte(nonce)),
|
delegation.WithNonce([]byte(nonce)),
|
||||||
delegation.WithExpiration(exp),
|
delegation.WithExpiration(exp),
|
||||||
delegation.WithMeta("foo", "fooo"),
|
delegation.WithMeta("foo", "fooo"),
|
||||||
@@ -116,21 +101,109 @@ func TestConstructors(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(privKey)
|
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Log(string(data))
|
|
||||||
|
|
||||||
golden.Assert(t, string(data), "root.dagjson")
|
golden.Assert(t, string(data), "root.dagjson")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
func TestEncryptedMeta(t *testing.T) {
|
||||||
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
t.Parallel()
|
||||||
|
|
||||||
|
cmd, err := command.Parse(subJectCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
pol, err := policy.FromDagJson(subjectPol)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
encryptionKey, err := base64.StdEncoding.DecodeString(aesKey)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Len(t, encryptionKey, 32)
|
||||||
|
|
||||||
return privKey
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
key string
|
||||||
|
value string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple string",
|
||||||
|
key: "secret1",
|
||||||
|
value: "hello world",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
key: "secret2",
|
||||||
|
value: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "special characters",
|
||||||
|
key: "secret3",
|
||||||
|
value: "!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unicode characters",
|
||||||
|
key: "secret4",
|
||||||
|
value: "Hello, 世界! 👋",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
|
delegation.WithEncryptedMetaString(tt.key, tt.value, encryptionKey),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedTkn, _, err := delegation.FromSealed(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = decodedTkn.Meta().GetString(tt.key)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
decrypted, err := decodedTkn.Meta().GetEncryptedString(tt.key, encryptionKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Verify the decrypted value is equal to the original
|
||||||
|
require.Equal(t, tt.value, decrypted)
|
||||||
|
|
||||||
|
// Try to decrypt with wrong key
|
||||||
|
wrongKey := make([]byte, 32)
|
||||||
|
_, err = decodedTkn.Meta().GetEncryptedString(tt.key, wrongKey)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("multiple encrypted values in the same token", func(t *testing.T) {
|
||||||
|
values := map[string]string{
|
||||||
|
"secret1": "value1",
|
||||||
|
"secret2": "value2",
|
||||||
|
"secret3": "value3",
|
||||||
|
}
|
||||||
|
var opts []delegation.Option
|
||||||
|
for k, v := range values {
|
||||||
|
opts = append(opts, delegation.WithEncryptedMetaString(k, v, encryptionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create token with multiple encrypted values
|
||||||
|
tkn, err := delegation.New(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol, opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, err := tkn.ToDagCbor(didtest.PersonaAlice.PrivKey())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decodedTkn, _, err := delegation.FromSealed(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
decrypted, err := decodedTkn.Meta().GetEncryptedString(k, encryptionKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, v, decrypted)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
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.
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.
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.
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
|
||||||
234
token/delegation/delegationtest/generator/generator.go
Normal file
234
token/delegation/delegationtest/generator/generator.go
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dave/jennifer/jen"
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenNamePrefix = "Token"
|
||||||
|
proorChainNamePrefix = "Proof"
|
||||||
|
tokenExt = ".dagcbor"
|
||||||
|
)
|
||||||
|
|
||||||
|
var constantNonce = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b}
|
||||||
|
|
||||||
|
type newDelegationParams struct {
|
||||||
|
privKey crypto.PrivKey
|
||||||
|
aud did.DID
|
||||||
|
sub did.DID
|
||||||
|
cmd command.Command
|
||||||
|
pol policy.Policy
|
||||||
|
opts []delegation.Option
|
||||||
|
}
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
name string
|
||||||
|
id cid.Cid
|
||||||
|
}
|
||||||
|
|
||||||
|
type proof struct {
|
||||||
|
name string
|
||||||
|
prf []cid.Cid
|
||||||
|
}
|
||||||
|
|
||||||
|
type acc struct {
|
||||||
|
name string
|
||||||
|
chain []cid.Cid
|
||||||
|
}
|
||||||
|
|
||||||
|
type variant struct {
|
||||||
|
name string
|
||||||
|
variant func(*newDelegationParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
func noopVariant() variant {
|
||||||
|
return variant{
|
||||||
|
name: "",
|
||||||
|
variant: func(_ *newDelegationParams) {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type generator struct {
|
||||||
|
dlgs []token
|
||||||
|
chains []proof
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *generator) chainPersonas(personas []didtest.Persona, acc acc, vari variant) error {
|
||||||
|
acc.name += personas[0].Name()
|
||||||
|
|
||||||
|
proofName := acc.name
|
||||||
|
if len(vari.name) > 0 {
|
||||||
|
proofName += "_" + vari.name
|
||||||
|
}
|
||||||
|
g.createProofChain(proofName, acc.chain)
|
||||||
|
|
||||||
|
if len(personas) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := personas[0].Name() + personas[1].Name()
|
||||||
|
|
||||||
|
params := newDelegationParams{
|
||||||
|
privKey: personas[0].PrivKey(),
|
||||||
|
aud: personas[1].DID(),
|
||||||
|
cmd: delegationtest.NominalCommand,
|
||||||
|
pol: policy.Policy{},
|
||||||
|
opts: []delegation.Option{
|
||||||
|
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
||||||
|
delegation.WithNonce(constantNonce),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create each nominal token and continue the chain
|
||||||
|
id, err := g.createDelegation(params, name, vari)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
acc.chain = append(acc.chain, id)
|
||||||
|
err = g.chainPersonas(personas[1:], acc, vari)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is Carol, create variants for each invalid and/or optional
|
||||||
|
// parameter and also continue the chain
|
||||||
|
if personas[0] == didtest.PersonaCarol {
|
||||||
|
variants := []variant{
|
||||||
|
{name: "InvalidExpandedCommand", variant: func(p *newDelegationParams) {
|
||||||
|
p.cmd = delegationtest.ExpandedCommand
|
||||||
|
}},
|
||||||
|
{name: "ValidAttenuatedCommand", variant: func(p *newDelegationParams) {
|
||||||
|
p.cmd = delegationtest.AttenuatedCommand
|
||||||
|
}},
|
||||||
|
{name: "InvalidSubject", variant: func(p *newDelegationParams) {
|
||||||
|
p.opts = append(p.opts, delegation.WithSubject(didtest.PersonaBob.DID()))
|
||||||
|
}},
|
||||||
|
{name: "InvalidExpired", variant: func(p *newDelegationParams) {
|
||||||
|
// Note: this makes the generator not deterministic
|
||||||
|
p.opts = append(p.opts, delegation.WithExpiration(time.Now().Add(time.Second)))
|
||||||
|
}},
|
||||||
|
{name: "InvalidInactive", variant: func(p *newDelegationParams) {
|
||||||
|
nbf, err := time.Parse(time.RFC3339, "2070-01-01T00:00:00Z")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
p.opts = append(p.opts, delegation.WithNotBefore(nbf))
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a branch in the recursion for each of the variants
|
||||||
|
for _, v := range variants {
|
||||||
|
id, err := g.createDelegation(params, name, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace the previous Carol token id with the one from the variant
|
||||||
|
acc.chain[len(acc.chain)-1] = id
|
||||||
|
err = g.chainPersonas(personas[1:], acc, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *generator) createDelegation(params newDelegationParams, name string, vari variant) (cid.Cid, error) {
|
||||||
|
vari.variant(¶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.opts...)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, id, err := tkn.ToSealed(params.privKey)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dlgName := tokenNamePrefix + name
|
||||||
|
if len(vari.name) > 0 {
|
||||||
|
dlgName += "_" + vari.name
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join("..", delegationtest.TokenDir, dlgName+tokenExt), data, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
g.dlgs = append(g.dlgs, token{
|
||||||
|
name: dlgName,
|
||||||
|
id: id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *generator) createProofChain(name string, prf []cid.Cid) {
|
||||||
|
if len(prf) < 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clone := make([]cid.Cid, len(prf))
|
||||||
|
copy(clone, prf)
|
||||||
|
|
||||||
|
g.chains = append(g.chains, proof{
|
||||||
|
name: proorChainNamePrefix + name,
|
||||||
|
prf: clone,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *generator) writeGoFile() error {
|
||||||
|
file := jen.NewFile("delegationtest")
|
||||||
|
file.HeaderComment("Code generated by delegationtest - DO NOT EDIT.")
|
||||||
|
|
||||||
|
refs := map[cid.Cid]string{}
|
||||||
|
|
||||||
|
for _, d := range g.dlgs {
|
||||||
|
refs[d.id] = d.name + "CID"
|
||||||
|
|
||||||
|
file.Var().Defs(
|
||||||
|
jen.Id(d.name+"CID").Op("=").Qual("github.com/ipfs/go-cid", "MustParse").Call(jen.Lit(d.id.String())),
|
||||||
|
jen.Id(d.name).Op("=").Id("mustGetDelegation").Call(jen.Id(d.name+"CID")),
|
||||||
|
)
|
||||||
|
file.Line()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range g.chains {
|
||||||
|
g := jen.CustomFunc(jen.Options{
|
||||||
|
Multi: true,
|
||||||
|
Separator: ",",
|
||||||
|
Close: "\n",
|
||||||
|
}, func(g *jen.Group) {
|
||||||
|
slices.Reverse(c.prf)
|
||||||
|
for _, p := range c.prf {
|
||||||
|
g.Id(refs[p])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
file.Var().Id(c.name).Op("=").Index().Qual("github.com/ipfs/go-cid", "Cid").Values(g)
|
||||||
|
file.Line()
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.Save("../token_gen.go")
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
112
token/delegation/delegationtest/token.go
Normal file
112
token/delegation/delegationtest/token.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package delegationtest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ExpandedCommand is the parent of the NominalCommand and represents
|
||||||
|
// the cases where the delegation proof-chain or invocation token tries
|
||||||
|
// to increase the privileges granted by the root delegation token.
|
||||||
|
// Execution of this command is generally prohibited in tests.
|
||||||
|
ExpandedCommand = command.MustParse("/expanded")
|
||||||
|
|
||||||
|
// NominalCommand is the command used for most test tokens and proof-
|
||||||
|
// chains. Execution of this command is generally allowed in tests.
|
||||||
|
NominalCommand = ExpandedCommand.Join("nominal")
|
||||||
|
|
||||||
|
// AttenuatedCommand is a sub-command of the NominalCommand. Execution
|
||||||
|
// of this command is generally allowed in tests.
|
||||||
|
AttenuatedCommand = NominalCommand.Join("attenuated")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProofEmpty provides an empty proof chain for testing purposes.
|
||||||
|
var ProofEmpty = []cid.Cid{}
|
||||||
|
|
||||||
|
const TokenDir = "data"
|
||||||
|
|
||||||
|
//go:embed data
|
||||||
|
var fs embed.FS
|
||||||
|
|
||||||
|
var _ delegation.Loader = (*delegationLoader)(nil)
|
||||||
|
|
||||||
|
type delegationLoader struct {
|
||||||
|
tokens map[cid.Cid]*delegation.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
ldr delegation.Loader
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDelegationLoader returns a singleton instance of a test
|
||||||
|
// DelegationLoader containing all the tokens present in the data/
|
||||||
|
// directory.
|
||||||
|
func GetDelegationLoader() delegation.Loader {
|
||||||
|
once.Do(func() {
|
||||||
|
var err error
|
||||||
|
ldr, err = loadDelegations()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ldr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDelegation implements invocation.DelegationLoader.
|
||||||
|
func (l *delegationLoader) GetDelegation(id cid.Cid) (*delegation.Token, error) {
|
||||||
|
tkn, ok := l.tokens[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, delegation.ErrDelegationNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDelegations() (delegation.Loader, error) {
|
||||||
|
dirEntries, err := fs.ReadDir(TokenDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkns := make(map[cid.Cid]*delegation.Token, len(dirEntries))
|
||||||
|
|
||||||
|
for _, dirEntry := range dirEntries {
|
||||||
|
data, err := fs.ReadFile(filepath.Join(TokenDir, dirEntry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkn, id, err := delegation.FromSealed(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tkns[id] = tkn
|
||||||
|
}
|
||||||
|
|
||||||
|
return &delegationLoader{
|
||||||
|
tokens: tkns,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDelegation is a shortcut that gets (or creates) the DelegationLoader
|
||||||
|
// and attempts to return the token referenced by the provided CID.
|
||||||
|
func GetDelegation(id cid.Cid) (*delegation.Token, error) {
|
||||||
|
return GetDelegationLoader().GetDelegation(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetDelegation(id cid.Cid) *delegation.Token {
|
||||||
|
tkn, err := GetDelegation(id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return tkn
|
||||||
|
}
|
||||||
240
token/delegation/delegationtest/token_gen.go
Normal file
240
token/delegation/delegationtest/token_gen.go
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
// Code generated by delegationtest - DO NOT EDIT.
|
||||||
|
|
||||||
|
package delegationtest
|
||||||
|
|
||||||
|
import gocid "github.com/ipfs/go-cid"
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenAliceBobCID = gocid.MustParse("bafyreicidrwvmac5lvjypucgityrtjsknojraio7ujjli4r5eyby66wjzm")
|
||||||
|
TokenAliceBob = mustGetDelegation(TokenAliceBobCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenBobCarolCID = gocid.MustParse("bafyreihxv2uhq43oxllzs2xfvxst7wtvvvl7pohb2chcz6hjvfv2ntea5u")
|
||||||
|
TokenBobCarol = mustGetDelegation(TokenBobCarolCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenCarolDanCID = gocid.MustParse("bafyreihclsgiroazq3heqdswvj2cafwqbpboicq7immo65scl7ahktpsdq")
|
||||||
|
TokenCarolDan = mustGetDelegation(TokenCarolDanCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenDanErinCID = gocid.MustParse("bafyreicja6ihewy64p3ake56xukotafjlkh4uqep2qhj52en46zzfwby3e")
|
||||||
|
TokenDanErin = mustGetDelegation(TokenDanErinCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenErinFrankCID = gocid.MustParse("bafyreicjlx3lobxm6hl5s4htd4ydwkkqeiou6rft4rnvulfdyoew565vka")
|
||||||
|
TokenErinFrank = mustGetDelegation(TokenErinFrankCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenCarolDan_InvalidExpandedCommandCID = gocid.MustParse("bafyreid3m3pk53gqgp5rlzqhvpedbwsqbidqlp4yz64vknwbzj7bxrmsr4")
|
||||||
|
TokenCarolDan_InvalidExpandedCommand = mustGetDelegation(TokenCarolDan_InvalidExpandedCommandCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenDanErin_InvalidExpandedCommandCID = gocid.MustParse("bafyreifn4sy5onwajx3kqvot5mib6m6xarzrqjozqbzgmzpmc5ox3g2uzm")
|
||||||
|
TokenDanErin_InvalidExpandedCommand = mustGetDelegation(TokenDanErin_InvalidExpandedCommandCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenErinFrank_InvalidExpandedCommandCID = gocid.MustParse("bafyreidmpgd36jznmq42bs34o4qi3fcbrsh4idkg6ejahudejzwb76fwxe")
|
||||||
|
TokenErinFrank_InvalidExpandedCommand = mustGetDelegation(TokenErinFrank_InvalidExpandedCommandCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenCarolDan_ValidAttenuatedCommandCID = gocid.MustParse("bafyreiekhtm237vyapk3c6voeb5lnz54crebqdqi3x4wn4u4cbrrhzsqfe")
|
||||||
|
TokenCarolDan_ValidAttenuatedCommand = mustGetDelegation(TokenCarolDan_ValidAttenuatedCommandCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenDanErin_ValidAttenuatedCommandCID = gocid.MustParse("bafyreicrvzqferyy7rgo75l5rn6r2nl7zyeexxjmu3dm4ff7rn2coblj4y")
|
||||||
|
TokenDanErin_ValidAttenuatedCommand = mustGetDelegation(TokenDanErin_ValidAttenuatedCommandCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenErinFrank_ValidAttenuatedCommandCID = gocid.MustParse("bafyreie6fhspk53kplcc2phla3e7z7fzldlbmmpuwk6nbow5q6s2zjmw2q")
|
||||||
|
TokenErinFrank_ValidAttenuatedCommand = mustGetDelegation(TokenErinFrank_ValidAttenuatedCommandCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenCarolDan_InvalidSubjectCID = gocid.MustParse("bafyreifgksz6756if42tnc6rqsnbaa2u3fdrveo7ek44lnj2d64d5sw26u")
|
||||||
|
TokenCarolDan_InvalidSubject = mustGetDelegation(TokenCarolDan_InvalidSubjectCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenDanErin_InvalidSubjectCID = gocid.MustParse("bafyreibdwew5nypsxrm4fq73wu6hw3lgwwiolj3bi33xdrbgcf3ogm6fty")
|
||||||
|
TokenDanErin_InvalidSubject = mustGetDelegation(TokenDanErin_InvalidSubjectCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenErinFrank_InvalidSubjectCID = gocid.MustParse("bafyreicr364mj3n7x4iyhcksxypelktcqkkw3ptg7ggxtqegw3p3mr6zc4")
|
||||||
|
TokenErinFrank_InvalidSubject = mustGetDelegation(TokenErinFrank_InvalidSubjectCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenCarolDan_InvalidExpiredCID = gocid.MustParse("bafyreigenypixaxvhzlry5rjnywvjyl4xvzlzxz2ui74uzys7qdhos4bbu")
|
||||||
|
TokenCarolDan_InvalidExpired = mustGetDelegation(TokenCarolDan_InvalidExpiredCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenDanErin_InvalidExpiredCID = gocid.MustParse("bafyreifvnfb7zqocpdysedcvjkb4y7tqfuziuqjhbbdoay4zg33pwpbzqi")
|
||||||
|
TokenDanErin_InvalidExpired = mustGetDelegation(TokenDanErin_InvalidExpiredCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenErinFrank_InvalidExpiredCID = gocid.MustParse("bafyreicvydzt3obkqx7krmoi3zu4tlirlksibxfks5jc7vlvjxjamv2764")
|
||||||
|
TokenErinFrank_InvalidExpired = mustGetDelegation(TokenErinFrank_InvalidExpiredCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenCarolDan_InvalidInactiveCID = gocid.MustParse("bafyreicea5y2nvlitvxijkupeavtg23i7ktjk3uejnaquguurzptiabk4u")
|
||||||
|
TokenCarolDan_InvalidInactive = mustGetDelegation(TokenCarolDan_InvalidInactiveCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenDanErin_InvalidInactiveCID = gocid.MustParse("bafyreifsgqzkmxj2vexuts3z766mwcjreiisjg2jykyzf7tbj5sclutpvq")
|
||||||
|
TokenDanErin_InvalidInactive = mustGetDelegation(TokenDanErin_InvalidInactiveCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TokenErinFrank_InvalidInactiveCID = gocid.MustParse("bafyreifbfegon24c6dndiqyktahzs65vhyasrygbw7nhsvojn6distsdre")
|
||||||
|
TokenErinFrank_InvalidInactive = mustGetDelegation(TokenErinFrank_InvalidInactiveCID)
|
||||||
|
)
|
||||||
|
|
||||||
|
var ProofAliceBob = []gocid.Cid{
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarol = []gocid.Cid{
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDan = []gocid.Cid{
|
||||||
|
TokenCarolDanCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErin = []gocid.Cid{
|
||||||
|
TokenDanErinCID,
|
||||||
|
TokenCarolDanCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErinFrank = []gocid.Cid{
|
||||||
|
TokenErinFrankCID,
|
||||||
|
TokenDanErinCID,
|
||||||
|
TokenCarolDanCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDan_InvalidExpandedCommand = []gocid.Cid{
|
||||||
|
TokenCarolDan_InvalidExpandedCommandCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErin_InvalidExpandedCommand = []gocid.Cid{
|
||||||
|
TokenDanErin_InvalidExpandedCommandCID,
|
||||||
|
TokenCarolDan_InvalidExpandedCommandCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand = []gocid.Cid{
|
||||||
|
TokenErinFrank_InvalidExpandedCommandCID,
|
||||||
|
TokenDanErin_InvalidExpandedCommandCID,
|
||||||
|
TokenCarolDan_InvalidExpandedCommandCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDan_ValidAttenuatedCommand = []gocid.Cid{
|
||||||
|
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErin_ValidAttenuatedCommand = []gocid.Cid{
|
||||||
|
TokenDanErin_ValidAttenuatedCommandCID,
|
||||||
|
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand = []gocid.Cid{
|
||||||
|
TokenErinFrank_ValidAttenuatedCommandCID,
|
||||||
|
TokenDanErin_ValidAttenuatedCommandCID,
|
||||||
|
TokenCarolDan_ValidAttenuatedCommandCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDan_InvalidSubject = []gocid.Cid{
|
||||||
|
TokenCarolDan_InvalidSubjectCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErin_InvalidSubject = []gocid.Cid{
|
||||||
|
TokenDanErin_InvalidSubjectCID,
|
||||||
|
TokenCarolDan_InvalidSubjectCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErinFrank_InvalidSubject = []gocid.Cid{
|
||||||
|
TokenErinFrank_InvalidSubjectCID,
|
||||||
|
TokenDanErin_InvalidSubjectCID,
|
||||||
|
TokenCarolDan_InvalidSubjectCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDan_InvalidExpired = []gocid.Cid{
|
||||||
|
TokenCarolDan_InvalidExpiredCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErin_InvalidExpired = []gocid.Cid{
|
||||||
|
TokenDanErin_InvalidExpiredCID,
|
||||||
|
TokenCarolDan_InvalidExpiredCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErinFrank_InvalidExpired = []gocid.Cid{
|
||||||
|
TokenErinFrank_InvalidExpiredCID,
|
||||||
|
TokenDanErin_InvalidExpiredCID,
|
||||||
|
TokenCarolDan_InvalidExpiredCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDan_InvalidInactive = []gocid.Cid{
|
||||||
|
TokenCarolDan_InvalidInactiveCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErin_InvalidInactive = []gocid.Cid{
|
||||||
|
TokenDanErin_InvalidInactiveCID,
|
||||||
|
TokenCarolDan_InvalidInactiveCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProofAliceBobCarolDanErinFrank_InvalidInactive = []gocid.Cid{
|
||||||
|
TokenErinFrank_InvalidInactiveCID,
|
||||||
|
TokenDanErin_InvalidInactiveCID,
|
||||||
|
TokenCarolDan_InvalidInactiveCID,
|
||||||
|
TokenBobCarolCID,
|
||||||
|
TokenAliceBobCID,
|
||||||
|
}
|
||||||
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -13,9 +12,8 @@ import (
|
|||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
"github.com/ipld/go-ipld-prime/codec/dagcbor"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy"
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
|
||||||
@@ -26,15 +24,7 @@ import (
|
|||||||
// The following example shows how to create a delegation.Token with
|
// The following example shows how to create a delegation.Token with
|
||||||
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
|
// distinct DIDs for issuer (iss), audience (aud) and subject (sub).
|
||||||
func ExampleNew() {
|
func ExampleNew() {
|
||||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
fmt.Println("issDid:", didtest.PersonaBob.DID().String())
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
issDid, err := did.FromPubKey(issPub)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
fmt.Println("issDid:", issDid)
|
|
||||||
|
|
||||||
audDid := did.MustParse(AudienceDID)
|
|
||||||
subDid := did.MustParse(subjectDID)
|
|
||||||
|
|
||||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
// The command defines the shape of the arguments that will be evaluated against the policy
|
||||||
cmd := command.MustParse("/foo/bar")
|
cmd := command.MustParse("/foo/bar")
|
||||||
@@ -51,8 +41,8 @@ func ExampleNew() {
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tkn, err := delegation.New(issPriv, audDid, cmd, pol,
|
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol,
|
||||||
delegation.WithSubject(subDid),
|
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
||||||
delegation.WithExpirationIn(time.Hour),
|
delegation.WithExpirationIn(time.Hour),
|
||||||
delegation.WithNotBeforeIn(time.Minute),
|
delegation.WithNotBeforeIn(time.Minute),
|
||||||
delegation.WithMeta("foo", "bar"),
|
delegation.WithMeta("foo", "bar"),
|
||||||
@@ -61,101 +51,91 @@ func ExampleNew() {
|
|||||||
printThenPanicOnErr(err)
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
// "Seal", meaning encode and wrap into a signed envelope.
|
// "Seal", meaning encode and wrap into a signed envelope.
|
||||||
data, id, err := tkn.ToSealed(issPriv)
|
data, id, err := tkn.ToSealed(didtest.PersonaBob.PrivKey())
|
||||||
printThenPanicOnErr(err)
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
printCIDAndSealed(id, data)
|
printCIDAndSealed(id, data)
|
||||||
|
|
||||||
// Example output:
|
// Example output:
|
||||||
//
|
//
|
||||||
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe
|
// issDid: did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU
|
||||||
//
|
//
|
||||||
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW
|
// CID (base58BTC): zdpuAsqfZkgg2jgZyob23sq1J9xwtf9PHgt1PsskVCMq7Vvxk
|
||||||
//
|
|
||||||
// DAG-CBOR (base64) out: glhA5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDWzqY2lzc3g4ZGlkOmtleTp6Nk1raFZGem5QZVI1NzJyVEs1MVVqb1ROcG5GOGN4dVdmUG05b0JNUHI3eThBQmVjbmJmGmcNXxZjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3RBMXVCZENwcTR1SkJxRTlqak1pTHl4WkJnOWE2eGdQUEtKak1xc3M2WmMyZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMu0HMgJ5Y+M84I/66
|
|
||||||
//
|
//
|
||||||
|
// DAG-CBOR (base64) out: lhAOnjc0bPptlI5MxRBrIK3YmAP1CxKfXOPkz6MHt/UJCx2gCN+6gXZX2N+BIJvmy8XmAO5sT2GYimiV7HlJH1AA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rZ3VwY2hoNUh3dUhhaFM3WXN5RThiTHVhMU1yOHAyaUtOUmh5dlN2UkFzOW5jY21kaC9mb28vYmFyY2V4cBpnROP/Y2lzc3g4ZGlkOmtleTp6Nk1rdkpQbUVaWlliZ2l3MW91VDFvb3VUc1RGQkhKU3RzOW9waFZzTmdjUm1ZeFVjbmJmGmdE1itjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3V1a2syc2tEWExRbjdOSzNFaDlqTW5kWWZ2REJ4eGt0Z3BpZEpBcWI3TTNwZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMv+Diy6GExIuM1eX4
|
||||||
// Converted to DAG-JSON out:
|
// Converted to DAG-JSON out:
|
||||||
// [
|
// [
|
||||||
// {
|
// {
|
||||||
// "/": {
|
|
||||||
// "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "h": {
|
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "NO0BcQ"
|
// "bytes": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "ucan/dlg@1.0.0-rc.1": {
|
// {
|
||||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
// "h": {
|
||||||
// "cmd": "/foo/bar",
|
|
||||||
// "exp": 1728933098,
|
|
||||||
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
|
||||||
// "meta": {
|
|
||||||
// "baz": 123,
|
|
||||||
// "foo": "bar"
|
|
||||||
// },
|
|
||||||
// "nbf": 1728929558,
|
|
||||||
// "nonce": {
|
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "u0HMgJ5Y+M84I/66"
|
// "bytes": "NO0BcQ"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "pol": [
|
// "ucan/dlg@1.0.0-rc.1": {
|
||||||
// [
|
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||||
// "==",
|
// "cmd": "/foo/bar",
|
||||||
// ".status",
|
// "exp": 1728933098,
|
||||||
// "draft"
|
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
||||||
// ],
|
// "meta": {
|
||||||
// [
|
// "baz": 123,
|
||||||
// "all",
|
// "foo": "bar"
|
||||||
// ".reviewer",
|
// },
|
||||||
|
// "nbf": 1728929558,
|
||||||
|
// "nonce": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "u0HMgJ5Y+M84I/66"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "pol": [
|
||||||
// [
|
// [
|
||||||
// "like",
|
// "==",
|
||||||
// ".email",
|
// ".status",
|
||||||
// "*@example.com"
|
// "draft"
|
||||||
// ]
|
// ],
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "any",
|
|
||||||
// ".tags",
|
|
||||||
// [
|
// [
|
||||||
// "or",
|
// "all",
|
||||||
|
// ".reviewer",
|
||||||
// [
|
// [
|
||||||
|
// "like",
|
||||||
|
// ".email",
|
||||||
|
// "*@example.com"
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "any",
|
||||||
|
// ".tags",
|
||||||
|
// [
|
||||||
|
// "or",
|
||||||
// [
|
// [
|
||||||
// "==",
|
// [
|
||||||
// ".",
|
// "==",
|
||||||
// "news"
|
// ".",
|
||||||
// ],
|
// "news"
|
||||||
// [
|
// ],
|
||||||
// "==",
|
// [
|
||||||
// ".",
|
// "==",
|
||||||
// "press"
|
// ".",
|
||||||
|
// "press"
|
||||||
|
// ]
|
||||||
// ]
|
// ]
|
||||||
// ]
|
// ]
|
||||||
// ]
|
// ]
|
||||||
// ]
|
// ],
|
||||||
// ],
|
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||||
// "sub": "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// ]
|
||||||
// ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The following example shows how to create a UCAN root delegation.Token
|
// The following example shows how to create a UCAN root delegation.Token
|
||||||
// - a delegation.Token with the subject (sub) set to the value of issuer
|
// - a delegation.Token with the subject (sub) set to the value of issuer
|
||||||
// (iss).
|
// (iss).
|
||||||
func ExampleRoot() {
|
func ExampleRoot() {
|
||||||
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
|
|
||||||
issDid, err := did.FromPubKey(issPub)
|
|
||||||
printThenPanicOnErr(err)
|
|
||||||
fmt.Println("issDid:", issDid)
|
|
||||||
|
|
||||||
audDid := did.MustParse(AudienceDID)
|
|
||||||
|
|
||||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
// The command defines the shape of the arguments that will be evaluated against the policy
|
||||||
cmd := command.MustParse("/foo/bar")
|
cmd := command.MustParse("/foo/bar")
|
||||||
|
|
||||||
@@ -171,7 +151,7 @@ func ExampleRoot() {
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tkn, err := delegation.Root(issPriv, audDid, cmd, pol,
|
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
||||||
delegation.WithExpirationIn(time.Hour),
|
delegation.WithExpirationIn(time.Hour),
|
||||||
delegation.WithNotBeforeIn(time.Minute),
|
delegation.WithNotBeforeIn(time.Minute),
|
||||||
delegation.WithMeta("foo", "bar"),
|
delegation.WithMeta("foo", "bar"),
|
||||||
@@ -180,7 +160,7 @@ func ExampleRoot() {
|
|||||||
printThenPanicOnErr(err)
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
// "Seal", meaning encode and wrap into a signed envelope.
|
// "Seal", meaning encode and wrap into a signed envelope.
|
||||||
data, id, err := tkn.ToSealed(issPriv)
|
data, id, err := tkn.ToSealed(didtest.PersonaAlice.PrivKey())
|
||||||
printThenPanicOnErr(err)
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
printCIDAndSealed(id, data)
|
printCIDAndSealed(id, data)
|
||||||
@@ -189,82 +169,82 @@ func ExampleRoot() {
|
|||||||
//
|
//
|
||||||
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
|
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
|
||||||
//
|
//
|
||||||
// CID (base58BTC): zdpuAwLojgfvFCbjz2FsKrvN1khDQ9mFGT6b6pxjMfz73Roed
|
// CID (base58BTC): zdpuAkwYz8nY7uU8j3F6wVTfFY1VEoExwvUAYBEwRWfTozddE
|
||||||
//
|
//
|
||||||
// DAG-CBOR (base64) out: glhA6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDW0wY2lzc3g4ZGlkOmtleTp6Nk1rbldKcXoxN1k0QWZzWFNKVUZLb211QlI0R1RrVmlNN2tKWXV0elRNa0N5RkZjbmJmGmcNX1xjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa25XSnF6MTdZNEFmc1hTSlVGS29tdUJSNEdUa1ZpTTdrSll1dHpUTWtDeUZGZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMJOsjYi1Pq3OIB0La
|
// DAG-CBOR (base64) out: glhAVpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rdkpQbUVaWlliZ2l3MW91VDFvb3VUc1RGQkhKU3RzOW9waFZzTmdjUm1ZeFVjY21kaC9mb28vYmFyY2V4cBpnROVoY2lzc3g4ZGlkOmtleTp6Nk1rdXVrazJza0RYTFFuN05LM0VoOWpNbmRZZnZEQnh4a3RncGlkSkFxYjdNM3BjbmJmGmdE15RjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3V1a2syc2tEWExRbjdOSzNFaDlqTW5kWWZ2REJ4eGt0Z3BpZEpBcWI3TTNwZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMwzDc03WBciJIGPWG
|
||||||
//
|
//
|
||||||
// Converted to DAG-JSON out:
|
// Converted to DAG-JSON out:
|
||||||
// [
|
// [
|
||||||
// {
|
// {
|
||||||
// "/": {
|
|
||||||
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// "h": {
|
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "NO0BcQ"
|
// "bytes": "VpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSAw"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "ucan/dlg@1.0.0-rc.1": {
|
// {
|
||||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
// "h": {
|
||||||
// "cmd": "/foo/bar",
|
|
||||||
// "exp": 1728933168,
|
|
||||||
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
|
|
||||||
// "meta": {
|
|
||||||
// "baz": 123,
|
|
||||||
// "foo": "bar"
|
|
||||||
// },
|
|
||||||
// "nbf": 1728929628,
|
|
||||||
// "nonce": {
|
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "JOsjYi1Pq3OIB0La"
|
// "bytes": "NO0BcQ"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "pol": [
|
// "ucan/dlg@1.0.0-rc.1": {
|
||||||
// [
|
// "aud": "did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU",
|
||||||
// "==",
|
// "cmd": "/foo/bar",
|
||||||
// ".status",
|
// "exp": 1732568424,
|
||||||
// "draft"
|
// "iss": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p",
|
||||||
// ],
|
// "meta": {
|
||||||
// [
|
// "baz": 123,
|
||||||
// "all",
|
// "foo": "bar"
|
||||||
// ".reviewer",
|
// },
|
||||||
|
// "nbf": 1732564884,
|
||||||
|
// "nonce": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "wzDc03WBciJIGPWG"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "pol": [
|
||||||
// [
|
// [
|
||||||
// "like",
|
// "==",
|
||||||
// ".email",
|
// ".status",
|
||||||
// "*@example.com"
|
// "draft"
|
||||||
// ]
|
// ],
|
||||||
// ],
|
|
||||||
// [
|
|
||||||
// "any",
|
|
||||||
// ".tags",
|
|
||||||
// [
|
// [
|
||||||
// "or",
|
// "all",
|
||||||
|
// ".reviewer",
|
||||||
// [
|
// [
|
||||||
|
// "like",
|
||||||
|
// ".email",
|
||||||
|
// "*@example.com"
|
||||||
|
// ]
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "any",
|
||||||
|
// ".tags",
|
||||||
|
// [
|
||||||
|
// "or",
|
||||||
// [
|
// [
|
||||||
// "==",
|
// [
|
||||||
// ".",
|
// "==",
|
||||||
// "news"
|
// ".",
|
||||||
// ],
|
// "news"
|
||||||
// [
|
// ],
|
||||||
// "==",
|
// [
|
||||||
// ".",
|
// "==",
|
||||||
// "press"
|
// ".",
|
||||||
|
// "press"
|
||||||
|
// ]
|
||||||
// ]
|
// ]
|
||||||
// ]
|
// ]
|
||||||
// ]
|
// ]
|
||||||
// ]
|
// ],
|
||||||
// ],
|
// "sub": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p"
|
||||||
// "sub": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF"
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// ]
|
||||||
// ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The following example demonstrates how to get a delegation.Token from
|
// The following example demonstrates how to get a delegation.Token from
|
||||||
// a DAG-CBOR []byte.
|
// a DAG-CBOR []byte.
|
||||||
func ExampleToken_FromSealed() {
|
func ExampleFromSealed() {
|
||||||
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
||||||
|
|
||||||
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package delegation
|
package delegation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@@ -193,8 +194,16 @@ func FromIPLD(node datamodel.Node) (*Token, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||||
var sub *string
|
// sanity check that privKey and issuer are matching
|
||||||
|
issPub, err := t.issuer.PubKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !issPub.Equals(privKey.GetPublic()) {
|
||||||
|
return nil, fmt.Errorf("private key doesn't match the issuer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var sub *string
|
||||||
if t.subject != did.Undef {
|
if t.subject != did.Undef {
|
||||||
s := t.subject.String()
|
s := t.subject.String()
|
||||||
sub = &s
|
sub = &s
|
||||||
|
|||||||
17
token/delegation/loader.go
Normal file
17
token/delegation/loader.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package delegation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrDelegationNotFound is returned if a delegation token is not found
|
||||||
|
var ErrDelegationNotFound = fmt.Errorf("delegation not found")
|
||||||
|
|
||||||
|
// Loader is a delegation token loader.
|
||||||
|
type Loader interface {
|
||||||
|
// GetDelegation returns the delegation.Token matching the given CID.
|
||||||
|
// If not found, ErrDelegationNotFound is returned.
|
||||||
|
GetDelegation(cid cid.Cid) (*Token, error)
|
||||||
|
}
|
||||||
@@ -44,6 +44,22 @@ func WithMeta(key string, val any) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithEncryptedMetaString adds a key/value pair in the "meta" field.
|
||||||
|
// The string value is encrypted with the given aesKey.
|
||||||
|
func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
return t.meta.AddEncrypted(key, val, encryptionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEncryptedMetaBytes adds a key/value pair in the "meta" field.
|
||||||
|
// The []byte value is encrypted with the given aesKey.
|
||||||
|
func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
return t.meta.AddEncrypted(key, val, encryptionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
// WithNotBefore set's the Token's optional "notBefore" field to the value
|
||||||
// of the provided time.Time.
|
// of the provided time.Time.
|
||||||
func WithNotBefore(nbf time.Time) Option {
|
func WithNotBefore(nbf time.Time) Option {
|
||||||
|
|||||||
@@ -26,17 +26,17 @@ const Tag = "ucan/dlg@1.0.0-rc.1"
|
|||||||
var schemaBytes []byte
|
var schemaBytes []byte
|
||||||
|
|
||||||
var (
|
var (
|
||||||
once sync.Once
|
once sync.Once
|
||||||
ts *schema.TypeSystem
|
ts *schema.TypeSystem
|
||||||
err error
|
errSchema error
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustLoadSchema() *schema.TypeSystem {
|
func mustLoadSchema() *schema.TypeSystem {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if errSchema != nil {
|
||||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
||||||
}
|
}
|
||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package delegation_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
@@ -11,6 +10,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
"github.com/ucan-wg/go-ucan/token/delegation"
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||||
)
|
)
|
||||||
@@ -22,7 +22,7 @@ func TestSchemaRoundTrip(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
delegationJson := golden.Get(t, "new.dagjson")
|
delegationJson := golden.Get(t, "new.dagjson")
|
||||||
privKey := privKey(t, issuerPrivKeyCfg)
|
privKey := didtest.PersonaAlice.PrivKey()
|
||||||
|
|
||||||
t.Run("via buffers", func(t *testing.T) {
|
t.Run("via buffers", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -36,18 +36,13 @@ func TestSchemaRoundTrip(t *testing.T) {
|
|||||||
cborBytes, id, err := p1.ToSealed(privKey)
|
cborBytes, id, err := p1.ToSealed(privKey)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||||
fmt.Println("cborBytes length", len(cborBytes))
|
|
||||||
fmt.Println("cbor", string(cborBytes))
|
|
||||||
|
|
||||||
p2, c2, err := delegation.FromSealed(cborBytes)
|
p2, c2, err := delegation.FromSealed(cborBytes)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, id, c2)
|
assert.Equal(t, id, c2)
|
||||||
fmt.Println("read Cbor", p2)
|
|
||||||
|
|
||||||
readJson, err := p2.ToDagJson(privKey)
|
readJson, err := p2.ToDagJson(privKey)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
fmt.Println("readJson length", len(readJson))
|
|
||||||
fmt.Println("json: ", string(readJson))
|
|
||||||
|
|
||||||
assert.JSONEq(t, string(delegationJson), string(readJson))
|
assert.JSONEq(t, string(delegationJson), string(readJson))
|
||||||
})
|
})
|
||||||
@@ -65,7 +60,6 @@ func TestSchemaRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
cborBytes := &bytes.Buffer{}
|
cborBytes := &bytes.Buffer{}
|
||||||
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
||||||
t.Log(len(id.Bytes()), id.Bytes())
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||||
|
|
||||||
@@ -79,6 +73,16 @@ func TestSchemaRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
assert.JSONEq(t, string(delegationJson), readJson.String())
|
assert.JSONEq(t, string(delegationJson), readJson.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("fails with wrong PrivKey", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p1, err := delegation.FromDagJson(delegationJson)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, err = p1.ToSealed(didtest.PersonaBob.PrivKey())
|
||||||
|
require.EqualError(t, err, "private key doesn't match the issuer")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkSchemaLoad(b *testing.B) {
|
func BenchmarkSchemaLoad(b *testing.B) {
|
||||||
@@ -90,7 +94,7 @@ func BenchmarkSchemaLoad(b *testing.B) {
|
|||||||
|
|
||||||
func BenchmarkRoundTrip(b *testing.B) {
|
func BenchmarkRoundTrip(b *testing.B) {
|
||||||
delegationJson := golden.Get(b, "new.dagjson")
|
delegationJson := golden.Get(b, "new.dagjson")
|
||||||
privKey := privKey(b, issuerPrivKeyCfg)
|
privKey := didtest.PersonaAlice.PrivKey()
|
||||||
|
|
||||||
b.Run("via buffers", func(b *testing.B) {
|
b.Run("via buffers", func(b *testing.B) {
|
||||||
p1, _ := delegation.FromDagJson(delegationJson)
|
p1, _ := delegation.FromDagJson(delegationJson)
|
||||||
|
|||||||
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":"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"}}]
|
||||||
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"}}]
|
||||||
@@ -2,22 +2,22 @@ package token
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
"github.com/ipld/go-ipld-prime/codec"
|
"github.com/ipld/go-ipld-prime/codec"
|
||||||
"github.com/libp2p/go-libp2p/core/crypto"
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Token interface {
|
type Token interface {
|
||||||
Marshaller
|
Marshaller
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
||||||
Issuer() did.DID
|
// This does NOT do any other kind of verifications.
|
||||||
// Meta returns the Token's metadata.
|
IsValidNow() bool
|
||||||
Meta() meta.ReadOnly
|
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
IsValidAt(t time.Time) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Marshaller interface {
|
type Marshaller interface {
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ func FromIPLD[T Tokener](node datamodel.Node) (T, error) {
|
|||||||
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
return zero, errors.New("the VarsigHeader key type doesn't match the issuer's key type")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: can we use the already serialized CBOR data here, instead of encoding again the payload?
|
||||||
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
|
data, err := ipld.Encode(info.sigPayloadNode, dagcbor.Encode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return zero, err
|
return zero, err
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
22
token/internal/parse/parse.go
Normal file
22
token/internal/parse/parse.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package parse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OptionalDID(s *string) (did.DID, error) {
|
||||||
|
if s == nil {
|
||||||
|
return did.Undef, nil
|
||||||
|
}
|
||||||
|
return did.Parse(*s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func OptionalTimestamp(sec *int64) *time.Time {
|
||||||
|
if sec == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t := time.Unix(*sec, 0)
|
||||||
|
return &t
|
||||||
|
}
|
||||||
37
token/invocation/errors.go
Normal file
37
token/invocation/errors.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package invocation
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Loading errors
|
||||||
|
var (
|
||||||
|
// ErrMissingDelegation
|
||||||
|
ErrMissingDelegation = errors.New("loader missing delegation for proof chain")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Time bound errors
|
||||||
|
var (
|
||||||
|
// ErrTokenExpired is returned if a token is invalid at execution time
|
||||||
|
ErrTokenInvalidNow = errors.New("token has expired")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Principal alignment errors
|
||||||
|
var (
|
||||||
|
// ErrNoProof is returned when no delegations were provided to prove
|
||||||
|
// that the invocation should be executed.
|
||||||
|
ErrNoProof = errors.New("at least one delegation must be provided to validate the invocation")
|
||||||
|
|
||||||
|
// ErrLastNotRoot is returned if the last delegation token in the proof
|
||||||
|
// chain is not a root delegation token.
|
||||||
|
ErrLastNotRoot = errors.New("the last delegation token in proof chain must be a root token")
|
||||||
|
|
||||||
|
// ErrBrokenChain is returned when the Audience of a delegation is
|
||||||
|
// not the Issuer of the previous one.
|
||||||
|
ErrBrokenChain = errors.New("delegation proof chain doesn't connect the invocation to the subject")
|
||||||
|
|
||||||
|
// ErrWrongSub is returned when the Subject of a delegation is not the invocation audience.
|
||||||
|
ErrWrongSub = errors.New("delegation subject need to match the invocation audience")
|
||||||
|
|
||||||
|
// ErrCommandNotCovered is returned when a delegation command doesn't cover (identical or parent of) the
|
||||||
|
// next delegation or invocation's command.
|
||||||
|
ErrCommandNotCovered = errors.New("allowed command doesn't cover the next delegation or invocation")
|
||||||
|
)
|
||||||
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, sub, cmd, prf,
|
||||||
|
invocation.WithArgument("uri", args["uri"]),
|
||||||
|
invocation.WithArgument("headers", args["headers"]),
|
||||||
|
invocation.WithArgument("payload", args["payload"]),
|
||||||
|
invocation.WithMeta("env", "development"),
|
||||||
|
invocation.WithMeta("tags", meta["tags"]),
|
||||||
|
invocation.WithExpirationIn(time.Minute),
|
||||||
|
invocation.WithoutInvokedAt())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to create invocation:", err.Error())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, cid, err := inv.ToSealed(privKey)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to seal invocation:", err.Error())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := prettyDAGJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failed to pretty DAG-JSON:", err.Error())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("CID:", cid)
|
||||||
|
fmt.Println("Token (pretty DAG-JSON):")
|
||||||
|
fmt.Println(json)
|
||||||
|
|
||||||
|
// Expected CID and DAG-JSON output:
|
||||||
|
// CID: bafyreid2n5q45vk4osned7k5huocbe3mxbisonh5vujepqftc5ftr543ae
|
||||||
|
// Token (pretty DAG-JSON):
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "gvyL7kdSkgmaDpDU/Qj9ohRwxYLCHER52HFMSFEqQqEcQC9qr4JCPP1f/WybvGGuVzYiA0Hx4JO+ohNz8BxUAA"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "h": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "NO0BcQ"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "ucan/inv@1.0.0-rc.1": {
|
||||||
|
// "args": {
|
||||||
|
// "headers": {
|
||||||
|
// "Content-Type": "application/json"
|
||||||
|
// },
|
||||||
|
// "payload": {
|
||||||
|
// "body": "UCAN is great",
|
||||||
|
// "draft": true,
|
||||||
|
// "title": "UCAN for Fun and Profit",
|
||||||
|
// "topics": [
|
||||||
|
// "authz",
|
||||||
|
// "journal"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "uri": "https://example.com/blog/posts"
|
||||||
|
// },
|
||||||
|
// "cmd": "/crud/create",
|
||||||
|
// "exp": 1729788921,
|
||||||
|
// "iss": "did:key:z6MkhniGGyP88eZrq2dpMvUPdS2RQMhTUAWzcu6kVGUvEtCJ",
|
||||||
|
// "meta": {
|
||||||
|
// "env": "development",
|
||||||
|
// "tags": [
|
||||||
|
// "blog",
|
||||||
|
// "post",
|
||||||
|
// "pr#123"
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
// "nonce": {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "2xXPoZwWln1TfXIp"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// "prf": [
|
||||||
|
// {
|
||||||
|
// "/": "bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "/": "bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "/": "bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// "sub": "did:key:z6MktWuvPvBe5UyHnDGuEdw8aJ5qrhhwLG6jy7cQYM6ckP6P"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyDAGJSON(data []byte) (string, error) {
|
||||||
|
var node ipld.Node
|
||||||
|
|
||||||
|
node, err := ipld.Decode(data, dagcbor.Decode)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := ipld.Encode(node, dagjson.Encode)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := json.Indent(&out, jsonData, "", " "); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupExampleNew() (privKey crypto.PrivKey, iss, sub did.DID, cmd command.Command, args map[string]any, prf []cid.Cid, meta map[string]any, errs error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
privKey, iss, err = did.GenerateEd25519()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf("failed to generate Issuer identity: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, sub, err = did.GenerateEd25519()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf("failed to generate Subject identity: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err = command.Parse("/crud/create")
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf("failed to parse command: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"body": "UCAN is great",
|
||||||
|
"draft": true,
|
||||||
|
"title": "UCAN for Fun and Profit",
|
||||||
|
"topics": []string{"authz", "journal"},
|
||||||
|
}
|
||||||
|
|
||||||
|
args = map[string]any{
|
||||||
|
// you can also directly pass IPLD values
|
||||||
|
"uri": basicnode.NewString("https://example.com/blog/posts"),
|
||||||
|
"headers": headers,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
prf = make([]cid.Cid, 3)
|
||||||
|
for i, v := range []string{
|
||||||
|
"zdpuAzx4sBrBCabrZZqXgvK3NDzh7Mf5mKbG11aBkkMCdLtCp",
|
||||||
|
"zdpuApTCXfoKh2sB1KaUaVSGofCBNPUnXoBb6WiCeitXEibZy",
|
||||||
|
"zdpuAoFdXRPw4n6TLcncoDhq1Mr6FGbpjAiEtqSBrTSaYMKkf",
|
||||||
|
} {
|
||||||
|
prf[i], err = cid.Parse(v)
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, fmt.Errorf("failed to parse proof cid: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta = map[string]any{
|
||||||
|
"env": basicnode.NewString("development"),
|
||||||
|
"tags": []string{"blog", "post", "pr#123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return // WARNING: named return values
|
||||||
|
}
|
||||||
@@ -8,37 +8,132 @@
|
|||||||
package invocation
|
package invocation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/did"
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/command"
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/nonce"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/parse"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Token is an immutable type that holds the fields of a UCAN invocation.
|
// Token is an immutable type that holds the fields of a UCAN invocation.
|
||||||
type Token struct {
|
type Token struct {
|
||||||
// Issuer DID (invoker)
|
// The DID of the Invoker
|
||||||
issuer did.DID
|
issuer did.DID
|
||||||
// Audience DID (receiver/executor)
|
// The DID of Subject being invoked
|
||||||
audience did.DID
|
|
||||||
// Subject DID (subject being invoked)
|
|
||||||
subject did.DID
|
subject did.DID
|
||||||
// The Command to invoke
|
// The DID of the intended Executor if different from the Subject
|
||||||
|
audience did.DID
|
||||||
|
|
||||||
|
// The Command
|
||||||
command command.Command
|
command command.Command
|
||||||
// TODO: args
|
// The Command's arguments
|
||||||
// TODO: prf
|
arguments *args.Args
|
||||||
// A unique, random nonce
|
// CIDs of the delegation.Token that prove the chain of authority
|
||||||
nonce []byte
|
// They need to form a strictly linear chain, and being ordered starting from the
|
||||||
|
// leaf Delegation (with aud matching the invocation's iss), in a strict sequence
|
||||||
|
// where the iss of the previous Delegation matches the aud of the next Delegation.
|
||||||
|
proof []cid.Cid
|
||||||
// Arbitrary Metadata
|
// Arbitrary Metadata
|
||||||
meta *meta.Meta
|
meta *meta.Meta
|
||||||
|
|
||||||
|
// A unique, random nonce
|
||||||
|
nonce []byte
|
||||||
// The timestamp at which the Invocation becomes invalid
|
// The timestamp at which the Invocation becomes invalid
|
||||||
expiration *time.Time
|
expiration *time.Time
|
||||||
// The timestamp at which the Invocation was created
|
// The timestamp at which the Invocation was created
|
||||||
invokedAt *time.Time
|
invokedAt *time.Time
|
||||||
// TODO: cause
|
|
||||||
|
// An optional CID of the Receipt that enqueued the Task
|
||||||
|
cause *cid.Cid
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates an invocation Token with the provided options.
|
||||||
|
//
|
||||||
|
// If no nonce is provided, a random 12-byte nonce is generated. Use the
|
||||||
|
// WithNonce or WithEmptyNonce options to specify provide your own nonce
|
||||||
|
// or to leave the nonce empty respectively.
|
||||||
|
//
|
||||||
|
// If no invokedAt is provided, the current time is used. Use the
|
||||||
|
// WithInvokedAt or WithInvokedAtIn Options to specify a different time
|
||||||
|
// or the WithoutInvokedAt Option to clear the Token's invokedAt field.
|
||||||
|
//
|
||||||
|
// With the exception of the WithMeta option, all others will overwrite
|
||||||
|
// the previous contents of their target field.
|
||||||
|
func New(iss, sub did.DID, cmd command.Command, prf []cid.Cid, opts ...Option) (*Token, error) {
|
||||||
|
iat := time.Now()
|
||||||
|
|
||||||
|
tkn := Token{
|
||||||
|
issuer: iss,
|
||||||
|
subject: sub,
|
||||||
|
command: cmd,
|
||||||
|
arguments: args.New(),
|
||||||
|
proof: prf,
|
||||||
|
meta: meta.NewMeta(),
|
||||||
|
nonce: nil,
|
||||||
|
invokedAt: &iat,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
if err := opt(&tkn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if len(tkn.nonce) == 0 {
|
||||||
|
tkn.nonce, err = nonce.Generate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tkn.validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tkn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) ExecutionAllowed(loader delegation.Loader) error {
|
||||||
|
return t.executionAllowed(loader, t.arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) ExecutionAllowedWithArgsHook(loader delegation.Loader, hook func(args args.ReadOnly) (*args.Args, error)) error {
|
||||||
|
newArgs, err := hook(t.arguments.ReadOnly())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return t.executionAllowed(loader, newArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) executionAllowed(loader delegation.Loader, arguments *args.Args) error {
|
||||||
|
delegations, err := t.loadProofs(loader)
|
||||||
|
if err != nil {
|
||||||
|
// All referenced delegations must be available - 4b
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.verifyProofs(delegations); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.verifyTimeBound(delegations); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.verifyArgs(delegations, arguments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issuer returns the did.DID representing the Token's issuer.
|
// Issuer returns the did.DID representing the Token's issuer.
|
||||||
@@ -46,28 +141,31 @@ func (t *Token) Issuer() did.DID {
|
|||||||
return t.issuer
|
return t.issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject returns the did.DID representing the Token's subject.
|
||||||
|
func (t *Token) Subject() did.DID {
|
||||||
|
return t.subject
|
||||||
|
}
|
||||||
|
|
||||||
// Audience returns the did.DID representing the Token's audience.
|
// Audience returns the did.DID representing the Token's audience.
|
||||||
func (t *Token) Audience() did.DID {
|
func (t *Token) Audience() did.DID {
|
||||||
return t.audience
|
return t.audience
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject returns the did.DID representing the Token's subject.
|
|
||||||
//
|
|
||||||
// This field may be did.Undef for delegations that are [Powerlined] but
|
|
||||||
// must be equal to the value returned by the Issuer method for root
|
|
||||||
// tokens.
|
|
||||||
func (t *Token) Subject() did.DID {
|
|
||||||
return t.subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command returns the capability's command.Command.
|
// Command returns the capability's command.Command.
|
||||||
func (t *Token) Command() command.Command {
|
func (t *Token) Command() command.Command {
|
||||||
return t.command
|
return t.command
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nonce returns the random Nonce encapsulated in this Token.
|
// Arguments returns the arguments to be used when the command is
|
||||||
func (t *Token) Nonce() []byte {
|
// invoked.
|
||||||
return t.nonce
|
func (t *Token) Arguments() args.ReadOnly {
|
||||||
|
return t.arguments.ReadOnly()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proof() returns the ordered list of cid.Cid which reference the
|
||||||
|
// delegation Tokens that authorize this invocation.
|
||||||
|
func (t *Token) Proof() []cid.Cid {
|
||||||
|
return t.proof
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meta returns the Token's metadata.
|
// Meta returns the Token's metadata.
|
||||||
@@ -75,11 +173,43 @@ func (t *Token) Meta() meta.ReadOnly {
|
|||||||
return t.meta.ReadOnly()
|
return t.meta.ReadOnly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nonce returns the random Nonce encapsulated in this Token.
|
||||||
|
func (t *Token) Nonce() []byte {
|
||||||
|
return t.nonce
|
||||||
|
}
|
||||||
|
|
||||||
// Expiration returns the time at which the Token expires.
|
// Expiration returns the time at which the Token expires.
|
||||||
func (t *Token) Expiration() *time.Time {
|
func (t *Token) Expiration() *time.Time {
|
||||||
return t.expiration
|
return t.expiration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InvokedAt returns the time.Time at which the invocation token was
|
||||||
|
// created.
|
||||||
|
func (t *Token) InvokedAt() *time.Time {
|
||||||
|
return t.invokedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cause returns the Token's (optional) cause field which may specify
|
||||||
|
// which describes the Receipt that requested the invocation.
|
||||||
|
func (t *Token) Cause() *cid.Cid {
|
||||||
|
return t.cause
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
func (t *Token) IsValidNow() bool {
|
||||||
|
return t.IsValidAt(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
||||||
|
// This does NOT do any other kind of verifications.
|
||||||
|
func (t *Token) IsValidAt(ti time.Time) bool {
|
||||||
|
if t.expiration != nil && ti.After(*t.expiration) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Token) validate() error {
|
func (t *Token) validate() error {
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
@@ -90,8 +220,7 @@ func (t *Token) validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requiredDID(t.issuer, "Issuer")
|
requiredDID(t.issuer, "Issuer")
|
||||||
|
requiredDID(t.subject, "Subject")
|
||||||
// TODO
|
|
||||||
|
|
||||||
if len(t.nonce) < 12 {
|
if len(t.nonce) < 12 {
|
||||||
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
errs = errors.Join(errs, fmt.Errorf("token nonce too small"))
|
||||||
@@ -100,25 +229,58 @@ func (t *Token) validate() error {
|
|||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Token) loadProofs(loader delegation.Loader) (res []*delegation.Token, err error) {
|
||||||
|
res = make([]*delegation.Token, len(t.proof))
|
||||||
|
for i, c := range t.proof {
|
||||||
|
res[i], err = loader.GetDelegation(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: need %s", ErrMissingDelegation, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
// tokenFromModel build a decoded view of the raw IPLD data.
|
// tokenFromModel build a decoded view of the raw IPLD data.
|
||||||
// This function also serves as validation.
|
// This function also serves as validation.
|
||||||
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
||||||
var (
|
var (
|
||||||
tkn Token
|
tkn Token
|
||||||
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO
|
if tkn.issuer, err = did.Parse(m.Iss); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse iss: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.subject, err = did.Parse(m.Sub); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.audience, err = parse.OptionalDID(m.Aud); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse audience: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.Nonce) == 0 {
|
||||||
|
return nil, fmt.Errorf("nonce is required")
|
||||||
|
}
|
||||||
|
tkn.nonce = m.Nonce
|
||||||
|
|
||||||
|
tkn.arguments = m.Args
|
||||||
|
tkn.proof = m.Prf
|
||||||
|
tkn.meta = m.Meta
|
||||||
|
|
||||||
|
tkn.expiration = parse.OptionalTimestamp(m.Exp)
|
||||||
|
tkn.invokedAt = parse.OptionalTimestamp(m.Iat)
|
||||||
|
|
||||||
|
tkn.cause = m.Cause
|
||||||
|
|
||||||
|
if err := tkn.validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &tkn, nil
|
return &tkn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateNonce creates a 12-byte random nonce.
|
|
||||||
// TODO: some crypto scheme require more, is that our case?
|
|
||||||
func generateNonce() ([]byte, error) {
|
|
||||||
res := make([]byte, 12)
|
|
||||||
_, err := rand.Read(res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
|
|
||||||
type DID string
|
type DID string
|
||||||
|
|
||||||
# The Invocation Payload attaches sender, receiver, and provenance to the Task.
|
# The Invocation Payload attaches sender, receiver, and provenance to the Task.
|
||||||
type Payload struct {
|
type Payload struct {
|
||||||
# Issuer DID (sender)
|
# The DID of the invoker
|
||||||
iss DID
|
iss DID
|
||||||
# Audience DID (receiver)
|
# The Subject being invoked
|
||||||
aud DID
|
sub DID
|
||||||
# Principal that the chain is about (the Subject)
|
# The DID of the intended Executor if different from the Subject
|
||||||
sub optional DID
|
aud optional DID
|
||||||
|
|
||||||
# The Command to eventually invoke
|
# The Command
|
||||||
cmd String
|
cmd String
|
||||||
|
# The Command's Arguments
|
||||||
# A unique, random nonce
|
args { String : Any}
|
||||||
nonce Bytes
|
# Delegations that prove the chain of authority
|
||||||
|
prf [ Link ]
|
||||||
|
|
||||||
# Arbitrary Metadata
|
# Arbitrary Metadata
|
||||||
meta {String : Any}
|
meta optional { String : Any }
|
||||||
|
|
||||||
|
# A unique, random nonce
|
||||||
|
nonce optional Bytes
|
||||||
# The timestamp at which the Invocation becomes invalid
|
# The timestamp at which the Invocation becomes invalid
|
||||||
exp nullable Int
|
exp nullable Int
|
||||||
|
# The Timestamp at which the Invocation was created
|
||||||
|
iat optional Int
|
||||||
|
|
||||||
|
# An optional CID of the Receipt that enqueued the Task
|
||||||
|
cause optional Link
|
||||||
}
|
}
|
||||||
|
|||||||
139
token/invocation/invocation_test.go
Normal file
139
token/invocation/invocation_test.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package invocation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/command"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
missingPrivKeyCfg = "CAESQMjRvrEIjpPYRQKmkAGw/pV0XgE958rYa4vlnKJjl1zz/sdnGnyV1xKLJk8D39edyjhHWyqcpgFnozQK62SG16k="
|
||||||
|
missingTknCIDStr = "bafyreigwypmw6eul6vadi6g6lnfbsfo2zck7gfzsbjoroqs3djhnzzc7mm"
|
||||||
|
missingDIDStr = "did:key:z6MkwboxFsH3kEuehBZ5fLkRmxi68yv1u38swA4r9Jm2VRma"
|
||||||
|
)
|
||||||
|
|
||||||
|
var emptyArguments = args.New()
|
||||||
|
|
||||||
|
func TestToken_ExecutionAllowed(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("passes - only root", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testPasses(t, didtest.PersonaBob, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBob)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("passes - valid chain", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testPasses(t, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("passes - proof chain attenuates command", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_ValidAttenuatedCommand)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("passes - invocation attenuates command", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testPasses(t, didtest.PersonaFrank, delegationtest.AttenuatedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - no proof", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFails(t, invocation.ErrNoProof, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofEmpty)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - missing referenced delegation", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
missingTknCID, err := cid.Parse(missingTknCIDStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
prf := []cid.Cid{missingTknCID, delegationtest.TokenAliceBobCID}
|
||||||
|
testFails(t, invocation.ErrMissingDelegation, didtest.PersonaCarol, delegationtest.NominalCommand, emptyArguments, prf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - referenced delegation expired", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpired)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - referenced delegation inactive", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFails(t, invocation.ErrTokenInvalidNow, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidInactive)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - last (or only) delegation not root", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
prf := []cid.Cid{delegationtest.TokenErinFrankCID, delegationtest.TokenDanErinCID, delegationtest.TokenCarolDanCID}
|
||||||
|
testFails(t, invocation.ErrLastNotRoot, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - broken chain", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
prf := []cid.Cid{delegationtest.TokenCarolDanCID, delegationtest.TokenAliceBobCID}
|
||||||
|
testFails(t, invocation.ErrBrokenChain, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - first not issued to invoker", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
prf := []cid.Cid{delegationtest.TokenBobCarolCID, delegationtest.TokenAliceBobCID}
|
||||||
|
testFails(t, invocation.ErrBrokenChain, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, prf)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - proof chain expands command", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.NominalCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidExpandedCommand)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - invocation expands command", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFails(t, invocation.ErrCommandNotCovered, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails - inconsistent subject", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFails(t, invocation.ErrWrongSub, didtest.PersonaFrank, delegationtest.ExpandedCommand, emptyArguments, delegationtest.ProofAliceBobCarolDanErinFrank_InvalidSubject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func test(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) error {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// TODO: use the args and add minimal test to check that they are verified against the policy
|
||||||
|
|
||||||
|
tkn, err := invocation.New(persona.DID(), didtest.PersonaAlice.DID(), cmd, prf, opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return tkn.ExecutionAllowed(delegationtest.GetDelegationLoader())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFails(t *testing.T, expErr error, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) {
|
||||||
|
err := test(t, persona, cmd, args, prf, opts...)
|
||||||
|
require.ErrorIs(t, err, expErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPasses(t *testing.T, persona didtest.Persona, cmd command.Command, args *args.Args, prf []cid.Cid, opts ...invocation.Option) {
|
||||||
|
err := test(t, persona, cmd, args, prf, opts...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package invocation
|
package invocation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@@ -193,14 +194,21 @@ func FromIPLD(node datamodel.Node) (*Token, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
||||||
var sub *string
|
// sanity check that privKey and issuer are matching
|
||||||
|
issPub, err := t.issuer.PubKey()
|
||||||
if t.subject != did.Undef {
|
if err != nil {
|
||||||
s := t.subject.String()
|
return nil, err
|
||||||
sub = &s
|
}
|
||||||
|
if !issPub.Equals(privKey.GetPublic()) {
|
||||||
|
return nil, fmt.Errorf("private key doesn't match the issuer")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
var aud *string
|
||||||
|
|
||||||
|
if t.audience != did.Undef {
|
||||||
|
a := t.audience.String()
|
||||||
|
aud = &a
|
||||||
|
}
|
||||||
|
|
||||||
var exp *int64
|
var exp *int64
|
||||||
if t.expiration != nil {
|
if t.expiration != nil {
|
||||||
@@ -208,14 +216,29 @@ func (t *Token) toIPLD(privKey crypto.PrivKey) (datamodel.Node, error) {
|
|||||||
exp = &u
|
exp = &u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var iat *int64
|
||||||
|
if t.invokedAt != nil {
|
||||||
|
i := t.invokedAt.Unix()
|
||||||
|
iat = &i
|
||||||
|
}
|
||||||
|
|
||||||
model := &tokenPayloadModel{
|
model := &tokenPayloadModel{
|
||||||
Iss: t.issuer.String(),
|
Iss: t.issuer.String(),
|
||||||
Aud: t.audience.String(),
|
Aud: aud,
|
||||||
Sub: sub,
|
Sub: t.subject.String(),
|
||||||
Cmd: t.command.String(),
|
Cmd: t.command.String(),
|
||||||
|
Args: t.arguments,
|
||||||
|
Prf: t.proof,
|
||||||
|
Meta: t.meta,
|
||||||
Nonce: t.nonce,
|
Nonce: t.nonce,
|
||||||
Meta: *t.meta,
|
|
||||||
Exp: exp,
|
Exp: exp,
|
||||||
|
Iat: iat,
|
||||||
|
Cause: t.cause,
|
||||||
|
}
|
||||||
|
|
||||||
|
// seems like it's a requirement to have a null meta if there are no values?
|
||||||
|
if len(model.Meta.Keys) == 0 {
|
||||||
|
model.Meta = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return envelope.ToIPLD(privKey, model)
|
return envelope.ToIPLD(privKey, model)
|
||||||
|
|||||||
38
token/invocation/ipld_test.go
Normal file
38
token/invocation/ipld_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package invocation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSealUnsealRoundtrip(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
privKey, iss, sub, cmd, args, prf, meta, err := setupExampleNew()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tkn1, err := invocation.New(iss, sub, cmd, prf,
|
||||||
|
invocation.WithArgument("uri", args["uri"]),
|
||||||
|
invocation.WithArgument("headers", args["headers"]),
|
||||||
|
invocation.WithArgument("payload", args["payload"]),
|
||||||
|
invocation.WithMeta("env", "development"),
|
||||||
|
invocation.WithMeta("tags", meta["tags"]),
|
||||||
|
invocation.WithExpirationIn(time.Minute),
|
||||||
|
invocation.WithoutInvokedAt(),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data, cid1, err := tkn1.ToSealed(privKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tkn2, cid2, err := invocation.FromSealed(data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, cid1, cid2)
|
||||||
|
assert.Equal(t, tkn1, tkn2)
|
||||||
|
}
|
||||||
141
token/invocation/options.go
Normal file
141
token/invocation/options.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package invocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option is a type that allows optional fields to be set during the
|
||||||
|
// creation of an invocation Token.
|
||||||
|
type Option func(*Token) error
|
||||||
|
|
||||||
|
// WithArgument adds a key/value pair to the Token's Arguments field.
|
||||||
|
func WithArgument(key string, val any) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
return t.arguments.Add(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAudience sets the Token's audience to the provided did.DID.
|
||||||
|
//
|
||||||
|
// If the provided did.DID is the same as the Token's subject, the
|
||||||
|
// audience is not set.
|
||||||
|
func WithAudience(aud did.DID) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
if t.subject.String() != aud.String() {
|
||||||
|
t.audience = aud
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMeta adds a key/value pair in the "meta" field.
|
||||||
|
//
|
||||||
|
// WithMeta can be used multiple times in the same call.
|
||||||
|
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
||||||
|
// and ipld.Node.
|
||||||
|
func WithMeta(key string, val any) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
return t.meta.Add(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEncryptedMetaString adds a key/value pair in the "meta" field.
|
||||||
|
// The string value is encrypted with the given aesKey.
|
||||||
|
func WithEncryptedMetaString(key, val string, encryptionKey []byte) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
return t.meta.AddEncrypted(key, val, encryptionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEncryptedMetaBytes adds a key/value pair in the "meta" field.
|
||||||
|
// The []byte value is encrypted with the given aesKey.
|
||||||
|
func WithEncryptedMetaBytes(key string, val, encryptionKey []byte) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
return t.meta.AddEncrypted(key, val, encryptionKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNonce sets the Token's nonce with the given value.
|
||||||
|
//
|
||||||
|
// If this option is not used, a random 12-byte nonce is generated for
|
||||||
|
// this required field. If you truly want to create an invocation Token
|
||||||
|
// without a nonce, use the WithEmptyNonce Option which will set the
|
||||||
|
// nonce to an empty byte array.
|
||||||
|
func WithNonce(nonce []byte) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
t.nonce = nonce
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEmptyNonce sets the Token's nonce to an empty byte slice as
|
||||||
|
// suggested by the UCAN spec for invocation tokens that represent
|
||||||
|
// idempotent operations.
|
||||||
|
func WithEmptyNonce() Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
t.nonce = []byte{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExpiration set's the Token's optional "expiration" field to the
|
||||||
|
// value of the provided time.Time.
|
||||||
|
func WithExpiration(exp time.Time) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
exp = exp.Round(time.Second)
|
||||||
|
t.expiration = &exp
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExpirationIn set's the Token's optional "expiration" field to
|
||||||
|
// Now() plus the given duration.
|
||||||
|
func WithExpirationIn(after time.Duration) Option {
|
||||||
|
return WithExpiration(time.Now().Add(after))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInvokedAt sets the Token's invokedAt field to the provided
|
||||||
|
// time.Time.
|
||||||
|
//
|
||||||
|
// If this Option is not provided, the invocation Token's iat field will
|
||||||
|
// be set to the value of time.Now(). If you want to create an invocation
|
||||||
|
// Token without this field being set, use the WithoutInvokedAt Option.
|
||||||
|
func WithInvokedAt(iat time.Time) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
t.invokedAt = &iat
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInvokedAtIn sets the Token's invokedAt field to Now() plus the
|
||||||
|
// given duration.
|
||||||
|
func WithInvokedAtIn(after time.Duration) Option {
|
||||||
|
return WithInvokedAt(time.Now().Add(after))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutInvokedAt clears the Token's invokedAt field.
|
||||||
|
func WithoutInvokedAt() Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
t.invokedAt = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCause sets the Token's cause field to the provided cid.Cid.
|
||||||
|
func WithCause(cause *cid.Cid) Option {
|
||||||
|
return func(t *Token) error {
|
||||||
|
t.cause = cause
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
141
token/invocation/proof.go
Normal file
141
token/invocation/proof.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package invocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/policy"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/delegation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// # Invocation token validation
|
||||||
|
//
|
||||||
|
// Per the specification, invocation Tokens must be validated before the command is executed.
|
||||||
|
// This validation effectively happens in multiple places in the codebase.
|
||||||
|
// Steps 1 and 2 are the same for all token types.
|
||||||
|
//
|
||||||
|
// 1. When a token is read/unsealed from its containing envelope (`envelope` package):
|
||||||
|
// a. The envelope can be decoded.
|
||||||
|
// b. The envelope contains a Signature, VarsigHeader and Payload.
|
||||||
|
// c. The Payload contains an iss field that contains a valid `did:key`.
|
||||||
|
// d. The public key can be extracted from the `did:key`.
|
||||||
|
// e. The public key type is supported by go-ucan.
|
||||||
|
// f. The Signature can be decoded per the VarsigHeader.
|
||||||
|
// g. The SigPayload can be verified using the Signature and public key.
|
||||||
|
// h. The field key of the TokenPayload matches the expected tag.
|
||||||
|
//
|
||||||
|
// 2. When the token is created or passes step one (token constructor or decoder):
|
||||||
|
// a. All required fields are present
|
||||||
|
// b. All populated fields respect their own rules (example: a policy is legal)
|
||||||
|
//
|
||||||
|
// 3. When an unsealed invocation passes steps one and two for execution (verifyTimeBound below):
|
||||||
|
// a. The invocation cannot be expired (expiration in the future or absent).
|
||||||
|
// b. All the delegation must not be expired (expiration in the future or absent).
|
||||||
|
// c. All the delegation must be active (nbf in the past or absent).
|
||||||
|
//
|
||||||
|
// 4. When the proof chain is being validated (verifyProofs below):
|
||||||
|
// a. There must be at least one delegation in the proof chain.
|
||||||
|
// b. All referenced delegations must be available.
|
||||||
|
// c. The first proof must be issued to the Invoker (audience DID).
|
||||||
|
// d. The Issuer of each delegation must be the Audience in the next one.
|
||||||
|
// e. The last token must be a root delegation.
|
||||||
|
// f. The Subject of each delegation must equal the invocation's Audience field.
|
||||||
|
// g. The command of each delegation must "allow" the one before it.
|
||||||
|
//
|
||||||
|
// 5. If steps 1-4 pass:
|
||||||
|
// a. The policy must "match" the arguments. (verifyArgs below)
|
||||||
|
// b. The nonce (if present) is not reused. (out of scope for go-ucan)
|
||||||
|
|
||||||
|
// verifyProofs controls that the proof chain allows the invocation:
|
||||||
|
// - principal alignment
|
||||||
|
// - command alignment
|
||||||
|
func (t *Token) verifyProofs(delegations []*delegation.Token) error {
|
||||||
|
// There must be at least one delegation referenced - 4a
|
||||||
|
if len(delegations) < 1 {
|
||||||
|
return ErrNoProof
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := t.command
|
||||||
|
iss := t.issuer
|
||||||
|
aud := t.audience
|
||||||
|
if !aud.Defined() {
|
||||||
|
aud = t.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// control from the invocation to the root
|
||||||
|
for i, dlgCid := range t.proof {
|
||||||
|
dlg := delegations[i]
|
||||||
|
|
||||||
|
// The Subject of each delegation must equal the invocation's Audience field. - 4f
|
||||||
|
if dlg.Subject() != aud {
|
||||||
|
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrWrongSub, dlgCid, aud, dlg.Subject())
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first proof must be issued to the Invoker (audience DID). - 4c
|
||||||
|
// The Issuer of each delegation must be the Audience in the next one. - 4d
|
||||||
|
if dlg.Audience() != iss {
|
||||||
|
return fmt.Errorf("%w: delegation %s, expected %s, got %s", ErrBrokenChain, dlgCid, iss, dlg.Audience())
|
||||||
|
}
|
||||||
|
iss = dlg.Issuer()
|
||||||
|
|
||||||
|
// The command of each delegation must "allow" the one before it. - 4g
|
||||||
|
if !dlg.Command().Covers(cmd) {
|
||||||
|
return fmt.Errorf("%w: delegation %s, %s doesn't cover %s", ErrCommandNotCovered, dlgCid, dlg.Command(), cmd)
|
||||||
|
}
|
||||||
|
cmd = dlg.Command()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last prf value must be a root delegation (have the issuer field match the Subject field) - 4e
|
||||||
|
if last := delegations[len(delegations)-1]; last.Issuer() != last.Subject() {
|
||||||
|
return fmt.Errorf("%w: expected %s, got %s", ErrLastNotRoot, last.Subject(), last.Issuer())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) verifyTimeBound(dlgs []*delegation.Token) error {
|
||||||
|
return t.verifyTimeBoundAt(time.Now(), dlgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) verifyTimeBoundAt(at time.Time, delegations []*delegation.Token) error {
|
||||||
|
// The invocation cannot be expired (expiration in the future or absent). - 3a
|
||||||
|
if !t.IsValidAt(at) {
|
||||||
|
return fmt.Errorf("%w: invocation", ErrTokenInvalidNow)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, dlgCid := range t.proof {
|
||||||
|
dlg := delegations[i]
|
||||||
|
|
||||||
|
// All the delegation must not be expired (expiration in the future or absent). - 3b
|
||||||
|
// All the delegation must be active (nbf in the past or absent). - 3c
|
||||||
|
if !dlg.IsValidAt(at) {
|
||||||
|
return fmt.Errorf("%w: delegation %s", ErrTokenInvalidNow, dlgCid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) verifyArgs(delegations []*delegation.Token, arguments *args.Args) error {
|
||||||
|
var count int
|
||||||
|
for i := range t.proof {
|
||||||
|
count += len(delegations[i].Policy())
|
||||||
|
}
|
||||||
|
|
||||||
|
policies := make(policy.Policy, 0, count)
|
||||||
|
for i := range t.proof {
|
||||||
|
policies = append(policies, delegations[i].Policy()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
argsIpld, err := arguments.ToIPLD()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, statement := policies.Match(argsIpld)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("the following UCAN policy is not satisfied: %v", statement.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,10 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-cid"
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/node/bindnode"
|
"github.com/ipld/go-ipld-prime/node/bindnode"
|
||||||
"github.com/ipld/go-ipld-prime/schema"
|
"github.com/ipld/go-ipld-prime/schema"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/pkg/args"
|
||||||
"github.com/ucan-wg/go-ucan/pkg/meta"
|
"github.com/ucan-wg/go-ucan/pkg/meta"
|
||||||
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||||
)
|
)
|
||||||
@@ -23,17 +25,17 @@ const Tag = "ucan/inv@1.0.0-rc.1"
|
|||||||
var schemaBytes []byte
|
var schemaBytes []byte
|
||||||
|
|
||||||
var (
|
var (
|
||||||
once sync.Once
|
once sync.Once
|
||||||
ts *schema.TypeSystem
|
ts *schema.TypeSystem
|
||||||
err error
|
errSchema error
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustLoadSchema() *schema.TypeSystem {
|
func mustLoadSchema() *schema.TypeSystem {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if errSchema != nil {
|
||||||
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
||||||
}
|
}
|
||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
@@ -44,28 +46,34 @@ func payloadType() schema.Type {
|
|||||||
|
|
||||||
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
||||||
|
|
||||||
// TODO
|
|
||||||
type tokenPayloadModel struct {
|
type tokenPayloadModel struct {
|
||||||
// Issuer DID (sender)
|
// The DID of the Invoker
|
||||||
Iss string
|
Iss string
|
||||||
// Audience DID (receiver)
|
// The DID of Subject being invoked
|
||||||
Aud string
|
Sub string
|
||||||
// Principal that the chain is about (the Subject)
|
// The DID of the intended Executor if different from the Subject
|
||||||
// optional: can be nil
|
Aud *string
|
||||||
Sub *string
|
|
||||||
|
|
||||||
// The Command to eventually invoke
|
// The Command
|
||||||
Cmd string
|
Cmd string
|
||||||
|
// The Command's Arguments
|
||||||
|
Args *args.Args
|
||||||
|
// Delegations that prove the chain of authority
|
||||||
|
Prf []cid.Cid
|
||||||
|
|
||||||
|
// Arbitrary Metadata
|
||||||
|
Meta *meta.Meta
|
||||||
|
|
||||||
// A unique, random nonce
|
// A unique, random nonce
|
||||||
Nonce []byte
|
Nonce []byte
|
||||||
|
|
||||||
// Arbitrary Metadata
|
|
||||||
Meta meta.Meta
|
|
||||||
|
|
||||||
// The timestamp at which the Invocation becomes invalid
|
// The timestamp at which the Invocation becomes invalid
|
||||||
// optional: can be nil
|
// optional: can be nil
|
||||||
Exp *int64
|
Exp *int64
|
||||||
|
// The timestamp at which the Invocation was created
|
||||||
|
Iat *int64
|
||||||
|
|
||||||
|
// An optional CID of the Receipt that enqueued the Task
|
||||||
|
Cause *cid.Cid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
func (e *tokenPayloadModel) Prototype() schema.TypedPrototype {
|
||||||
|
|||||||
102
token/invocation/schema_test.go
Normal file
102
token/invocation/schema_test.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
package invocation_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-libp2p/core/crypto"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gotest.tools/v3/golden"
|
||||||
|
|
||||||
|
"github.com/ucan-wg/go-ucan/did/didtest"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/internal/envelope"
|
||||||
|
"github.com/ucan-wg/go-ucan/token/invocation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
issuerPrivKeyCfg = "CAESQK45xBfqIxRp7ZdRdck3tIJZKocCqvANQc925dCJhFwO7DJNA2j94zkF0TNx5mpXV0s6utfkFdHddWTaPVU6yZc="
|
||||||
|
newCID = "zdpuAqY6Zypg4UnpbSUgDvYGneyFaTKaZevzxgSxV4rmv3Fpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSchemaRoundTrip(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
invocationJson := golden.Get(t, "new.dagjson")
|
||||||
|
privKey := privKey(t, issuerPrivKeyCfg)
|
||||||
|
|
||||||
|
t.Run("via buffers", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||||
|
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
|
||||||
|
|
||||||
|
p1, err := invocation.FromDagJson(invocationJson)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cborBytes, id, err := p1.ToSealed(privKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||||
|
fmt.Println("cborBytes length", len(cborBytes))
|
||||||
|
fmt.Println("cbor", string(cborBytes))
|
||||||
|
|
||||||
|
p2, c2, err := invocation.FromSealed(cborBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, id, c2)
|
||||||
|
fmt.Println("read Cbor", p2)
|
||||||
|
|
||||||
|
readJson, err := p2.ToDagJson(privKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println("readJson length", len(readJson))
|
||||||
|
fmt.Println("json: ", string(readJson))
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(invocationJson), string(readJson))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("via streaming", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(invocationJson)
|
||||||
|
|
||||||
|
// format: dagJson --> PayloadModel --> dagCbor --> PayloadModel --> dagJson
|
||||||
|
// function: DecodeDagJson() Seal() Unseal() EncodeDagJson()
|
||||||
|
|
||||||
|
p1, err := invocation.FromDagJsonReader(buf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cborBytes := &bytes.Buffer{}
|
||||||
|
id, err := p1.ToSealedWriter(cborBytes, privKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, newCID, envelope.CIDToBase58BTC(id))
|
||||||
|
|
||||||
|
p2, c2, err := invocation.FromSealedReader(cborBytes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, envelope.CIDToBase58BTC(id), envelope.CIDToBase58BTC(c2))
|
||||||
|
|
||||||
|
readJson := &bytes.Buffer{}
|
||||||
|
require.NoError(t, p2.ToDagJsonWriter(readJson, privKey))
|
||||||
|
|
||||||
|
assert.JSONEq(t, string(invocationJson), readJson.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails with wrong PrivKey", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p1, err := invocation.FromDagJson(invocationJson)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, _, err = p1.ToSealed(didtest.PersonaBob.PrivKey())
|
||||||
|
require.EqualError(t, err, "private key doesn't match the issuer")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
||||||
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
1
token/invocation/testdata/new.dagjson
vendored
Normal file
1
token/invocation/testdata/new.dagjson
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[{"/":{"bytes":"o/vTvTs8SEkD9QL/eNhhW0fAng/SGBouywCbUnOfsF2RFHxaV02KTCyzgDxlJLZ2XN/Vk5igLmlKL3QIXMaeCQ"}},{"h":{"/":{"bytes":"NO0BcQ"}},"ucan/inv@1.0.0-rc.1":{"args":{"headers":{"Content-Type":"application/json"},"payload":{"body":"UCAN is great","draft":true,"title":"UCAN for Fun and Profit","topics":["authz","journal"]},"uri":"https://example.com/blog/posts"},"cmd":"/crud/create","exp":1730812145,"iss":"did:key:z6MkvMGkN5nbUQLBVqJhr13Zdqyh9rR1VuF16PuZbfocBxpv","meta":{"env":"development","tags":["blog","post","pr#123"]},"nonce":{"/":{"bytes":"q1AH6MJrqoTH6av7"}},"prf":[{"/":"bafyreigx3qxd2cndpe66j2mdssj773ecv7tqd7wovcnz5raguw6lj7sjoe"},{"/":"bafyreib34ira254zdqgehz6f2bhwme2ja2re3ltcalejv4x4tkcveujvpa"},{"/":"bafyreibkb66tpo2ixqx3fe5hmekkbuasrod6olt5bwm5u5pi726mduuwlq"}],"sub":"did:key:z6MkuFj35aiTL7YQiVMobuSeUQju92g7wZzufS3HAc6NFFcQ"}}]
|
||||||
Reference in New Issue
Block a user