adjust the toolkit to the new location
This commit is contained in:
committed by
Michael Muré
parent
06f478b9c3
commit
0647e4ff8a
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user