Compare commits
1 Commits
v1
...
match-trac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b05a136b81 |
77
Readme.md
77
Readme.md
@@ -1,77 +0,0 @@
|
|||||||
<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
BIN
assets/logo.png
Binary file not shown.
|
Before 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.
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
// 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,7 +3,6 @@ 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,7 +1,5 @@
|
|||||||
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
137
pkg/args/args.go
@@ -1,137 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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,34 +98,7 @@ 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 {
|
||||||
if c == separator {
|
return strings.Split(string(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,21 +73,6 @@ 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,13 +40,6 @@ 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,7 +2,6 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"iter"
|
"iter"
|
||||||
@@ -35,16 +34,13 @@ 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, delegation.ErrDelegationNotFound
|
return nil, fmt.Errorf("not a delegation token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllDelegations returns all the delegation.Token in the container.
|
// GetAllDelegations returns all the delegation.Token in the container.
|
||||||
@@ -102,36 +98,15 @@ func FromCbor(r io.Reader) (Reader, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if n.Kind() != datamodel.Kind_Map {
|
if n.Kind() != datamodel.Kind_List {
|
||||||
return nil, fmt.Errorf("invalid container format: expected map")
|
return nil, fmt.Errorf("not a list")
|
||||||
}
|
|
||||||
if n.Length() != 1 {
|
|
||||||
return nil, fmt.Errorf("invalid container format: expected single version key")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the first (and only) key-value pair
|
ctn := make(Reader, n.Length())
|
||||||
it := n.MapIterator()
|
|
||||||
key, tokensNode, err := it.Next()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
version, err := key.AsString()
|
it := n.ListIterator()
|
||||||
if err != nil {
|
for !it.Done() {
|
||||||
return nil, fmt.Errorf("invalid container format: version must be string")
|
_, val, err := it.Next()
|
||||||
}
|
|
||||||
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(iss, aud, cmd, pol, opts...)
|
t, err := delegation.New(priv, aud, cmd, pol, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -176,30 +176,3 @@ 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,8 +14,6 @@ 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
|
||||||
|
|
||||||
@@ -45,12 +43,10 @@ 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.BuildMap(basicnode.Prototype.Any, 1, func(ma datamodel.MapAssembler) {
|
node, err := qp.BuildList(basicnode.Prototype.Any, int64(len(ctn)), func(la datamodel.ListAssembler) {
|
||||||
qp.MapEntry(ma, currentContainerVersion, qp.List(int64(len(ctn)), func(la datamodel.ListAssembler) {
|
for _, bytes := range ctn {
|
||||||
for _, bytes := range ctn {
|
qp.ListEntry(la, qp.Bytes(bytes))
|
||||||
qp.ListEntry(la, qp.Bytes(bytes))
|
}
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
130
pkg/meta/meta.go
130
pkg/meta/meta.go
@@ -1,28 +1,24 @@
|
|||||||
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 ErrNotFound = errors.New("key not found in meta")
|
var ErrUnsupported = fmt.Errorf("failure adding unsupported type to meta")
|
||||||
|
|
||||||
var ErrNotEncryptable = errors.New("value of this type cannot be encrypted")
|
var ErrNotFound = fmt.Errorf("key-value not found in meta")
|
||||||
|
|
||||||
// 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
|
// This also serves as a way to construct the underlying IPLD data with minimum allocations and transformations,
|
||||||
// and transformations, while hiding the IPLD complexity from the caller.
|
// 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
|
||||||
}
|
}
|
||||||
@@ -54,21 +50,6 @@ 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.
|
||||||
@@ -102,21 +83,6 @@ 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.
|
||||||
@@ -129,46 +95,33 @@ 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 val are any CBOR compatible type, or directly IPLD values.
|
// Accepted types for the value are: bool, string, int, int32, int64, []byte,
|
||||||
|
// and ipld.Node.
|
||||||
func (m *Meta) Add(key string, val any) error {
|
func (m *Meta) Add(key string, val any) error {
|
||||||
if _, ok := m.Values[key]; ok {
|
|
||||||
return fmt.Errorf("duplicate key %q", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
node, err := literal.Any(val)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Keys = append(m.Keys, key)
|
|
||||||
m.Values[key] = node
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddEncrypted adds a key/value pair in the meta set.
|
|
||||||
// The value is encrypted with the given encryptionKey.
|
|
||||||
// Accepted types for the value are: string, []byte.
|
|
||||||
func (m *Meta) AddEncrypted(key string, val any, encryptionKey []byte) error {
|
|
||||||
var encrypted []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch val := val.(type) {
|
switch val := val.(type) {
|
||||||
|
case bool:
|
||||||
|
m.Values[key] = basicnode.NewBool(val)
|
||||||
case string:
|
case string:
|
||||||
encrypted, err = crypto.EncryptWithAESKey([]byte(val), encryptionKey)
|
m.Values[key] = basicnode.NewString(val)
|
||||||
if err != nil {
|
case int:
|
||||||
return err
|
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:
|
case []byte:
|
||||||
encrypted, err = crypto.EncryptWithAESKey(val, encryptionKey)
|
m.Values[key] = basicnode.NewBytes(val)
|
||||||
if err != nil {
|
case datamodel.Node:
|
||||||
return err
|
m.Values[key] = val
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return ErrNotEncryptable
|
return fmt.Errorf("%w: %s", ErrUnsupported, fqtn(val))
|
||||||
}
|
}
|
||||||
|
m.Keys = append(m.Keys, key)
|
||||||
return m.Add(key, encrypted)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Equals tells if two Meta hold the same key/values.
|
// Equals tells if two Meta hold the same key/values.
|
||||||
@@ -191,19 +144,18 @@ 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 {
|
||||||
buf.WriteString("\n\t")
|
if i > 0 {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
i++
|
||||||
buf.WriteString(key)
|
buf.WriteString(key)
|
||||||
buf.WriteString(": ")
|
buf.WriteString(":")
|
||||||
buf.WriteString(strings.ReplaceAll(printer.Sprint(node), "\n", "\n\t"))
|
buf.WriteString(printer.Sprint(node))
|
||||||
buf.WriteString(",")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.Values) > 0 {
|
|
||||||
buf.WriteString("\n")
|
|
||||||
}
|
|
||||||
buf.WriteString("}")
|
buf.WriteString("}")
|
||||||
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,3 +163,15 @@ 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,64 +14,11 @@ func TestMeta_Add(t *testing.T) {
|
|||||||
|
|
||||||
type Unsupported struct{}
|
type Unsupported struct{}
|
||||||
|
|
||||||
t.Run("error if not primitive or Node", func(t *testing.T) {
|
t.Run("error if not primative or Node", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
err := (&meta.Meta{}).Add("invalid", &Unsupported{})
|
||||||
require.Error(t, err)
|
require.ErrorIs(t, err, meta.ErrUnsupported)
|
||||||
})
|
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,10 +17,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -33,10 +29,6 @@ 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,7 +4,6 @@ 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"
|
||||||
@@ -34,14 +33,8 @@ 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) {
|
||||||
// deterministic iteration
|
for k, v := range m {
|
||||||
keys := make([]string, 0, len(m))
|
qp.MapEntry(ma, k, anyAssemble(v))
|
||||||
for key := range m {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, key := range keys {
|
|
||||||
qp.MapEntry(ma, key, anyAssemble(m[key]))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -55,64 +48,6 @@ 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
|
||||||
@@ -155,14 +90,10 @@ func anyAssemble(val any) qp.Assemble {
|
|||||||
if rt.Key().Kind() != reflect.String {
|
if rt.Key().Kind() != reflect.String {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// deterministic iteration
|
it := rv.MapRange()
|
||||||
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 _, key := range keys {
|
for it.Next() {
|
||||||
qp.MapEntry(ma, key.String(), anyAssemble(rv.MapIndex(key)))
|
qp.MapEntry(ma, it.Key().String(), anyAssemble(it.Value()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
@@ -175,11 +106,6 @@ 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,9 +3,7 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -55,7 +53,6 @@ func TestMap(t *testing.T) {
|
|||||||
"barbar": "foo",
|
"barbar": "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"link": cid.MustParse("bafzbeigai3eoy2ccc7ybwjfz5r3rdxqrinwi4rwytly24tdbh6yk7zslrm"),
|
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -118,140 +115,6 @@ 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,19 +9,21 @@ 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.
|
||||||
// The first Statement failing to match is returned as well.
|
func (p Policy) Match(node datamodel.Node) bool {
|
||||||
func (p Policy) Match(node datamodel.Node) (bool, Statement) {
|
|
||||||
for _, stmt := range p {
|
for _, stmt := range p {
|
||||||
res, leaf := matchStatement(stmt, node)
|
res, _ := matchStatement(stmt, node)
|
||||||
switch res {
|
switch res {
|
||||||
case matchResultNoData, matchResultFalse:
|
case matchResultNoData, matchResultFalse:
|
||||||
return false, leaf
|
return false
|
||||||
case matchResultOptionalNoData, matchResultTrue:
|
case matchResultOptionalNoData, matchResultTrue:
|
||||||
// continue
|
// continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -60,7 +62,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) (_ matchResult, leafMost Statement) {
|
func matchStatement(cur Statement, node ipld.Node) (output 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
|
||||||
@@ -68,6 +70,11 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
|||||||
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:
|
||||||
@@ -132,9 +139,9 @@ func matchStatement(cur Statement, node ipld.Node) (_ matchResult, leafMost Stat
|
|||||||
case matchResultNoData, matchResultOptionalNoData:
|
case matchResultNoData, matchResultOptionalNoData:
|
||||||
return res, leaf
|
return res, leaf
|
||||||
case matchResultTrue:
|
case matchResultTrue:
|
||||||
return matchResultFalse, cur
|
return matchResultFalse, leaf
|
||||||
case matchResultFalse:
|
case matchResultFalse:
|
||||||
return matchResultTrue, nil
|
return matchResultTrue, leaf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case KindAnd:
|
case KindAnd:
|
||||||
@@ -275,3 +282,18 @@ 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,11 +7,8 @@ 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"
|
||||||
@@ -20,252 +17,228 @@ 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) {
|
||||||
nd := literal.String("test")
|
np := basicnode.Prototype.String
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignString("test")
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.String("test")))
|
pol := MustConstruct(Equal(".", literal.String("test")))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Int(138)))
|
pol := MustConstruct(Equal(".", literal.Int(138)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Float(1.138)
|
np := basicnode.Prototype.Float
|
||||||
|
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, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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")}
|
||||||
|
|
||||||
nd := literal.Link(l0)
|
np := basicnode.Prototype.Link
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignLink(l0)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
pol := MustConstruct(Equal(".", literal.Link(l0)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd, _ := literal.Map(map[string]any{
|
np := basicnode.Prototype.Map
|
||||||
"foo": "bar",
|
nb := np.NewBuilder()
|
||||||
})
|
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, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd, _ := literal.List([]any{"foo"})
|
np := basicnode.Prototype.List
|
||||||
|
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, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
pol := MustConstruct(GreaterThan(".", literal.Int(1)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Int(1)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Float(1.38)
|
np := basicnode.Prototype.Float
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignFloat(1.38)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
pol := MustConstruct(GreaterThan(".", literal.Float(1)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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) {
|
||||||
nd := literal.Float(1.38)
|
np := basicnode.Prototype.Float
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignFloat(1.38)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
pol := MustConstruct(GreaterThanOrEqual(".", literal.Float(1)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
pol := MustConstruct(LessThan(".", literal.Int(1138)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
pol := MustConstruct(LessThanOrEqual(".", literal.Int(1138)))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Bool(false)
|
np := basicnode.Prototype.Bool
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignBool(false)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
pol := MustConstruct(Not(Equal(".", literal.Bool(true))))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(
|
pol := MustConstruct(
|
||||||
And(
|
And(
|
||||||
@@ -273,9 +246,8 @@ func TestMatch(t *testing.T) {
|
|||||||
LessThan(".", literal.Int(1138)),
|
LessThan(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok, leaf := pol.Match(nd)
|
ok := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Nil(t, leaf)
|
|
||||||
|
|
||||||
pol = MustConstruct(
|
pol = MustConstruct(
|
||||||
And(
|
And(
|
||||||
@@ -283,18 +255,19 @@ func TestMatch(t *testing.T) {
|
|||||||
Equal(".", literal.Int(1138)),
|
Equal(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok, leaf = pol.Match(nd)
|
ok = 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd := literal.Int(138)
|
np := basicnode.Prototype.Int
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignInt(138)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(
|
pol := MustConstruct(
|
||||||
Or(
|
Or(
|
||||||
@@ -302,9 +275,8 @@ func TestMatch(t *testing.T) {
|
|||||||
LessThan(".", literal.Int(1138)),
|
LessThan(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok, leaf := pol.Match(nd)
|
ok := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Nil(t, leaf)
|
|
||||||
|
|
||||||
pol = MustConstruct(
|
pol = MustConstruct(
|
||||||
Or(
|
Or(
|
||||||
@@ -312,14 +284,12 @@ func TestMatch(t *testing.T) {
|
|||||||
Equal(".", literal.Int(1138)),
|
Equal(".", literal.Int(1138)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ok, leaf = pol.Match(nd)
|
ok = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
require.Equal(t, pol[0], leaf)
|
|
||||||
|
|
||||||
pol = MustConstruct(Or())
|
pol = MustConstruct(Or())
|
||||||
ok, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
@@ -333,12 +303,14 @@ 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) {
|
||||||
nd := literal.String(s)
|
np := basicnode.Prototype.String
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignString(s)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Like(".", pattern))
|
pol := MustConstruct(Like(".", pattern))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := pol.Match(nd)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
require.Nil(t, leaf)
|
|
||||||
})
|
})
|
||||||
}(s)
|
}(s)
|
||||||
}
|
}
|
||||||
@@ -352,56 +324,70 @@ 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) {
|
||||||
nd := literal.String(s)
|
np := basicnode.Prototype.String
|
||||||
|
nb := np.NewBuilder()
|
||||||
|
nb.AssignString(s)
|
||||||
|
nd := nb.Build()
|
||||||
|
|
||||||
pol := MustConstruct(Like(".", pattern))
|
pol := MustConstruct(Like(".", pattern))
|
||||||
ok, leaf := pol.Match(nd)
|
ok := 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) {
|
||||||
nd, _ := literal.List([]any{
|
np := basicnode.Prototype.List
|
||||||
map[string]int{"value": 5},
|
nb := np.NewBuilder()
|
||||||
map[string]int{"value": 10},
|
la, _ := nb.BeginList(5)
|
||||||
map[string]int{"value": 20},
|
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||||
map[string]int{"value": 50},
|
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||||
map[string]int{"value": 100},
|
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||||
})
|
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, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = 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) {
|
||||||
nd, _ := literal.List([]any{
|
np := basicnode.Prototype.List
|
||||||
map[string]int{"value": 5},
|
nb := np.NewBuilder()
|
||||||
map[string]int{"value": 10},
|
la, _ := nb.BeginList(5)
|
||||||
map[string]int{"value": 20},
|
la.AssembleValue().AssignNode(buildValueNode(5))
|
||||||
map[string]int{"value": 50},
|
la.AssembleValue().AssignNode(buildValueNode(10))
|
||||||
map[string]int{"value": 100},
|
la.AssembleValue().AssignNode(buildValueNode(20))
|
||||||
})
|
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, leaf := pol.Match(nd)
|
ok := 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, leaf = pol.Match(nd)
|
ok = pol.Match(nd)
|
||||||
require.False(t, ok)
|
require.False(t, ok)
|
||||||
require.Equal(t, pol[0], leaf)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -419,8 +405,7 @@ func TestPolicyExamples(t *testing.T) {
|
|||||||
|
|
||||||
pol, err := FromDagJson(policy)
|
pol, err := FromDagJson(policy)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
res, _ := pol.Match(data)
|
return pol.Match(data)
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("And", func(t *testing.T) {
|
t.Run("And", func(t *testing.T) {
|
||||||
@@ -524,7 +509,7 @@ func FuzzMatch(f *testing.F) {
|
|||||||
t.Skip()
|
t.Skip()
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = policy.Match(dataNode)
|
policy.Match(dataNode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,7 +584,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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -904,55 +889,3 @@ 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,37 +37,6 @@ 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,6 +1,7 @@
|
|||||||
package selector
|
package selector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -353,6 +354,7 @@ 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())
|
||||||
@@ -402,11 +404,13 @@ 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) {
|
||||||
@@ -550,3 +554,9 @@ 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,6 +2,7 @@ package selector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,6 +87,8 @@ 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)
|
||||||
})
|
})
|
||||||
@@ -97,6 +101,8 @@ 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)
|
||||||
|
|
||||||
@@ -104,6 +110,8 @@ 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)
|
||||||
})
|
})
|
||||||
@@ -116,6 +124,8 @@ 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)
|
||||||
|
|
||||||
@@ -132,6 +142,8 @@ 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")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -152,6 +164,8 @@ 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,12 +1,15 @@
|
|||||||
package selector_test
|
package selector_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
"github.com/ipld/go-ipld-prime/codec/dagjson"
|
||||||
|
"github.com/ipld/go-ipld-prime/datamodel"
|
||||||
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
basicnode "github.com/ipld/go-ipld-prime/node/basic"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
"github.com/ucan-wg/go-ucan/pkg/policy/selector"
|
||||||
@@ -52,7 +55,7 @@ func TestSupportedForms(t *testing.T) {
|
|||||||
require.NotNil(t, res)
|
require.NotNil(t, res)
|
||||||
|
|
||||||
exp := makeNode(t, tc.Output)
|
exp := makeNode(t, tc.Output)
|
||||||
require.True(t, ipld.DeepEqual(exp, res))
|
equalIPLD(t, exp, res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +106,23 @@ 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,16 +10,17 @@ 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.
|
||||||
@@ -46,10 +47,15 @@ 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 assembled
|
// When creating a delegated token, the Issuer's (iss) DID is assembed
|
||||||
// using the public key associated with the private key sent as the first
|
// using the public key associated with the private key sent as the first
|
||||||
// parameter.
|
// parameter.
|
||||||
func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
func New(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
|
iss, err := did.FromPrivKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
tkn := &Token{
|
tkn := &Token{
|
||||||
issuer: iss,
|
issuer: iss,
|
||||||
audience: aud,
|
audience: aud,
|
||||||
@@ -66,9 +72,8 @@ func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Optio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
if len(tkn.nonce) == 0 {
|
if len(tkn.nonce) == 0 {
|
||||||
tkn.nonce, err = nonce.Generate()
|
tkn.nonce, err = generateNonce()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -87,10 +92,15 @@ func New(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Optio
|
|||||||
// 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(iss, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
func Root(privKey crypto.PrivKey, aud did.DID, cmd command.Command, pol policy.Policy, opts ...Option) (*Token, error) {
|
||||||
opts = append(opts, WithSubject(iss))
|
sub, err := did.FromPrivKey(privKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return New(iss, aud, cmd, pol, opts...)
|
opts = append(opts, WithSubject(sub))
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -142,24 +152,6 @@ 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
|
||||||
|
|
||||||
@@ -192,19 +184,27 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|||||||
return nil, fmt.Errorf("parse iss: %w", err)
|
return nil, fmt.Errorf("parse iss: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tkn.audience, err = did.Parse(m.Aud); err != nil {
|
tkn.audience, err = did.Parse(m.Aud)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse audience: %w", err)
|
return nil, fmt.Errorf("parse audience: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tkn.subject, err = parse.OptionalDID(m.Sub); err != nil {
|
if m.Sub != nil {
|
||||||
return nil, fmt.Errorf("parse subject: %w", err)
|
tkn.subject, err = did.Parse(*m.Sub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse subject: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tkn.subject = did.Undef
|
||||||
}
|
}
|
||||||
|
|
||||||
if tkn.command, err = command.Parse(m.Cmd); err != nil {
|
tkn.command, err = command.Parse(m.Cmd)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse command: %w", err)
|
return nil, fmt.Errorf("parse command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tkn.policy, err = policy.FromIPLD(m.Pol); err != nil {
|
tkn.policy, err = policy.FromIPLD(m.Pol)
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parse policy: %w", err)
|
return nil, fmt.Errorf("parse policy: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,8 +215,15 @@ func tokenFromModel(m tokenPayloadModel) (*Token, error) {
|
|||||||
|
|
||||||
tkn.meta = m.Meta
|
tkn.meta = m.Meta
|
||||||
|
|
||||||
tkn.notBefore = parse.OptionalTimestamp(m.Nbf)
|
if m.Nbf != nil {
|
||||||
tkn.expiration = parse.OptionalTimestamp(m.Exp)
|
t := time.Unix(*m.Nbf, 0)
|
||||||
|
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
|
||||||
@@ -224,3 +231,14 @@ 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/didtest"
|
"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/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,8 +17,16 @@ import (
|
|||||||
const (
|
const (
|
||||||
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
nonce = "6roDhGi0kiNriQAz7J3d+bOeoI/tj8ENikmQNbtjnD0"
|
||||||
|
|
||||||
subJectCmd = "/foo/bar"
|
AudiencePrivKeyCfg = "CAESQL1hvbXpiuk2pWr/XFbfHJcZNpJ7S90iTA3wSCTc/BPRneCwPnCZb6c0vlD6ytDWqaOt0HEOPYnqEpnzoBDprSM="
|
||||||
subjectPol = `
|
AudienceDID = "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv"
|
||||||
|
|
||||||
|
issuerPrivKeyCfg = "CAESQLSql38oDmQXIihFFaYIjb73mwbPsc7MIqn4o8PN4kRNnKfHkw5gRP1IV9b6d0estqkZayGZ2vqMAbhRixjgkDU="
|
||||||
|
issuerDID = "did:key:z6Mkpzn2n3ZGT2VaqMGSQC3tzmzV4TS9S71iFsDXE1WnoNH2"
|
||||||
|
|
||||||
|
subjectPrivKeyCfg = "CAESQL9RtjZ4dQBeXtvDe53UyvslSd64kSGevjdNiA1IP+hey5i/3PfRXSuDr71UeJUo1fLzZ7mGldZCOZL3gsIQz5c="
|
||||||
|
subjectDID = "did:key:z6MktA1uBdCpq4uJBqE9jjMiLyxZBg9a6xgPPKJjMqss6Zc2"
|
||||||
|
subJectCmd = "/foo/bar"
|
||||||
|
subjectPol = `
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
"==",
|
"==",
|
||||||
@@ -56,15 +64,20 @@ const (
|
|||||||
]
|
]
|
||||||
`
|
`
|
||||||
|
|
||||||
newCID = "zdpuAwa4qv3ncMDPeDoqVxjZy3JoyWsbqUzm94rdA1AvRFkkw"
|
newCID = "zdpuAn9JgGPvnt2WCmTaKktZdbuvcVGTg9bUT5kQaufwUtZ6e"
|
||||||
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)
|
||||||
|
|
||||||
@@ -75,25 +88,27 @@ 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(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
tkn, err := delegation.New(privKey, aud, cmd, pol,
|
||||||
delegation.WithNonce([]byte(nonce)),
|
delegation.WithNonce([]byte(nonce)),
|
||||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
delegation.WithSubject(sub),
|
||||||
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(didtest.PersonaAlice.PrivKey())
|
data, err := tkn.ToDagJson(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(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
tkn, err := delegation.Root(privKey, aud, cmd, pol,
|
||||||
delegation.WithNonce([]byte(nonce)),
|
delegation.WithNonce([]byte(nonce)),
|
||||||
delegation.WithExpiration(exp),
|
delegation.WithExpiration(exp),
|
||||||
delegation.WithMeta("foo", "fooo"),
|
delegation.WithMeta("foo", "fooo"),
|
||||||
@@ -101,109 +116,21 @@ func TestConstructors(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
data, err := tkn.ToDagJson(didtest.PersonaAlice.PrivKey())
|
data, err := tkn.ToDagJson(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 TestEncryptedMeta(t *testing.T) {
|
func privKey(t require.TestingT, privKeyCfg string) crypto.PrivKey {
|
||||||
t.Parallel()
|
privKeyMar, err := crypto.ConfigDecodeKey(privKeyCfg)
|
||||||
|
|
||||||
cmd, err := command.Parse(subJectCmd)
|
|
||||||
require.NoError(t, err)
|
|
||||||
pol, err := policy.FromDagJson(subjectPol)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
encryptionKey, err := base64.StdEncoding.DecodeString(aesKey)
|
privKey, err := crypto.UnmarshalPrivateKey(privKeyMar)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, encryptionKey, 32)
|
|
||||||
|
|
||||||
tests := []struct {
|
return privKey
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# 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.
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,33 +0,0 @@
|
|||||||
// 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
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
// 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,
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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,6 +2,7 @@ package delegation_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -12,8 +13,9 @@ 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/didtest"
|
"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/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"
|
||||||
@@ -24,7 +26,15 @@ 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() {
|
||||||
fmt.Println("issDid:", didtest.PersonaBob.DID().String())
|
issPriv, issPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
|
issDid, err := did.FromPubKey(issPub)
|
||||||
|
printThenPanicOnErr(err)
|
||||||
|
fmt.Println("issDid:", issDid)
|
||||||
|
|
||||||
|
audDid := did.MustParse(AudienceDID)
|
||||||
|
subDid := did.MustParse(subjectDID)
|
||||||
|
|
||||||
// The command defines the shape of the arguments that will be evaluated against the policy
|
// 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")
|
||||||
@@ -41,8 +51,8 @@ func ExampleNew() {
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tkn, err := delegation.New(didtest.PersonaBob.DID(), didtest.PersonaCarol.DID(), cmd, pol,
|
tkn, err := delegation.New(issPriv, audDid, cmd, pol,
|
||||||
delegation.WithSubject(didtest.PersonaAlice.DID()),
|
delegation.WithSubject(subDid),
|
||||||
delegation.WithExpirationIn(time.Hour),
|
delegation.WithExpirationIn(time.Hour),
|
||||||
delegation.WithNotBeforeIn(time.Minute),
|
delegation.WithNotBeforeIn(time.Minute),
|
||||||
delegation.WithMeta("foo", "bar"),
|
delegation.WithMeta("foo", "bar"),
|
||||||
@@ -51,91 +61,101 @@ 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(didtest.PersonaBob.PrivKey())
|
data, id, err := tkn.ToSealed(issPriv)
|
||||||
printThenPanicOnErr(err)
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
printCIDAndSealed(id, data)
|
printCIDAndSealed(id, data)
|
||||||
|
|
||||||
// Example output:
|
// Example output:
|
||||||
//
|
//
|
||||||
// issDid: did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU
|
// issDid: did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe
|
||||||
//
|
//
|
||||||
// CID (base58BTC): zdpuAsqfZkgg2jgZyob23sq1J9xwtf9PHgt1PsskVCMq7Vvxk
|
// CID (base58BTC): zdpuAv6g2eJSc4RJwEpmooGLVK4wJ4CZpnM92tPVYt5jtMoLW
|
||||||
|
//
|
||||||
|
// 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": "5rvl8uKmDVGvAVSt4m/0MGiXl9dZwljJJ9m2qHCoIB617l26UvMxyH5uvN9hM7ozfVATiq4mLhoGgm9IGnEEAg"
|
// "bytes": "NO0BcQ"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// {
|
// "ucan/dlg@1.0.0-rc.1": {
|
||||||
// "h": {
|
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||||
|
// "cmd": "/foo/bar",
|
||||||
|
// "exp": 1728933098,
|
||||||
|
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
||||||
|
// "meta": {
|
||||||
|
// "baz": 123,
|
||||||
|
// "foo": "bar"
|
||||||
|
// },
|
||||||
|
// "nbf": 1728929558,
|
||||||
|
// "nonce": {
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "NO0BcQ"
|
// "bytes": "u0HMgJ5Y+M84I/66"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "ucan/dlg@1.0.0-rc.1": {
|
// "pol": [
|
||||||
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
// [
|
||||||
// "cmd": "/foo/bar",
|
// "==",
|
||||||
// "exp": 1728933098,
|
// ".status",
|
||||||
// "iss": "did:key:z6MkhVFznPeR572rTK51UjoTNpnF8cxuWfPm9oBMPr7y8ABe",
|
// "draft"
|
||||||
// "meta": {
|
// ],
|
||||||
// "baz": 123,
|
// [
|
||||||
// "foo": "bar"
|
// "all",
|
||||||
// },
|
// ".reviewer",
|
||||||
// "nbf": 1728929558,
|
|
||||||
// "nonce": {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "u0HMgJ5Y+M84I/66"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "pol": [
|
|
||||||
// [
|
// [
|
||||||
// "==",
|
// "like",
|
||||||
// ".status",
|
// ".email",
|
||||||
// "draft"
|
// "*@example.com"
|
||||||
// ],
|
// ]
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "any",
|
||||||
|
// ".tags",
|
||||||
// [
|
// [
|
||||||
// "all",
|
// "or",
|
||||||
// ".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")
|
||||||
|
|
||||||
@@ -151,7 +171,7 @@ func ExampleRoot() {
|
|||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
tkn, err := delegation.Root(didtest.PersonaAlice.DID(), didtest.PersonaBob.DID(), cmd, pol,
|
tkn, err := delegation.Root(issPriv, audDid, 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"),
|
||||||
@@ -160,7 +180,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(didtest.PersonaAlice.PrivKey())
|
data, id, err := tkn.ToSealed(issPriv)
|
||||||
printThenPanicOnErr(err)
|
printThenPanicOnErr(err)
|
||||||
|
|
||||||
printCIDAndSealed(id, data)
|
printCIDAndSealed(id, data)
|
||||||
@@ -169,82 +189,82 @@ func ExampleRoot() {
|
|||||||
//
|
//
|
||||||
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
|
// issDid: did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF
|
||||||
//
|
//
|
||||||
// CID (base58BTC): zdpuAkwYz8nY7uU8j3F6wVTfFY1VEoExwvUAYBEwRWfTozddE
|
// CID (base58BTC): zdpuAwLojgfvFCbjz2FsKrvN1khDQ9mFGT6b6pxjMfz73Roed
|
||||||
//
|
//
|
||||||
// DAG-CBOR (base64) out: glhAVpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSA6JhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rdkpQbUVaWlliZ2l3MW91VDFvb3VUc1RGQkhKU3RzOW9waFZzTmdjUm1ZeFVjY21kaC9mb28vYmFyY2V4cBpnROVoY2lzc3g4ZGlkOmtleTp6Nk1rdXVrazJza0RYTFFuN05LM0VoOWpNbmRZZnZEQnh4a3RncGlkSkFxYjdNM3BjbmJmGmdE15RjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa3V1a2syc2tEWExRbjdOSzNFaDlqTW5kWWZ2REJ4eGt0Z3BpZEpBcWI3TTNwZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMwzDc03WBciJIGPWG
|
// DAG-CBOR (base64) out: glhA6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBqJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGpY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cBpnDW0wY2lzc3g4ZGlkOmtleTp6Nk1rbldKcXoxN1k0QWZzWFNKVUZLb211QlI0R1RrVmlNN2tKWXV0elRNa0N5RkZjbmJmGmcNX1xjcG9sg4NiPT1nLnN0YXR1c2VkcmFmdINjYWxsaS5yZXZpZXdlcoNkbGlrZWYuZW1haWxtKkBleGFtcGxlLmNvbYNjYW55ZS50YWdzgmJvcoKDYj09YS5kbmV3c4NiPT1hLmVwcmVzc2NzdWJ4OGRpZDprZXk6ejZNa25XSnF6MTdZNEFmc1hTSlVGS29tdUJSNEdUa1ZpTTdrSll1dHpUTWtDeUZGZG1ldGGiY2Jhehh7Y2Zvb2NiYXJlbm9uY2VMJOsjYi1Pq3OIB0La
|
||||||
//
|
//
|
||||||
// Converted to DAG-JSON out:
|
// Converted to DAG-JSON out:
|
||||||
// [
|
// [
|
||||||
// {
|
// {
|
||||||
|
// "/": {
|
||||||
|
// "bytes": "6dBhbhhGE36CW22OxjOEIAqdDmBqCNsAhCRljnBdXd7YrVOUG+bnXGCIwd4dTGgpEdmY06PFIl7IXKXCh/ESBg"
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// "h": {
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "VpW67FJ+myNi+azvnw2jivuiqXTuMrDZI2Qdaa8jE1Oi3mkjnm7DyqSQGADcomcuDslMWKmJ+OIyvbPG5PtSAw"
|
// "bytes": "NO0BcQ"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// {
|
// "ucan/dlg@1.0.0-rc.1": {
|
||||||
// "h": {
|
// "aud": "did:key:z6Mkq5YmbJcTrPExNDi26imrTCpKhepjBFBSHqrBDN2ArPkv",
|
||||||
|
// "cmd": "/foo/bar",
|
||||||
|
// "exp": 1728933168,
|
||||||
|
// "iss": "did:key:z6MknWJqz17Y4AfsXSJUFKomuBR4GTkViM7kJYutzTMkCyFF",
|
||||||
|
// "meta": {
|
||||||
|
// "baz": 123,
|
||||||
|
// "foo": "bar"
|
||||||
|
// },
|
||||||
|
// "nbf": 1728929628,
|
||||||
|
// "nonce": {
|
||||||
// "/": {
|
// "/": {
|
||||||
// "bytes": "NO0BcQ"
|
// "bytes": "JOsjYi1Pq3OIB0La"
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
// "ucan/dlg@1.0.0-rc.1": {
|
// "pol": [
|
||||||
// "aud": "did:key:z6MkvJPmEZZYbgiw1ouT1oouTsTFBHJSts9ophVsNgcRmYxU",
|
// [
|
||||||
// "cmd": "/foo/bar",
|
// "==",
|
||||||
// "exp": 1732568424,
|
// ".status",
|
||||||
// "iss": "did:key:z6Mkuukk2skDXLQn7NK3Eh9jMndYfvDBxxktgpidJAqb7M3p",
|
// "draft"
|
||||||
// "meta": {
|
// ],
|
||||||
// "baz": 123,
|
// [
|
||||||
// "foo": "bar"
|
// "all",
|
||||||
// },
|
// ".reviewer",
|
||||||
// "nbf": 1732564884,
|
|
||||||
// "nonce": {
|
|
||||||
// "/": {
|
|
||||||
// "bytes": "wzDc03WBciJIGPWG"
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// "pol": [
|
|
||||||
// [
|
// [
|
||||||
// "==",
|
// "like",
|
||||||
// ".status",
|
// ".email",
|
||||||
// "draft"
|
// "*@example.com"
|
||||||
// ],
|
// ]
|
||||||
|
// ],
|
||||||
|
// [
|
||||||
|
// "any",
|
||||||
|
// ".tags",
|
||||||
// [
|
// [
|
||||||
// "all",
|
// "or",
|
||||||
// ".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 ExampleFromSealed() {
|
func ExampleToken_FromSealed() {
|
||||||
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
const cborBase64 = "glhAmnAkgfjAx4SA5pzJmtaHRJtTGNpF1y6oqb4yhGoM2H2EUGbBYT4rVDjMKBgCjhdGHjipm00L8iR5SsQh3sIEBaJhaEQ07QFxc3VjYW4vZGxnQDEuMC4wLXJjLjGoY2F1ZHg4ZGlkOmtleTp6Nk1rcTVZbWJKY1RyUEV4TkRpMjZpbXJUQ3BLaGVwakJGQlNIcXJCRE4yQXJQa3ZjY21kaC9mb28vYmFyY2V4cPZjaXNzeDhkaWQ6a2V5Ono2TWtwem4ybjNaR1QyVmFxTUdTUUMzdHptelY0VFM5UzcxaUZzRFhFMVdub05IMmNwb2yDg2I9PWcuc3RhdHVzZWRyYWZ0g2NhbGxpLnJldmlld2Vyg2RsaWtlZi5lbWFpbG0qQGV4YW1wbGUuY29tg2NhbnllLnRhZ3OCYm9ygoNiPT1hLmRuZXdzg2I9PWEuZXByZXNzY3N1Yng4ZGlkOmtleTp6Nk1rdEExdUJkQ3BxNHVKQnFFOWpqTWlMeXhaQmc5YTZ4Z1BQS0pqTXFzczZaYzJkbWV0YaBlbm9uY2VMAAECAwQFBgcICQoL"
|
||||||
|
|
||||||
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
cborBytes, err := base64.StdEncoding.DecodeString(cborBase64)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package delegation
|
package delegation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@@ -194,16 +193,8 @@ 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) {
|
||||||
// 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
|
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
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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,22 +44,6 @@ 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
|
||||||
errSchema error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustLoadSchema() *schema.TypeSystem {
|
func mustLoadSchema() *schema.TypeSystem {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
})
|
})
|
||||||
if errSchema != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||||
}
|
}
|
||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package delegation_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ipld/go-ipld-prime"
|
"github.com/ipld/go-ipld-prime"
|
||||||
@@ -10,7 +11,6 @@ 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 := didtest.PersonaAlice.PrivKey()
|
privKey := privKey(t, issuerPrivKeyCfg)
|
||||||
|
|
||||||
t.Run("via buffers", func(t *testing.T) {
|
t.Run("via buffers", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -36,13 +36,18 @@ 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))
|
||||||
})
|
})
|
||||||
@@ -60,6 +65,7 @@ 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))
|
||||||
|
|
||||||
@@ -73,16 +79,6 @@ 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) {
|
||||||
@@ -94,7 +90,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 := didtest.PersonaAlice.PrivKey()
|
privKey := privKey(b, issuerPrivKeyCfg)
|
||||||
|
|
||||||
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":"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"}}]
|
[{"/":{"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"}}]
|
||||||
2
token/delegation/testdata/root.dagjson
vendored
2
token/delegation/testdata/root.dagjson
vendored
@@ -1 +1 @@
|
|||||||
[{"/":{"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"}}]
|
[{"/":{"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"}}]
|
||||||
@@ -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
|
||||||
|
|
||||||
// IsValidNow verifies that the token can be used at the current time, based on expiration or "not before" fields.
|
// Issuer returns the did.DID representing the Token's issuer.
|
||||||
// This does NOT do any other kind of verifications.
|
Issuer() did.DID
|
||||||
IsValidNow() bool
|
// Meta returns the Token's metadata.
|
||||||
// IsValidNow verifies that the token can be used at the given time, based on expiration or "not before" fields.
|
Meta() meta.ReadOnly
|
||||||
// This does NOT do any other kind of verifications.
|
|
||||||
IsValidAt(t time.Time) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Marshaller interface {
|
type Marshaller interface {
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ 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
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
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")
|
|
||||||
)
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
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,132 +8,37 @@
|
|||||||
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 {
|
||||||
// The DID of the Invoker
|
// Issuer DID (invoker)
|
||||||
issuer did.DID
|
issuer did.DID
|
||||||
// The DID of Subject being invoked
|
// Audience DID (receiver/executor)
|
||||||
subject did.DID
|
|
||||||
// The DID of the intended Executor if different from the Subject
|
|
||||||
audience did.DID
|
audience did.DID
|
||||||
|
// Subject DID (subject being invoked)
|
||||||
// The Command
|
subject did.DID
|
||||||
|
// The Command to invoke
|
||||||
command command.Command
|
command command.Command
|
||||||
// The Command's arguments
|
// TODO: args
|
||||||
arguments *args.Args
|
// TODO: prf
|
||||||
// CIDs of the delegation.Token that prove the chain of authority
|
|
||||||
// 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
|
|
||||||
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
|
||||||
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.
|
||||||
@@ -141,31 +46,28 @@ func (t *Token) Issuer() did.DID {
|
|||||||
return t.issuer
|
return t.issuer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subject returns the did.DID representing the Token's subject.
|
|
||||||
func (t *Token) Subject() did.DID {
|
|
||||||
return t.subject
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audience returns the did.DID representing the Token's audience.
|
// Audience returns the did.DID representing the Token's audience.
|
||||||
func (t *Token) Audience() did.DID {
|
func (t *Token) Audience() did.DID {
|
||||||
return t.audience
|
return t.audience
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subject returns the did.DID representing the Token's subject.
|
||||||
|
//
|
||||||
|
// This field may be did.Undef for delegations that are [Powerlined] but
|
||||||
|
// must be equal to the value returned by the Issuer method for root
|
||||||
|
// tokens.
|
||||||
|
func (t *Token) Subject() did.DID {
|
||||||
|
return t.subject
|
||||||
|
}
|
||||||
|
|
||||||
// Command returns the capability's command.Command.
|
// Command returns the capability's command.Command.
|
||||||
func (t *Token) Command() command.Command {
|
func (t *Token) Command() command.Command {
|
||||||
return t.command
|
return t.command
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arguments returns the arguments to be used when the command is
|
// Nonce returns the random Nonce encapsulated in this Token.
|
||||||
// invoked.
|
func (t *Token) Nonce() []byte {
|
||||||
func (t *Token) Arguments() args.ReadOnly {
|
return t.nonce
|
||||||
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.
|
||||||
@@ -173,43 +75,11 @@ 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
|
||||||
|
|
||||||
@@ -220,7 +90,8 @@ 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"))
|
||||||
@@ -229,58 +100,25 @@ 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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if tkn.issuer, err = did.Parse(m.Iss); err != nil {
|
// TODO
|
||||||
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,32 +1,23 @@
|
|||||||
|
|
||||||
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 {
|
||||||
# The DID of the invoker
|
# Issuer DID (sender)
|
||||||
iss DID
|
iss DID
|
||||||
# The Subject being invoked
|
# Audience DID (receiver)
|
||||||
sub DID
|
aud DID
|
||||||
# The DID of the intended Executor if different from the Subject
|
# Principal that the chain is about (the Subject)
|
||||||
aud optional DID
|
sub optional DID
|
||||||
|
|
||||||
# The Command
|
# The Command to eventually invoke
|
||||||
cmd String
|
cmd String
|
||||||
# The Command's Arguments
|
|
||||||
args { String : Any}
|
|
||||||
# Delegations that prove the chain of authority
|
|
||||||
prf [ Link ]
|
|
||||||
|
|
||||||
# Arbitrary Metadata
|
|
||||||
meta optional { String : Any }
|
|
||||||
|
|
||||||
# A unique, random nonce
|
# A unique, random nonce
|
||||||
nonce optional Bytes
|
nonce Bytes
|
||||||
|
|
||||||
|
# Arbitrary Metadata
|
||||||
|
meta {String : Any}
|
||||||
|
|
||||||
# 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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
package invocation
|
package invocation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/ipfs/go-cid"
|
"github.com/ipfs/go-cid"
|
||||||
@@ -194,21 +193,14 @@ 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) {
|
||||||
// sanity check that privKey and issuer are matching
|
var sub *string
|
||||||
issPub, err := t.issuer.PubKey()
|
|
||||||
if err != nil {
|
if t.subject != did.Undef {
|
||||||
return nil, err
|
s := t.subject.String()
|
||||||
}
|
sub = &s
|
||||||
if !issPub.Equals(privKey.GetPublic()) {
|
|
||||||
return nil, fmt.Errorf("private key doesn't match the issuer")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var aud *string
|
// TODO
|
||||||
|
|
||||||
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 {
|
||||||
@@ -216,29 +208,14 @@ 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: aud,
|
Aud: t.audience.String(),
|
||||||
Sub: t.subject.String(),
|
Sub: sub,
|
||||||
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)
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
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,12 +5,10 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -25,17 +23,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
|
||||||
errSchema error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
func mustLoadSchema() *schema.TypeSystem {
|
func mustLoadSchema() *schema.TypeSystem {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
ts, errSchema = ipld.LoadSchemaBytes(schemaBytes)
|
ts, err = ipld.LoadSchemaBytes(schemaBytes)
|
||||||
})
|
})
|
||||||
if errSchema != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to load IPLD schema: %s", errSchema))
|
panic(fmt.Errorf("failed to load IPLD schema: %s", err))
|
||||||
}
|
}
|
||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
@@ -46,34 +44,28 @@ func payloadType() schema.Type {
|
|||||||
|
|
||||||
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
var _ envelope.Tokener = (*tokenPayloadModel)(nil)
|
||||||
|
|
||||||
|
// TODO
|
||||||
type tokenPayloadModel struct {
|
type tokenPayloadModel struct {
|
||||||
// The DID of the Invoker
|
// Issuer DID (sender)
|
||||||
Iss string
|
Iss string
|
||||||
// The DID of Subject being invoked
|
// Audience DID (receiver)
|
||||||
Sub string
|
Aud string
|
||||||
// The DID of the intended Executor if different from the Subject
|
// Principal that the chain is about (the Subject)
|
||||||
Aud *string
|
// optional: can be nil
|
||||||
|
Sub *string
|
||||||
|
|
||||||
// The Command
|
// The Command to eventually invoke
|
||||||
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 {
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
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
1
token/invocation/testdata/new.dagjson
vendored
@@ -1 +0,0 @@
|
|||||||
[{"/":{"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