adjust the toolkit to the new location

This commit is contained in:
Michael Muré
2025-08-05 12:11:20 +02:00
committed by Michael Muré
parent 06f478b9c3
commit 0647e4ff8a
47 changed files with 233 additions and 828 deletions

View File

@@ -8,7 +8,7 @@ Please note that UCAN in itself doesn't enforce any protocol, topology or transp
Your situation may be different from this, and would call for a different setup.
Remember that everything in `go-ucan-toolkit` is essentially helpers, pre-made building blocks. You can use them, change them or make your own.
Remember that everything in `/toolkit` is essentially helpers, pre-made building blocks. You can use them, change them or make your own.
## Scenario 1

View File

@@ -4,10 +4,10 @@ import (
"encoding/json"
"net/http"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/MetaMask/go-did-it"
"github.com/INFURA/go-ucan-toolkit/issuer"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/toolkit/issuer"
)
func RequestResolver(r *http.Request) (*issuer.ResolvedRequest, error) {

View File

@@ -8,12 +8,13 @@ import (
"log"
"net/http"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/INFURA/go-ucan-toolkit/client"
"github.com/INFURA/go-ucan-toolkit/issuer"
"github.com/ucan-wg/go-ucan/toolkit/client"
"github.com/ucan-wg/go-ucan/toolkit/issuer"
)
var _ client.DelegationRequester = &Requester{}
@@ -55,5 +56,5 @@ func (r Requester) RequestDelegation(ctx context.Context, audience did.DID, cmd
return nil, err
}
return issuer.DecodeResponse(res)
return issuer.DecodeResponse(res, audience, cmd, subject)
}

View File

@@ -13,19 +13,20 @@ import (
"syscall"
"time"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/token/delegation"
example "github.com/INFURA/go-ucan-toolkit/_example"
protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer"
"github.com/INFURA/go-ucan-toolkit/client"
"github.com/INFURA/go-ucan-toolkit/issuer"
"github.com/INFURA/go-ucan-toolkit/server/bearer"
example "github.com/ucan-wg/go-ucan/toolkit/_example"
protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer"
"github.com/ucan-wg/go-ucan/toolkit/client"
"github.com/ucan-wg/go-ucan/toolkit/issuer"
"github.com/ucan-wg/go-ucan/toolkit/server/bearer"
)
func main() {
@@ -49,7 +50,7 @@ func main() {
}
}
func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DID,
func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivateKeySigningBytes, d did.DID,
serviceIssuerUrl string, serviceUrl string, serviceDid did.DID) error {
log.Printf("Alice DID is %s", d.String())
@@ -67,7 +68,7 @@ func run(ctx context.Context, ownIssuerUrl string, priv crypto.PrivKey, d did.DI
return delegation.New(iss, aud, cmd, policies, subject)
}
cli, err := client.NewWithIssuer(priv, protocol.NewRequester("http://"+serviceIssuerUrl), issuingLogic)
cli, err := client.NewWithIssuer(priv, d, protocol.NewRequester("http://"+serviceIssuerUrl), issuingLogic)
if err != nil {
return err
}

View File

@@ -12,14 +12,17 @@ import (
"syscall"
"time"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
didkeyctl "github.com/MetaMask/go-did-it/controller/did-key"
"github.com/MetaMask/go-did-it/crypto/ed25519"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
example "github.com/INFURA/go-ucan-toolkit/_example"
protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer"
"github.com/INFURA/go-ucan-toolkit/client"
"github.com/INFURA/go-ucan-toolkit/server/bearer"
example "github.com/ucan-wg/go-ucan/toolkit/_example"
protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer"
"github.com/ucan-wg/go-ucan/toolkit/client"
"github.com/ucan-wg/go-ucan/toolkit/server/bearer"
)
func main() {
@@ -43,14 +46,15 @@ func main() {
func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl string, serviceDid did.DID) error {
// Let's generate a keypair for our client:
priv, d, err := did.GenerateEd25519()
pub, priv, err := ed25519.GenerateKeyPair()
if err != nil {
return err
}
d := didkeyctl.FromPublicKey(pub)
log.Printf("Bob DID is %s", d.String())
cli, err := client.NewClient(priv, protocol.NewRequester("http://"+aliceUrl))
cli, err := client.NewClient(priv, d, protocol.NewRequester("http://"+aliceUrl))
if err != nil {
return err
}
@@ -69,13 +73,13 @@ func run(ctx context.Context, aliceUrl string, aliceDid did.DID, serverUrl strin
select {
case <-ctx.Done():
return nil
case <-time.After(5 * time.Second):
case <-time.After(1 * time.Second):
}
}
}
func makeRequest(ctx context.Context, clientDid did.DID, serviceUrl string, aliceDid did.DID, proofs container.Writer) error {
// we construct a URL that include the our DID and Alice DID as path, as requested by the UCAN policy we get issued
// we construct a URL that include our DID and Alice DID as path, as requested by the UCAN policy we get issued
u, err := url.JoinPath("http://"+serviceUrl, aliceDid.String(), clientDid.String())
if err != nil {
return err

View File

@@ -11,16 +11,17 @@ import (
"syscall"
"time"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/token/delegation"
example "github.com/INFURA/go-ucan-toolkit/_example"
protocol "github.com/INFURA/go-ucan-toolkit/_example/_protocol-issuer"
"github.com/INFURA/go-ucan-toolkit/issuer"
example "github.com/ucan-wg/go-ucan/toolkit/_example"
protocol "github.com/ucan-wg/go-ucan/toolkit/_example/_protocol-issuer"
"github.com/ucan-wg/go-ucan/toolkit/issuer"
)
func main() {
@@ -42,7 +43,7 @@ func main() {
}
}
func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivKey) error {
func run(ctx context.Context, issuerUrl string, servicePrivKey crypto.PrivateKeySigningBytes) error {
issuingLogic := func(iss did.DID, aud did.DID, cmd command.Command) (*delegation.Token, error) {
log.Printf("issuing delegation to %v for %v", aud, cmd)

View File

@@ -9,10 +9,10 @@ import (
"os/signal"
"syscall"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
example "github.com/INFURA/go-ucan-toolkit/_example"
"github.com/INFURA/go-ucan-toolkit/server/exectx"
example "github.com/ucan-wg/go-ucan/toolkit/_example"
"github.com/ucan-wg/go-ucan/toolkit/server/exectx"
)
func main() {

View File

@@ -3,8 +3,10 @@ package example
import (
"encoding/base64"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
didkeyctl "github.com/MetaMask/go-did-it/controller/did-key"
"github.com/MetaMask/go-did-it/crypto"
"github.com/MetaMask/go-did-it/crypto/ed25519"
)
// Endpoints
@@ -16,20 +18,20 @@ var AliceIssuerUrl = ":8082"
// Service
var ServicePrivKey crypto.PrivKey
var ServicePrivKey crypto.PrivateKeySigningBytes
var ServiceDid did.DID
// Alice
var AlicePrivKey crypto.PrivKey
var AlicePrivKey crypto.PrivateKeySigningBytes
var AliceDid did.DID
func init() {
servPrivRaw, _ := base64.StdEncoding.DecodeString("CAESQGs7hPBRBmxH1UmHrdcPrBkecuFUuCWHK0kMJvZYCBqIa35SGxUdXVGuigQDkMpf7xO4C2C2Acl8QTtSrYS7Cnc=")
ServicePrivKey, _ = crypto.UnmarshalPrivateKey(servPrivRaw)
ServiceDid, _ = did.FromPrivKey(ServicePrivKey)
servPrivRaw, _ := base64.StdEncoding.DecodeString("HVcbgoj30c+7zoQzUgpl7Jc7bkXoyvo9bMX5OHaAohpv036EMxuWXGqmEWhFKHPEuRAaIGSURK8pyUYOAseiiQ==")
ServicePrivKey, _ = ed25519.PrivateKeyFromBytes(servPrivRaw)
ServiceDid = didkeyctl.FromPrivateKey(ServicePrivKey)
alicePrivRaw, _ := base64.StdEncoding.DecodeString("CAESQFESA31nDYUhXXwbCNSFvg7M+TOFgyxy0tVX6o+TkJAKqAwDvtGxZeGyUjibGd/op+xOLvzE6BrTIOw62K3yLp8=")
AlicePrivKey, _ = crypto.UnmarshalPrivateKey(alicePrivRaw)
AliceDid, _ = did.FromPrivKey(AlicePrivKey)
alicePrivRaw, _ := base64.StdEncoding.DecodeString("jIIk/4ZBgIzx7fU41AWYRUDjgQmgFTIXxN4WeZAPCjwE04oLfiHgNjwIIZi97a6WwSIL5tFGdkrqDkSmDx95tw==")
AlicePrivKey, _ = ed25519.PrivateKeyFromBytes(alicePrivRaw)
AliceDid = didkeyctl.FromPrivateKey(AlicePrivKey)
}

View File

@@ -5,9 +5,10 @@ import (
"fmt"
"iter"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"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/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
"github.com/ucan-wg/go-ucan/pkg/policy"
@@ -17,17 +18,13 @@ import (
type Client struct {
did did.DID
privKey crypto.PrivKey
privKey crypto.PrivateKeySigningBytes
pool *Pool
requester DelegationRequester
}
func NewClient(privKey crypto.PrivKey, requester DelegationRequester) (*Client, error) {
d, err := did.FromPrivKey(privKey)
if err != nil {
return nil, err
}
func NewClient(privKey crypto.PrivateKeySigningBytes, d did.DID, requester DelegationRequester) (*Client, error) {
return &Client{
did: d,
privKey: privKey,

View File

@@ -6,8 +6,9 @@ import (
"iter"
"time"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/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"
@@ -21,7 +22,7 @@ func ExampleNewClient() {
// requester is an adaptor for a real world issuer, we use a mock in that example
requester := &requesterMock{persona: servicePersona}
client, err := NewClient(clientPersona.PrivKey(), requester)
client, err := NewClient(clientPersona.PrivKey(), clientPersona.DID(), requester)
handleError(err)
cont, err := client.PrepareInvoke(

View File

@@ -5,9 +5,10 @@ import (
"fmt"
"iter"
"github.com/MetaMask/go-did-it"
"github.com/MetaMask/go-did-it/crypto"
"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/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
)
@@ -29,8 +30,8 @@ type WithIssuer struct {
logic DlgIssuingLogic
}
func NewWithIssuer(privKey crypto.PrivKey, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) {
client, err := NewClient(privKey, requester)
func NewWithIssuer(privKey crypto.PrivateKeySigningBytes, d did.DID, requester DelegationRequester, logic DlgIssuingLogic) (*WithIssuer, error) {
client, err := NewClient(privKey, d, requester)
if err != nil {
return nil, err
}

View File

@@ -6,8 +6,9 @@ import (
"sync"
"time"
"github.com/MetaMask/go-did-it"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
)

View File

@@ -4,8 +4,9 @@ import (
"iter"
"math"
"github.com/MetaMask/go-did-it"
"github.com/ipfs/go-cid"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
)
@@ -23,12 +24,12 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd com
// TODO: maybe that should be part of delegation.Token directly?
dlgMatch := func(dlg *delegation.Token, issuer did.DID, cmd command.Command, subject did.DID) bool {
// The Subject of each delegation must equal the invocation's Subject (or Audience if defined). - 4f
if dlg.Subject() != subject {
if !dlg.Subject().Equal(subject) {
return false
}
// 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() != issuer {
if !dlg.Audience().Equal(issuer) {
return false
}
// The command of each delegation must "allow" the one before it. - 4g
@@ -72,7 +73,7 @@ func FindProof(dlgs func() iter.Seq[*delegation.Bundle], issuer did.DID, cmd com
at := cur.bundle
// if it's a root delegation, we found a valid proof
if at.Decoded.Issuer() == at.Decoded.Subject() {
if at.Decoded.Issuer().Equal(at.Decoded.Subject()) {
if len(bestProof) == 0 || len(cur.path) < len(bestProof) || len(cur.path) == len(bestProof) && cur.size < bestSize {
bestProof = append([]cid.Cid{}, cur.path...) // make a copy
bestSize = cur.size

View File

@@ -4,8 +4,9 @@ import (
"iter"
"testing"
"github.com/MetaMask/go-did-it/didtest"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/delegation/delegationtest"

View File

@@ -5,8 +5,9 @@ import (
"iter"
"time"
"github.com/MetaMask/go-did-it"
"github.com/avast/retry-go/v4"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
)

View File

@@ -1,99 +0,0 @@
package client
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"iter"
"net/http"
"net/url"
"github.com/ucan-wg/go-ucan/did"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
)
var _ DelegationRequester = &InfuraRequester{}
type InfuraRequester struct {
baseURL string
}
// NewInfuraRequester create a requester client for the Infura UCAN token issuer.
// dev: http://ucan-issuer-api.commercial-dev.eks-dev.infura.org
// prod: http://ucan-issuer-api.commercial-prod.eks.infura.org
func NewInfuraRequester(baseURL string) *InfuraRequester {
return &InfuraRequester{baseURL: baseURL}
}
func (i InfuraRequester) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) {
p, err := url.JoinPath(i.baseURL, "v1/token/generate-with-did")
if err != nil {
return nil, err
}
payload := struct {
Cmd string `json:"cmd"`
Aud string `json:"aud"`
}{
Cmd: cmd.String(),
Aud: audience.String(),
}
body, err := json.Marshal(payload)
if err != nil {
return nil, err
}
r, err := http.NewRequest(http.MethodPost, p, bytes.NewReader(body))
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Do(r.WithContext(ctx))
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
msg, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("request failed with status %d, then failed to read response body: %w", res.StatusCode, err)
}
return nil, fmt.Errorf("request failed with status %d: %s", res.StatusCode, msg)
}
resp := struct {
Cid string `json:"cid"`
Content string `json:"content"`
}{}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
raw, err := base64.StdEncoding.DecodeString(resp.Content)
if err != nil {
return nil, err
}
tkn, c, err := delegation.FromSealed(raw)
if err != nil {
return nil, err
}
// For sanity, we verify that the delegation we got matches the expected subject,
// meaning that we are talking to the expected issuer.
if tkn.Subject() != subject {
return nil, fmt.Errorf("received token has unexpected subject: expected %s, got %s", subject, tkn.Subject())
}
return func(yield func(*delegation.Bundle, error) bool) {
yield(&delegation.Bundle{
Cid: c,
Decoded: tkn,
Sealed: raw,
}, nil)
}, nil
}

View File

@@ -6,12 +6,12 @@ import (
"iter"
"net/http"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/INFURA/go-ucan-toolkit/client"
"github.com/ucan-wg/go-ucan/toolkit/client"
)
type RequestResolver func(r *http.Request) (*ResolvedRequest, error)
@@ -60,7 +60,7 @@ func HttpWrapper(requester client.DelegationRequester, resolver RequestResolver)
})
}
func DecodeResponse(res *http.Response) (iter.Seq2[*delegation.Bundle, error], error) {
func DecodeResponse(res *http.Response, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) {
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, err := io.ReadAll(res.Body)
@@ -73,9 +73,20 @@ func DecodeResponse(res *http.Response) (iter.Seq2[*delegation.Bundle, error], e
if err != nil {
return nil, err
}
// the container doesn't guarantee the ordering, so we must order the delegation in a chain
proof := client.FindProof(func() iter.Seq[*delegation.Bundle] {
return cont.GetAllDelegations()
}, audience, cmd, subject)
return func(yield func(*delegation.Bundle, error) bool) {
for bundle := range cont.GetAllDelegations() {
if !yield(&bundle, nil) {
for _, c := range proof {
bndl, err := cont.GetDelegationBundle(c)
if err != nil {
yield(nil, err)
return
}
if !yield(bndl, nil) {
return
}
}

View File

@@ -5,12 +5,13 @@ import (
"fmt"
"iter"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
didkeyctl "github.com/MetaMask/go-did-it/controller/did-key"
"github.com/MetaMask/go-did-it/crypto"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/INFURA/go-ucan-toolkit/client"
"github.com/ucan-wg/go-ucan/toolkit/client"
)
// RootIssuingLogic is a function that decides what powers are given to a client.
@@ -29,16 +30,13 @@ var _ client.DelegationRequester = &RootIssuer{}
// Feel free to replace this component with your own flavor.
type RootIssuer struct {
did did.DID
privKey crypto.PrivKey
privKey crypto.PrivateKeySigningBytes
logic RootIssuingLogic
}
func NewRootIssuer(privKey crypto.PrivKey, logic RootIssuingLogic) (*RootIssuer, error) {
d, err := did.FromPrivKey(privKey)
if err != nil {
return nil, err
}
func NewRootIssuer(privKey crypto.PrivateKeySigningBytes, logic RootIssuingLogic) (*RootIssuer, error) {
d := didkeyctl.FromPrivateKey(privKey)
return &RootIssuer{
did: d,
privKey: privKey,
@@ -47,7 +45,7 @@ func NewRootIssuer(privKey crypto.PrivKey, logic RootIssuingLogic) (*RootIssuer,
}
func (r *RootIssuer) RequestDelegation(ctx context.Context, audience did.DID, cmd command.Command, subject did.DID) (iter.Seq2[*delegation.Bundle, error], error) {
if subject != r.did {
if !subject.Equal(r.did) {
return nil, fmt.Errorf("subject DID doesn't match the issuer DID")
}

View File

@@ -4,7 +4,9 @@ import (
"net/http"
"testing"
_ "github.com/MetaMask/go-did-it/verifiers/did-key"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/container"
"github.com/ucan-wg/go-ucan/pkg/container/containertest"
)

View File

@@ -4,9 +4,9 @@ import (
"errors"
"net/http"
"github.com/ucan-wg/go-ucan/did"
"github.com/MetaMask/go-did-it"
"github.com/INFURA/go-ucan-toolkit/server/bearer"
"github.com/ucan-wg/go-ucan/toolkit/server/bearer"
)
// ExtractMW returns an HTTP middleware tasked with:
@@ -38,7 +38,7 @@ func ExtractMW(next http.Handler, serviceDID did.DID) http.Handler {
return
}
if ucanCtx.Invocation().Subject() != serviceDID {
if !ucanCtx.Invocation().Subject().Equal(serviceDID) {
http.Error(w, "UCAN delegation doesn't match the service DID", http.StatusUnauthorized)
return
}

View File

@@ -6,9 +6,10 @@ import (
"net/http/httptest"
"testing"
"github.com/MetaMask/go-did-it/didtest"
"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/container"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"

View File

@@ -6,9 +6,9 @@ import (
"net/http"
"slices"
"github.com/INFURA/go-ethlibs/jsonrpc"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
@@ -16,8 +16,7 @@ import (
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"
"github.com/INFURA/go-ucan-toolkit/server/extargs"
"github.com/ucan-wg/go-ucan/toolkit/server/extargs"
)
var _ delegation.Loader = &UcanCtx{}
@@ -33,9 +32,8 @@ type UcanCtx struct {
meta *meta.Meta // all meta combined, with no overwriting
// argument sources
http *extargs.HttpExtArgs
jsonrpc *extargs.JsonRpcExtArgs
infura *extargs.InfuraExtArgs
http *extargs.HttpExtArgs
custom map[string]*extargs.CustomExtArgs
}
// FromContainer prepare a UcanCtx from a UCAN container, for further evaluation in a server pipeline.
@@ -79,7 +77,7 @@ func FromContainer(cont container.Reader) (*UcanCtx, error) {
chainTo := inv.Issuer()
for _, c := range inv.Proof() {
dlg := ctx.dlgs[c]
if dlg.Audience() != chainTo {
if !dlg.Audience().Equal(chainTo) {
return nil, fmt.Errorf("proof chain is broken or not ordered correctly")
}
chainTo = dlg.Issuer()
@@ -137,28 +135,19 @@ func (ctn *UcanCtx) VerifyHttp(req *http.Request) error {
return ctn.http.Verify()
}
// VerifyJsonRpc verify the delegation's policies against arguments constructed from the JsonRpc request.
// These arguments will be set in the `.jsonrpc` argument key, at the root.
// This function can only be called once per context.
// VerifyCustom verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler.
// These arguments will be set under the given argument key, at the root.
// This function can only be called once per context and key.
// After being used, those constructed arguments will be used in ExecutionAllowed as well.
func (ctn *UcanCtx) VerifyJsonRpc(req *jsonrpc.Request) error {
if ctn.jsonrpc != nil {
panic("only use once per request context")
func (ctn *UcanCtx) VerifyCustom(key string, assembler func(ma datamodel.MapAssembler)) error {
if ctn.custom == nil {
ctn.custom = make(map[string]*extargs.CustomExtArgs)
}
ctn.jsonrpc = extargs.NewJsonRpcExtArgs(ctn.policies, ctn.inv.Arguments(), req)
return ctn.jsonrpc.Verify()
}
// VerifyInfura verify the delegation's policies against arbitrary arguments provider through an IPLD MapAssembler.
// These arguments will be set in the `.inf` argument key, at the root.
// This function can only be called once per context.
// After being used, those constructed arguments will be used in ExecutionAllowed as well.
func (ctn *UcanCtx) VerifyInfura(assembler func(ma datamodel.MapAssembler)) error {
if ctn.infura != nil {
panic("only use once per request context")
if _, ok := ctn.custom[key]; ok {
panic("only use once per request context and key")
}
ctn.infura = extargs.NewInfuraExtArgs(ctn.policies, assembler)
return ctn.infura.Verify()
ctn.custom[key] = extargs.NewCustomExtArgs(key, ctn.policies, assembler)
return ctn.custom[key].Verify()
}
// ExecutionAllowed does the final verification of the invocation.
@@ -174,19 +163,14 @@ func (ctn *UcanCtx) ExecutionAllowed() error {
}
newArgs.Include(httpArgs)
}
if ctn.jsonrpc != nil {
jsonRpcArgs, err := ctn.jsonrpc.Args()
if err != nil {
return nil, err
if ctn.custom != nil {
for _, cea := range ctn.custom {
customArgs, err := cea.Args()
if err != nil {
return nil, err
}
newArgs.Include(customArgs)
}
newArgs.Include(jsonRpcArgs)
}
if ctn.infura != nil {
infuraArgs, err := ctn.infura.Args()
if err != nil {
return nil, err
}
newArgs.Include(infuraArgs)
}
return newArgs, nil

View File

@@ -1,29 +1,26 @@
package exectx_test
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/INFURA/go-ethlibs/jsonrpc"
"github.com/MetaMask/go-did-it/didtest"
"github.com/ipfs/go-cid"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/did/didtest"
"github.com/ucan-wg/go-ucan/pkg/command"
"github.com/ucan-wg/go-ucan/pkg/container"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/token/delegation"
"github.com/ucan-wg/go-ucan/token/invocation"
"github.com/INFURA/go-ucan-toolkit/server/exectx"
"github.com/ucan-wg/go-ucan/toolkit/server/exectx"
)
const (
@@ -45,16 +42,11 @@ func TestUcanCtxFullFlow(t *testing.T) {
// some basic HTTP constraints
policy.Equal(".http.method", literal.String("GET")),
policy.Like(".http.path", "/foo/*"),
// some JsonRpc constraints
policy.Or(
policy.Like(".jsonrpc.method", "eth_*"),
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
),
// some infura constraints
// some custom constraints
// Network
policy.Equal(".inf.ntwk", literal.String(network)),
policy.Equal(".custom.ntwk", literal.String(network)),
// Quota
policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)),
policy.LessThanOrEqual(".custom.quota.ur", literal.Int(1234)),
)
dlg, err := delegation.Root(service.DID(), user.DID(), cmd, pol,
@@ -84,12 +76,7 @@ func TestUcanCtxFullFlow(t *testing.T) {
// MAKING A REQUEST: we pass the container in the Bearer HTTP header
jrpc := jsonrpc.NewRequest()
jrpc.Method = "eth_call"
jrpc.Params = jsonrpc.MustParams("0x599784", true)
jrpcBytes, err := jrpc.MarshalJSON()
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "/foo/bar", bytes.NewReader(jrpcBytes))
req, err := http.NewRequest(http.MethodGet, "/foo/bar", nil)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+string(contBytes))
@@ -132,33 +119,13 @@ func TestUcanCtxFullFlow(t *testing.T) {
})
}
// SERVER: JsonRpc checks
// SERVER: custom args checks
jsonrpcMw := func(next http.Handler) http.Handler {
customArgsMw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ucanCtx, ok := exectx.FromContext(r.Context())
require.True(t, ok)
var jrpc jsonrpc.Request
err := json.NewDecoder(r.Body).Decode(&jrpc)
require.NoError(t, err)
err = ucanCtx.VerifyJsonRpc(&jrpc)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// SERVER: custom infura checks
infuraMw := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ucanCtx, ok := exectx.FromContext(r.Context())
require.True(t, ok)
err := ucanCtx.VerifyInfura(func(ma datamodel.MapAssembler) {
err := ucanCtx.VerifyCustom("custom", func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "ntwk", qp.String(network))
qp.MapEntry(ma, "quota", qp.Map(1, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "ur", qp.Int(1234))
@@ -183,7 +150,7 @@ func TestUcanCtxFullFlow(t *testing.T) {
w.WriteHeader(http.StatusOK)
}
sut := authMw(httpMw(jsonrpcMw(infuraMw(http.HandlerFunc(handler)))))
sut := authMw(httpMw(customArgsMw(http.HandlerFunc(handler))))
rec := httptest.NewRecorder()
sut.ServeHTTP(rec, req)

View File

@@ -8,31 +8,28 @@ In this package, we cross the chasm of the pure UCAN world into our practical ne
## Example
Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request (header and JSONRPC body):
Below is an example of `args` in Dag-Json format, where the values are recomposed server-side from the HTTP request:
```json
{
"http": {
"scheme": "https",
"method": "POST",
"host": "mainnet.infura.io",
"host": "example.com",
"path": ""
},
"jsonrpc": {
"jsonrpc": "2.0",
"method": "eth_blockbynumber",
"params": [],
"id": 1
"custom": {
"foo": "bar"
}
}
```
Those `args` can be evaluated against a delegation's policy, for example:
```json
{
"cmd": "/infura/jsonrpc",
"cmd": "/foo/bar",
"pol": [
["==", ".http.host", "mainnet.infura.io"],
["like", ".jsonrpc.method", "eth_*"]
["==", ".http.host", "example.com"],
["like", ".custom.foo", "ba*"]
]
}
```
@@ -50,7 +47,7 @@ There is a way to get around that, and have the best of both worlds, but **it co
```json
{
"http": "zQmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D",
"jsonrpc": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9"
"custom": "zQmP8jTG1m9GSDJLCbeWhVSVgEzCPPwXRdCRuJtQ5Tz9Kc9"
}
```
@@ -63,4 +60,4 @@ Therefore, the server-side logic is made to have this hashing optional:
- the client can opt out of passing that hash, and won't benefit from the enforced security
The particular hash selected is SHA2-256 of the DAG-CBOR encoded argument, expressed in the form of a Multihash in raw bytes.
The arguments being hashed are the complete map of values, including the root key being replaced (for example `jsonrpc` or `http`).
The arguments being hashed are the complete map of values, including the root key being replaced (for example `http` or `custom` here).

View File

@@ -8,13 +8,13 @@ import (
"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/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy"
)
const InfuraArgsKey = "inf"
type InfuraExtArgs struct {
type CustomExtArgs struct {
key string
pol policy.Policy
originalArgs args.ReadOnly
assembler func(ma datamodel.MapAssembler)
@@ -24,44 +24,44 @@ type InfuraExtArgs struct {
argsIpld ipld.Node
}
func NewInfuraExtArgs(pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *InfuraExtArgs {
return &InfuraExtArgs{pol: pol, assembler: assembler}
func NewCustomExtArgs(key string, pol policy.Policy, assembler func(ma datamodel.MapAssembler)) *CustomExtArgs {
return &CustomExtArgs{key: key, pol: pol, assembler: assembler}
}
func (ia *InfuraExtArgs) Verify() error {
if err := ia.makeArgs(); err != nil {
func (cea *CustomExtArgs) Verify() error {
if err := cea.makeArgs(); err != nil {
return err
}
// Note: InfuraExtArgs doesn't support verifying a hash computed client-side like the other
// Note: CustomExtArgs doesn't support verifying a hash computed client-side like the other
// external args, as the arguments are by nature dynamic. The client can't generate a meaningful hash.
ok, leaf := ia.pol.PartialMatch(ia.argsIpld)
ok, leaf := cea.pol.PartialMatch(cea.argsIpld)
if !ok {
return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String())
}
return nil
}
func (ia *InfuraExtArgs) Args() (*args.Args, error) {
if err := ia.makeArgs(); err != nil {
func (cea *CustomExtArgs) Args() (*args.Args, error) {
if err := cea.makeArgs(); err != nil {
return nil, err
}
return ia.args, nil
return cea.args, nil
}
func (ia *InfuraExtArgs) makeArgs() error {
func (cea *CustomExtArgs) makeArgs() error {
var outerErr error
ia.once.Do(func() {
cea.once.Do(func() {
var err error
ia.args, err = makeInfuraArgs(ia.assembler)
cea.args, err = makeCustomArgs(cea.key, cea.assembler)
if err != nil {
outerErr = err
return
}
ia.argsIpld, err = ia.args.ToIPLD()
cea.argsIpld, err = cea.args.ToIPLD()
if err != nil {
outerErr = err
return
@@ -70,14 +70,14 @@ func (ia *InfuraExtArgs) makeArgs() error {
return outerErr
}
func makeInfuraArgs(assembler func(ma datamodel.MapAssembler)) (*args.Args, error) {
func makeCustomArgs(key string, assembler func(ma datamodel.MapAssembler)) (*args.Args, error) {
n, err := qp.BuildMap(basicnode.Prototype.Any, -1, assembler)
if err != nil {
return nil, err
}
res := args.New()
err = res.Add(InfuraArgsKey, n)
err = res.Add(key, n)
if err != nil {
return nil, err
}

View File

@@ -7,18 +7,19 @@ import (
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/stretchr/testify/require"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
)
func ExampleInfuraExtArgs() {
// Note: this is an example for how to build arguments, but you likely want to use InfuraExtArgs
func ExampleCustomExtArgs() {
// Note: this is an example for how to build arguments, but you likely want to use CustomExtArgs
// through UcanCtx.
pol := policy.Policy{} // policies from the delegations
// We will construct the following args:
// {
// "key": {
// "ntwk":"eth-mainnet",
// "quota":{
// "ur":1234,
@@ -29,7 +30,7 @@ func ExampleInfuraExtArgs() {
// "up":1234
// }
// }
infArgs := NewInfuraExtArgs(pol, func(ma datamodel.MapAssembler) {
customArgs := NewCustomExtArgs("key", pol, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet"))
qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "ur", qp.Int(1234))
@@ -41,14 +42,14 @@ func ExampleInfuraExtArgs() {
}))
})
err := infArgs.Verify()
err := customArgs.Verify()
fmt.Println(err)
// Output:
// <nil>
}
func TestInfura(t *testing.T) {
func TestCustom(t *testing.T) {
assembler := func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "ntwk", qp.String("eth-mainnet"))
qp.MapEntry(ma, "quota", qp.Map(6, func(ma datamodel.MapAssembler) {
@@ -74,24 +75,24 @@ func TestInfura(t *testing.T) {
{
name: "matching args",
pol: policy.MustConstruct(
policy.Equal(".inf.ntwk", literal.String("eth-mainnet")),
policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)),
policy.Equal(".key.ntwk", literal.String("eth-mainnet")),
policy.LessThanOrEqual(".key.quota.ur", literal.Int(1234)),
),
expected: true,
},
{
name: "wrong network",
pol: policy.MustConstruct(
policy.Equal(".inf.ntwk", literal.String("avalanche-fuji")),
policy.LessThanOrEqual(".inf.quota.ur", literal.Int(1234)),
policy.Equal(".key.ntwk", literal.String("avalanche-fuji")),
policy.LessThanOrEqual(".key.quota.ur", literal.Int(1234)),
),
expected: false,
},
{
name: "unrespected quota",
pol: policy.MustConstruct(
policy.Equal(".inf.ntwk", literal.String("eth-mainnet")),
policy.LessThanOrEqual(".inf.quota.ur", literal.Int(100)),
policy.Equal(".key.ntwk", literal.String("eth-mainnet")),
policy.LessThanOrEqual(".key.quota.ur", literal.Int(100)),
),
expected: false,
},
@@ -99,7 +100,7 @@ func TestInfura(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
extArgs := NewInfuraExtArgs(tc.pol, assembler)
extArgs := NewCustomExtArgs("key", tc.pol, assembler)
_, err := extArgs.Args()
require.NoError(t, err)

View File

@@ -13,6 +13,7 @@ import (
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/multiformats/go-multihash"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/token/invocation"
@@ -88,7 +89,7 @@ func (hea *HttpExtArgs) verifyHash() error {
mhBytes, err := n.AsBytes()
if err != nil {
return fmt.Errorf("http args hash should be a string")
return fmt.Errorf("http args hash should be bytes")
}
data, err := ipld.Encode(hea.argsIpld, dagcbor.Encode)
@@ -112,7 +113,7 @@ func (hea *HttpExtArgs) verifyHash() error {
// If that hash is inserted at the HttpArgsKey key in the invocation arguments,
// this increases the security as the UCAN token cannot be used with a different
// HTTP request.
// For convenience, the hash is returned as a read to use invocation argument.
// For convenience, the hash is returned as a ready-to-use invocation argument.
func MakeHttpHash(req *http.Request) (invocation.Option, error) {
// Note: the hash is computed on the full IPLD args, including HttpArgsKey
computedArgs, err := makeHttpArgs(req)

View File

@@ -5,9 +5,10 @@ import (
"net/http/httptest"
"testing"
"github.com/MetaMask/go-did-it/didtest"
"github.com/multiformats/go-multihash"
"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/pkg/policy"
@@ -200,3 +201,10 @@ func TestHttpHash(t *testing.T) {
})
}
}
func must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}

View File

@@ -1,171 +0,0 @@
package extargs
import (
"bytes"
"encoding/json"
"fmt"
"sync"
"github.com/INFURA/go-ethlibs/jsonrpc"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent/qp"
"github.com/ipld/go-ipld-prime/node/basicnode"
"github.com/multiformats/go-multihash"
"github.com/ucan-wg/go-ucan/pkg/args"
"github.com/ucan-wg/go-ucan/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/token/invocation"
)
// JsonRpcArgsKey is the key in the args, used for:
// - if it exists in the invocation, holds a hash of the args derived from the JsonRpc request
// - in the final args to be evaluated against the policies, holds the args derived from the JsonRpc request
const JsonRpcArgsKey = "jsonrpc"
type JsonRpcExtArgs struct {
pol policy.Policy
originalArgs args.ReadOnly
req *jsonrpc.Request
once sync.Once
args *args.Args
argsIpld ipld.Node
}
func NewJsonRpcExtArgs(pol policy.Policy, originalArgs args.ReadOnly, req *jsonrpc.Request) *JsonRpcExtArgs {
return &JsonRpcExtArgs{pol: pol, originalArgs: originalArgs, req: req}
}
func (jrea *JsonRpcExtArgs) Verify() error {
if err := jrea.makeArgs(); err != nil {
return err
}
if err := jrea.verifyHash(); err != nil {
return err
}
ok, leaf := jrea.pol.PartialMatch(jrea.argsIpld)
if !ok {
return fmt.Errorf("the following UCAN policy is not satisfied: %v", leaf.String())
}
return nil
}
func (jrea *JsonRpcExtArgs) Args() (*args.Args, error) {
if err := jrea.makeArgs(); err != nil {
return nil, err
}
return jrea.args, nil
}
func (jrea *JsonRpcExtArgs) makeArgs() error {
var outerErr error
jrea.once.Do(func() {
var err error
jrea.args, err = makeJsonRpcArgs(jrea.req)
if err != nil {
outerErr = err
return
}
jrea.argsIpld, err = jrea.args.ToIPLD()
if err != nil {
outerErr = err
return
}
})
return outerErr
}
func (jrea *JsonRpcExtArgs) verifyHash() error {
n, err := jrea.originalArgs.GetNode(JsonRpcArgsKey)
if err != nil {
// no hash found, nothing to verify
return nil
}
mhBytes, err := n.AsBytes()
if err != nil {
return fmt.Errorf("jsonrpc args hash should be a string")
}
data, err := ipld.Encode(jrea.argsIpld, dagcbor.Encode)
if err != nil {
return fmt.Errorf("can't encode derived args in dag-cbor: %w", err)
}
sum, err := multihash.Sum(data, multihash.SHA2_256, -1)
if err != nil {
return err
}
if !bytes.Equal(mhBytes, sum) {
return fmt.Errorf("derived args from jsonrpc request don't match the expected hash")
}
return nil
}
// MakeJsonRpcHash compute the hash of the derived arguments from the JsonRPC request.
// If that hash is inserted at the JsonRpcArgsKey key in the invocation arguments,
// this increases the security as the UCAN token cannot be used with a different
// JsonRPC request.
// For convenience, the hash is returned as a read to use invocation argument.
func MakeJsonRpcHash(req *jsonrpc.Request) (invocation.Option, error) {
// Note: the hash is computed on the full IPLD args, including JsonRpcArgsKey
computedArgs, err := makeJsonRpcArgs(req)
if err != nil {
return nil, err
}
n, err := computedArgs.ToIPLD()
if err != nil {
return nil, err
}
data, err := ipld.Encode(n, dagcbor.Encode)
if err != nil {
return nil, err
}
sum, err := multihash.Sum(data, multihash.SHA2_256, -1)
if err != nil {
return nil, err
}
return invocation.WithArgument(JsonRpcArgsKey, []byte(sum)), nil
}
func makeJsonRpcArgs(req *jsonrpc.Request) (*args.Args, error) {
deserialized := make([]any, len(req.Params))
for i, param := range req.Params {
err := json.Unmarshal(param, &deserialized[i])
if err != nil {
return nil, err
}
}
params, err := literal.List(deserialized)
if err != nil {
return nil, err
}
n, err := qp.BuildMap(basicnode.Prototype.Any, 3, func(ma datamodel.MapAssembler) {
qp.MapEntry(ma, "jsonrpc", qp.String(req.JSONRPC))
qp.MapEntry(ma, "method", qp.String(req.Method))
qp.MapEntry(ma, "params", qp.Node(params))
})
if err != nil {
return nil, err
}
res := args.New()
err = res.Add(JsonRpcArgsKey, n)
if err != nil {
return nil, err
}
return res, nil
}

View File

@@ -1,188 +0,0 @@
package extargs
import (
"testing"
"github.com/INFURA/go-ethlibs/jsonrpc"
"github.com/multiformats/go-multihash"
"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/pkg/policy"
"github.com/ucan-wg/go-ucan/pkg/policy/literal"
"github.com/ucan-wg/go-ucan/token/invocation"
)
func TestJsonRpc(t *testing.T) {
tests := []struct {
name string
req *jsonrpc.Request
pol policy.Policy
expected bool
}{
{
name: "or on method, not matching",
req: jsonrpc.MustRequest(1839673506133526, "eth_getBlockByNumber",
"0x599784", true,
),
pol: policy.MustConstruct(
policy.Or(
policy.Equal(".jsonrpc.method", literal.String("eth_getCode")),
policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")),
policy.Equal(".jsonrpc.method", literal.String("eth_call")),
policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")),
),
),
expected: false,
},
{
name: "or on method, matching",
req: jsonrpc.MustRequest(1839673506133526, "eth_call",
map[string]string{"to": "0xBADBADBADBADBADBADBADBADBADBADBADBADBAD1"},
),
pol: policy.MustConstruct(
policy.Or(
policy.Equal(".jsonrpc.method", literal.String("eth_getCode")),
policy.Equal(".jsonrpc.method", literal.String("eth_getBalance")),
policy.Equal(".jsonrpc.method", literal.String("eth_call")),
policy.Equal(".jsonrpc.method", literal.String("eth_blockNumber")),
),
),
expected: true,
},
{
name: "complex, optional parameter, matching",
req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
true, false, 1234, "callTracer",
),
pol: policy.MustConstruct(
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
policy.Or(
policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")),
policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")),
),
),
expected: true,
},
{
name: "complex, optional parameter, missing parameter",
req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
true, false, 1234,
),
pol: policy.MustConstruct(
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
policy.Or(
policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")),
policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")),
),
),
expected: true,
},
{
name: "complex, parameter not matching",
req: jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
true, false, 1234, "ho_no",
),
pol: policy.MustConstruct(
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
policy.Or(
policy.Equal(".jsonrpc.params[3]?", literal.String("callTracer")),
policy.Equal(".jsonrpc.params[3]?", literal.String("prestateTracer")),
),
),
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// we don't test the args hash here
emptyArgs := args.New().ReadOnly()
ctx := NewJsonRpcExtArgs(tc.pol, emptyArgs, tc.req)
_, err := ctx.Args()
require.NoError(t, err)
if tc.expected {
require.NoError(t, ctx.Verify())
} else {
require.Error(t, ctx.Verify())
}
})
}
}
func TestJsonRpcHash(t *testing.T) {
servicePersona := didtest.PersonaAlice
clientPersona := didtest.PersonaBob
req := jsonrpc.MustRequest(1839673506133526, "debug_traceCall",
true, false, 1234, "ho_no",
)
pol := policy.MustConstruct(
policy.Equal(".jsonrpc.method", literal.String("debug_traceCall")),
)
makeArg := func(data []byte, code uint64) invocation.Option {
mh, err := multihash.Sum(data, code, -1)
require.NoError(t, err)
return invocation.WithArgument(JsonRpcArgsKey, []byte(mh))
}
tests := []struct {
name string
argOptions []invocation.Option
expected bool
}{
{
name: "correct hash",
argOptions: []invocation.Option{must(MakeJsonRpcHash(req))},
expected: true,
},
{
name: "non-matching hash",
argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.SHA2_256)},
expected: false,
},
{
name: "wrong type of hash",
argOptions: []invocation.Option{makeArg([]byte{1, 2, 3, 4}, multihash.BLAKE3)},
expected: false,
},
{
name: "no hash",
argOptions: nil,
expected: true, // having a hash is not enforced
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
inv, err := invocation.New(
clientPersona.DID(),
command.MustParse("/foo"),
servicePersona.DID(),
nil,
tc.argOptions..., // inject hash argument, if any
)
require.NoError(t, err)
ctx := NewJsonRpcExtArgs(pol, inv.Arguments(), req)
if tc.expected {
require.NoError(t, ctx.Verify())
} else {
require.Error(t, ctx.Verify())
}
})
}
}
func must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}