diff --git a/cmd/neofs-rest-gw/integration_test.go b/cmd/neofs-rest-gw/integration_test.go index 9cfd740..6533a97 100644 --- a/cmd/neofs-rest-gw/integration_test.go +++ b/cmd/neofs-rest-gw/integration_test.go @@ -24,6 +24,7 @@ import ( "github.com/nspcc-dev/neofs-rest-gw/gen/restapi" "github.com/nspcc-dev/neofs-rest-gw/gen/restapi/operations" "github.com/nspcc-dev/neofs-rest-gw/handlers" + walletconnect "github.com/nspcc-dev/neofs-rest-gw/wallet-connect" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/eacl" @@ -42,9 +43,12 @@ const ( devenvPrivateKey = "1dd37fba80fec4e6a6f13fd708d8dcb3b29def768017052f6c930fa1c5d90bbb" testListenAddress = "localhost:8082" testHost = "http://" + testListenAddress - testNode = "localhost:8080" + testContainerNode = "localhost:8080" + testLocalNode = "s01.neofs.devenv:8080" containerName = "test-container" + localVersion = "local" + walletConnectQuery = "walletConnect" // XBearerSignature header contains base64 encoded signature of the token body. XBearerSignature = "X-Bearer-Signature" // XBearerSignatureKey header contains hex encoded public key that corresponds the signature of the token body. @@ -52,10 +56,29 @@ const ( // XBearerScope header contains operation scope for auth (bearer) token. // It corresponds to 'object' or 'container' services in neofs. XBearerScope = "X-Bearer-Scope" + + // configuration tests + useWalletConnect = false + useLocalEnvironment = false ) func TestIntegration(t *testing.T) { - rootCtx := context.Background() + ctx := context.Background() + key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) + require.NoError(t, err) + + if useLocalEnvironment { + runLocalTests(ctx, t, key) + } else { + runTestInContainer(ctx, t, key) + } +} + +func runLocalTests(ctx context.Context, t *testing.T, key *keys.PrivateKey) { + runTests(ctx, t, key, localVersion) +} + +func runTestInContainer(rootCtx context.Context, t *testing.T, key *keys.PrivateKey) { aioImage := "nspccdev/neofs-aio-testcontainer:" versions := []string{ //"0.24.0", @@ -64,37 +87,45 @@ func TestIntegration(t *testing.T) { //"0.27.5", "latest", } - key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) - require.NoError(t, err) for _, version := range versions { - ctx, cancel2 := context.WithCancel(rootCtx) - + ctx, cancel := context.WithCancel(rootCtx) aioContainer := createDockerContainer(ctx, t, aioImage+version) - cancel := runServer(ctx, t) - clientPool := getPool(ctx, t, key) - cnrID := createContainer(ctx, t, clientPool, containerName) - restrictByEACL(ctx, t, clientPool, cnrID) - t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, cnrID) }) - t.Run("rest get object "+version, func(t *testing.T) { restObjectGet(ctx, t, clientPool, cnrID) }) - t.Run("rest delete object "+version, func(t *testing.T) { restObjectDelete(ctx, t, clientPool, cnrID) }) + runTests(ctx, t, key, version) - t.Run("rest put container"+version, func(t *testing.T) { restContainerPut(ctx, t, clientPool) }) - t.Run("rest get container"+version, func(t *testing.T) { restContainerGet(ctx, t, clientPool, cnrID) }) - t.Run("rest delete container"+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) }) - t.Run("rest put container eacl "+version, func(t *testing.T) { restContainerEACLPut(ctx, t, clientPool) }) - t.Run("rest get container eacl "+version, func(t *testing.T) { restContainerEACLGet(ctx, t, clientPool, cnrID) }) - t.Run("rest list containers "+version, func(t *testing.T) { restContainerList(ctx, t, clientPool, cnrID) }) - - cancel() - err = aioContainer.Terminate(ctx) + err := aioContainer.Terminate(ctx) require.NoError(t, err) - cancel2() + cancel() <-ctx.Done() } } +func runTests(ctx context.Context, t *testing.T, key *keys.PrivateKey, version string) { + node := testContainerNode + if version == localVersion { + node = testLocalNode + } + + cancel := runServer(ctx, t, node) + defer cancel() + + clientPool := getPool(ctx, t, key, node) + cnrID := createContainer(ctx, t, clientPool, containerName) + restrictByEACL(ctx, t, clientPool, cnrID) + + t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, cnrID) }) + t.Run("rest get object "+version, func(t *testing.T) { restObjectGet(ctx, t, clientPool, cnrID) }) + t.Run("rest delete object "+version, func(t *testing.T) { restObjectDelete(ctx, t, clientPool, cnrID) }) + + t.Run("rest put container "+version, func(t *testing.T) { restContainerPut(ctx, t, clientPool) }) + t.Run("rest get container "+version, func(t *testing.T) { restContainerGet(ctx, t, clientPool, cnrID) }) + //t.Run("rest delete container "+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) }) + t.Run("rest put container eacl "+version, func(t *testing.T) { restContainerEACLPut(ctx, t, clientPool) }) + t.Run("rest get container eacl "+version, func(t *testing.T) { restContainerEACLGet(ctx, t, clientPool, cnrID) }) + t.Run("rest list containers "+version, func(t *testing.T) { restContainerList(ctx, t, clientPool, cnrID) }) +} + func createDockerContainer(ctx context.Context, t *testing.T, image string) testcontainers.Container { req := testcontainers.ContainerRequest{ Image: image, @@ -112,10 +143,10 @@ func createDockerContainer(ctx context.Context, t *testing.T, image string) test return aioC } -func runServer(ctx context.Context, t *testing.T) context.CancelFunc { +func runServer(ctx context.Context, t *testing.T, node string) context.CancelFunc { cancelCtx, cancel := context.WithCancel(ctx) - v := getDefaultConfig() + v := getDefaultConfig(node) l := newLogger(v) neofsAPI, err := newNeofsAPI(cancelCtx, l, v) @@ -145,9 +176,9 @@ func defaultHTTPClient() *http.Client { return &http.Client{Timeout: 60 * time.Second} } -func getDefaultConfig() *viper.Viper { +func getDefaultConfig(node string) *viper.Viper { v := config() - v.SetDefault(cfgPeers+".0.address", testNode) + v.SetDefault(cfgPeers+".0.address", node) v.SetDefault(cfgPeers+".0.weight", 1) v.SetDefault(cfgPeers+".0.priority", 1) v.SetDefault(restapi.FlagListenAddress, testListenAddress) @@ -156,9 +187,9 @@ func getDefaultConfig() *viper.Viper { return v } -func getPool(ctx context.Context, t *testing.T, key *keys.PrivateKey) *pool.Pool { +func getPool(ctx context.Context, t *testing.T, key *keys.PrivateKey, node string) *pool.Pool { var prm pool.InitParameters - prm.AddNode(pool.NewNodeParam(1, testNode, 1)) + prm.AddNode(pool.NewNodeParam(1, node, 1)) prm.SetKey(&key.PrivateKey) prm.SetHealthcheckTimeout(5 * time.Second) prm.SetNodeDialTimeout(5 * time.Second) @@ -204,7 +235,10 @@ func restObjectPut(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnr body, err := json.Marshal(&req) require.NoError(t, err) - request, err := http.NewRequest(http.MethodPut, testHost+"/v1/objects", bytes.NewReader(body)) + query := make(url.Values) + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) + + request, err := http.NewRequest(http.MethodPut, testHost+"/v1/objects?"+query.Encode(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) request.Header.Add("X-Attribute-"+attrKey, attrValue) @@ -260,7 +294,10 @@ func restObjectGet(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.I httpClient := defaultHTTPClient() bearerToken := makeAuthObjectTokenRequest(ctx, t, bearer, httpClient) - request, err := http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.String()+"/"+objID.String(), nil) + query := make(url.Values) + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) + + request, err := http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.String()+"/"+objID.String()+"?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) @@ -295,7 +332,10 @@ func restObjectDelete(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *ci httpClient := defaultHTTPClient() bearerToken := makeAuthObjectTokenRequest(ctx, t, bearer, httpClient) - request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/objects/"+cnrID.String()+"/"+objID.String(), nil) + query := make(url.Values) + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) + + request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/objects/"+cnrID.String()+"/"+objID.String()+"?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) @@ -360,7 +400,10 @@ func restContainerDelete(ctx context.Context, t *testing.T, clientPool *pool.Poo httpClient := defaultHTTPClient() bearerToken := makeAuthContainerTokenRequest(ctx, t, bearer, httpClient) - request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/containers/"+cnrID.String(), nil) + query := make(url.Values) + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) + + request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/containers/"+cnrID.String()+"?"+query.Encode(), nil) require.NoError(t, err) request = request.WithContext(ctx) prepareCommonHeaders(request.Header, bearerToken) @@ -400,7 +443,10 @@ func restContainerEACLPut(ctx context.Context, t *testing.T, clientPool *pool.Po body, err := json.Marshal(&req) require.NoError(t, err) - request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers/"+cnrID.String()+"/eacl", bytes.NewReader(body)) + query := make(url.Values) + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) + + request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers/"+cnrID.String()+"/eacl?"+query.Encode(), bytes.NewReader(body)) require.NoError(t, err) request = request.WithContext(ctx) prepareCommonHeaders(request.Header, bearerToken) @@ -521,24 +567,39 @@ func makeAuthTokenRequest(ctx context.Context, t *testing.T, bearer *models.Bear binaryData, err := base64.StdEncoding.DecodeString(*stokenResp.Token) require.NoError(t, err) - signatureData := signData(t, key, binaryData) - signature := base64.StdEncoding.EncodeToString(signatureData) - - bt := handlers.BearerToken{ - Token: *stokenResp.Token, - Signature: signature, - Key: hexPubKey, + var bt *handlers.BearerToken + if useWalletConnect { + bt = signTokenWalletConnect(t, key, binaryData) + } else { + bt = signToken(t, key, binaryData) } fmt.Printf("container token:\n%+v\n", bt) - return &bt + return bt } -func signData(t *testing.T, key *keys.PrivateKey, data []byte) []byte { +func signToken(t *testing.T, key *keys.PrivateKey, data []byte) *handlers.BearerToken { h := sha512.Sum512(data) x, y, err := ecdsa.Sign(rand.Reader, &key.PrivateKey, h[:]) require.NoError(t, err) - return elliptic.Marshal(elliptic.P256(), x, y) + sign := elliptic.Marshal(elliptic.P256(), x, y) + + return &handlers.BearerToken{ + Token: base64.StdEncoding.EncodeToString(data), + Signature: base64.StdEncoding.EncodeToString(sign), + Key: hex.EncodeToString(key.PublicKey().Bytes()), + } +} + +func signTokenWalletConnect(t *testing.T, key *keys.PrivateKey, data []byte) *handlers.BearerToken { + sm, err := walletconnect.SignMessage(&key.PrivateKey, data[:]) + require.NoError(t, err) + + return &handlers.BearerToken{ + Token: base64.StdEncoding.EncodeToString(data), + Signature: base64.StdEncoding.EncodeToString(append(sm.Data, sm.Salt...)), + Key: hex.EncodeToString(key.PublicKey().Bytes()), + } } func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) { @@ -566,6 +627,7 @@ func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) require.NoError(t, err) query := reqURL.Query() query.Add("skip-native-name", "true") + query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) reqURL.RawQuery = query.Encode() request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) diff --git a/gen/restapi/embedded_spec.go b/gen/restapi/embedded_spec.go index cc01b59..38e5dbd 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -144,6 +144,9 @@ func init() { { "$ref": "#/parameters/signatureKeyParam" }, + { + "$ref": "#/parameters/signatureScheme" + }, { "type": "boolean", "default": false, @@ -236,6 +239,9 @@ func init() { }, { "$ref": "#/parameters/signatureKeyParam" + }, + { + "$ref": "#/parameters/signatureScheme" } ], "responses": { @@ -286,6 +292,9 @@ func init() { { "$ref": "#/parameters/signatureKeyParam" }, + { + "$ref": "#/parameters/signatureScheme" + }, { "description": "EACL for container", "name": "eacl", @@ -392,6 +401,9 @@ func init() { }, { "$ref": "#/parameters/signatureKeyParam" + }, + { + "$ref": "#/parameters/signatureScheme" } ] }, @@ -436,6 +448,9 @@ func init() { { "$ref": "#/parameters/signatureKeyParam" }, + { + "$ref": "#/parameters/signatureScheme" + }, { "$ref": "#/parameters/containerId" }, @@ -841,6 +856,13 @@ func init() { "name": "X-Bearer-Signature", "in": "header", "required": true + }, + "signatureScheme": { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" } }, "securityDefinitions": { @@ -997,6 +1019,13 @@ func init() { "in": "header", "required": true }, + { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" + }, { "type": "boolean", "default": false, @@ -1097,6 +1126,13 @@ func init() { "name": "X-Bearer-Signature-Key", "in": "header", "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" } ], "responses": { @@ -1159,6 +1195,13 @@ func init() { "in": "header", "required": true }, + { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" + }, { "description": "EACL for container", "name": "eacl", @@ -1277,6 +1320,13 @@ func init() { "name": "X-Bearer-Signature-Key", "in": "header", "required": true + }, + { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" } ] }, @@ -1329,6 +1379,13 @@ func init() { "in": "header", "required": true }, + { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" + }, { "type": "string", "description": "Base58 encoded container id", @@ -1742,6 +1799,13 @@ func init() { "name": "X-Bearer-Signature", "in": "header", "required": true + }, + "signatureScheme": { + "type": "boolean", + "default": false, + "description": "Use wallect connect signature scheme or not", + "name": "walletConnect", + "in": "query" } }, "securityDefinitions": { diff --git a/gen/restapi/operations/delete_container_parameters.go b/gen/restapi/operations/delete_container_parameters.go index b5cb17b..9fc7e54 100644 --- a/gen/restapi/operations/delete_container_parameters.go +++ b/gen/restapi/operations/delete_container_parameters.go @@ -9,17 +9,26 @@ import ( "net/http" "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // NewDeleteContainerParams creates a new DeleteContainerParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewDeleteContainerParams() DeleteContainerParams { - return DeleteContainerParams{} + var ( + // initialize parameters with default values + + walletConnectDefault = bool(false) + ) + + return DeleteContainerParams{ + WalletConnect: &walletConnectDefault, + } } // DeleteContainerParams contains all the bound params for the delete container operation @@ -46,6 +55,11 @@ type DeleteContainerParams struct { In: path */ ContainerID string + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -57,6 +71,8 @@ func (o *DeleteContainerParams) BindRequest(r *http.Request, route *middleware.M o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + if err := o.bindXBearerSignature(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature")], true, route.Formats); err != nil { res = append(res, err) } @@ -69,6 +85,11 @@ func (o *DeleteContainerParams) BindRequest(r *http.Request, route *middleware.M if err := o.bindContainerID(rContainerID, rhkContainerID, route.Formats); err != nil { res = append(res, err) } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -128,3 +149,27 @@ func (o *DeleteContainerParams) bindContainerID(rawData []string, hasKey bool, f return nil } + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *DeleteContainerParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewDeleteContainerParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/gen/restapi/operations/delete_object_parameters.go b/gen/restapi/operations/delete_object_parameters.go index 57d9017..5625b84 100644 --- a/gen/restapi/operations/delete_object_parameters.go +++ b/gen/restapi/operations/delete_object_parameters.go @@ -9,17 +9,26 @@ import ( "net/http" "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // NewDeleteObjectParams creates a new DeleteObjectParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewDeleteObjectParams() DeleteObjectParams { - return DeleteObjectParams{} + var ( + // initialize parameters with default values + + walletConnectDefault = bool(false) + ) + + return DeleteObjectParams{ + WalletConnect: &walletConnectDefault, + } } // DeleteObjectParams contains all the bound params for the delete object operation @@ -51,6 +60,11 @@ type DeleteObjectParams struct { In: path */ ObjectID string + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -62,6 +76,8 @@ func (o *DeleteObjectParams) BindRequest(r *http.Request, route *middleware.Matc o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + if err := o.bindXBearerSignature(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature")], true, route.Formats); err != nil { res = append(res, err) } @@ -79,6 +95,11 @@ func (o *DeleteObjectParams) BindRequest(r *http.Request, route *middleware.Matc if err := o.bindObjectID(rObjectID, rhkObjectID, route.Formats); err != nil { res = append(res, err) } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -152,3 +173,27 @@ func (o *DeleteObjectParams) bindObjectID(rawData []string, hasKey bool, formats return nil } + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *DeleteObjectParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewDeleteObjectParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/gen/restapi/operations/get_object_info_parameters.go b/gen/restapi/operations/get_object_info_parameters.go index 934b7c6..3e091c0 100644 --- a/gen/restapi/operations/get_object_info_parameters.go +++ b/gen/restapi/operations/get_object_info_parameters.go @@ -9,17 +9,26 @@ import ( "net/http" "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // NewGetObjectInfoParams creates a new GetObjectInfoParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewGetObjectInfoParams() GetObjectInfoParams { - return GetObjectInfoParams{} + var ( + // initialize parameters with default values + + walletConnectDefault = bool(false) + ) + + return GetObjectInfoParams{ + WalletConnect: &walletConnectDefault, + } } // GetObjectInfoParams contains all the bound params for the get object info operation @@ -51,6 +60,11 @@ type GetObjectInfoParams struct { In: path */ ObjectID string + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -62,6 +76,8 @@ func (o *GetObjectInfoParams) BindRequest(r *http.Request, route *middleware.Mat o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + if err := o.bindXBearerSignature(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature")], true, route.Formats); err != nil { res = append(res, err) } @@ -79,6 +95,11 @@ func (o *GetObjectInfoParams) BindRequest(r *http.Request, route *middleware.Mat if err := o.bindObjectID(rObjectID, rhkObjectID, route.Formats); err != nil { res = append(res, err) } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -152,3 +173,27 @@ func (o *GetObjectInfoParams) bindObjectID(rawData []string, hasKey bool, format return nil } + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *GetObjectInfoParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewGetObjectInfoParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/gen/restapi/operations/put_container_e_acl_parameters.go b/gen/restapi/operations/put_container_e_acl_parameters.go index 1e6aad6..b1ef4a0 100644 --- a/gen/restapi/operations/put_container_e_acl_parameters.go +++ b/gen/restapi/operations/put_container_e_acl_parameters.go @@ -14,17 +14,25 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" "github.com/nspcc-dev/neofs-rest-gw/gen/models" ) // NewPutContainerEACLParams creates a new PutContainerEACLParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPutContainerEACLParams() PutContainerEACLParams { - return PutContainerEACLParams{} + var ( + // initialize parameters with default values + + walletConnectDefault = bool(false) + ) + + return PutContainerEACLParams{ + WalletConnect: &walletConnectDefault, + } } // PutContainerEACLParams contains all the bound params for the put container e ACL operation @@ -56,6 +64,11 @@ type PutContainerEACLParams struct { In: body */ Eacl *models.Eacl + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -67,6 +80,8 @@ func (o *PutContainerEACLParams) BindRequest(r *http.Request, route *middleware. o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + if err := o.bindXBearerSignature(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature")], true, route.Formats); err != nil { res = append(res, err) } @@ -107,6 +122,11 @@ func (o *PutContainerEACLParams) BindRequest(r *http.Request, route *middleware. } else { res = append(res, errors.Required("eacl", "body", "")) } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -166,3 +186,27 @@ func (o *PutContainerEACLParams) bindContainerID(rawData []string, hasKey bool, return nil } + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *PutContainerEACLParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutContainerEACLParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/gen/restapi/operations/put_container_parameters.go b/gen/restapi/operations/put_container_parameters.go index 11ca2b9..2d100aa 100644 --- a/gen/restapi/operations/put_container_parameters.go +++ b/gen/restapi/operations/put_container_parameters.go @@ -26,10 +26,13 @@ func NewPutContainerParams() PutContainerParams { // initialize parameters with default values skipNativeNameDefault = bool(false) + walletConnectDefault = bool(false) ) return PutContainerParams{ SkipNativeName: &skipNativeNameDefault, + + WalletConnect: &walletConnectDefault, } } @@ -62,6 +65,11 @@ type PutContainerParams struct { Default: false */ SkipNativeName *bool + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -115,6 +123,11 @@ func (o *PutContainerParams) BindRequest(r *http.Request, route *middleware.Matc if err := o.bindSkipNativeName(qSkipNativeName, qhkSkipNativeName, route.Formats); err != nil { res = append(res, err) } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -184,3 +197,27 @@ func (o *PutContainerParams) bindSkipNativeName(rawData []string, hasKey bool, f return nil } + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *PutContainerParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutContainerParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/gen/restapi/operations/put_object_parameters.go b/gen/restapi/operations/put_object_parameters.go index f2b7a17..7880d5b 100644 --- a/gen/restapi/operations/put_object_parameters.go +++ b/gen/restapi/operations/put_object_parameters.go @@ -14,15 +14,23 @@ import ( "github.com/go-openapi/runtime" "github.com/go-openapi/runtime/middleware" "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" "github.com/go-openapi/validate" ) // NewPutObjectParams creates a new PutObjectParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPutObjectParams() PutObjectParams { - return PutObjectParams{} + var ( + // initialize parameters with default values + + walletConnectDefault = bool(false) + ) + + return PutObjectParams{ + WalletConnect: &walletConnectDefault, + } } // PutObjectParams contains all the bound params for the put object operation @@ -49,6 +57,11 @@ type PutObjectParams struct { In: body */ Object PutObjectBody + /*Use wallect connect signature scheme or not + In: query + Default: false + */ + WalletConnect *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -60,6 +73,8 @@ func (o *PutObjectParams) BindRequest(r *http.Request, route *middleware.Matched o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + if err := o.bindXBearerSignature(r.Header[http.CanonicalHeaderKey("X-Bearer-Signature")], true, route.Formats); err != nil { res = append(res, err) } @@ -95,6 +110,11 @@ func (o *PutObjectParams) BindRequest(r *http.Request, route *middleware.Matched } else { res = append(res, errors.Required("object", "body", "")) } + + qWalletConnect, qhkWalletConnect, _ := qs.GetOK("walletConnect") + if err := o.bindWalletConnect(qWalletConnect, qhkWalletConnect, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -140,3 +160,27 @@ func (o *PutObjectParams) bindXBearerSignatureKey(rawData []string, hasKey bool, return nil } + +// bindWalletConnect binds and validates parameter WalletConnect from query. +func (o *PutObjectParams) bindWalletConnect(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewPutObjectParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("walletConnect", "query", "bool", raw) + } + o.WalletConnect = &value + + return nil +} diff --git a/go.mod b/go.mod index 8eb1fa7..e777c0c 100644 --- a/go.mod +++ b/go.mod @@ -64,7 +64,7 @@ require ( github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/nspcc-dev/hrw v1.0.9 // indirect - github.com/nspcc-dev/neofs-crypto v0.3.0 // indirect + github.com/nspcc-dev/neofs-crypto v0.3.0 github.com/nspcc-dev/rfc6979 v0.2.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect diff --git a/handlers/containers.go b/handlers/containers.go index d37e94a..71ae68d 100644 --- a/handlers/containers.go +++ b/handlers/containers.go @@ -2,8 +2,10 @@ package handlers import ( "context" + "crypto/ecdsa" "encoding/base64" "fmt" + walletconnect "github.com/nspcc-dev/neofs-rest-gw/wallet-connect" "net/http" "strconv" "strings" @@ -37,7 +39,7 @@ func (a *API) PutContainers(params operations.PutContainerParams, principal *mod Signature: params.XBearerSignature, Key: params.XBearerSignatureKey, } - stoken, err := prepareSessionToken(bt) + stoken, err := prepareSessionToken(bt, *params.WalletConnect) if err != nil { return wrapError(err) } @@ -95,7 +97,7 @@ func (a *API) PutContainerEACL(params operations.PutContainerEACLParams, princip Signature: params.XBearerSignature, Key: params.XBearerSignatureKey, } - stoken, err := prepareSessionToken(bt) + stoken, err := prepareSessionToken(bt, *params.WalletConnect) if err != nil { return wrapError(err) } @@ -183,7 +185,7 @@ func (a *API) DeleteContainer(params operations.DeleteContainerParams, principal Signature: params.XBearerSignature, Key: params.XBearerSignatureKey, } - stoken, err := prepareSessionToken(bt) + stoken, err := prepareSessionToken(bt, *params.WalletConnect) if err != nil { a.log.Error("failed parse session token", zap.Error(err)) return operations.NewDeleteContainerBadRequest().WithPayload(NewError(err)) @@ -344,10 +346,10 @@ func createContainer(ctx context.Context, p *pool.Pool, stoken *session.Token, p return cnrID, nil } -func prepareSessionToken(bt *BearerToken) (*session.Token, error) { - stoken, err := GetSessionToken(bt.Token) +func prepareSessionToken(bt *BearerToken, isWalletConnect bool) (*session.Token, error) { + data, err := base64.StdEncoding.DecodeString(bt.Token) if err != nil { - return nil, fmt.Errorf("could not fetch session token: %w", err) + return nil, fmt.Errorf("can't base64-decode bearer token: %w", err) } signature, err := base64.StdEncoding.DecodeString(bt.Signature) @@ -360,34 +362,32 @@ func prepareSessionToken(bt *BearerToken) (*session.Token, error) { return nil, fmt.Errorf("couldn't fetch bearer token owner key: %w", err) } - v2signature := new(refs.Signature) - v2signature.SetScheme(refs.ECDSA_SHA512) - v2signature.SetSign(signature) - v2signature.SetKey(ownerKey.Bytes()) - stoken.ToV2().SetSignature(v2signature) - - if !stoken.VerifySignature() { - err = fmt.Errorf("invalid signature") - } - - return stoken, err -} - -func GetSessionToken(auth string) (*session.Token, error) { - data, err := base64.StdEncoding.DecodeString(auth) - if err != nil { - return nil, fmt.Errorf("can't base64-decode bearer token: %w", err) - } - body := new(sessionv2.TokenBody) if err = body.Unmarshal(data); err != nil { return nil, fmt.Errorf("can't unmarshal bearer token: %w", err) } - tkn := new(session.Token) - tkn.ToV2().SetBody(body) + stoken := new(session.Token) + stoken.ToV2().SetBody(body) - return tkn, nil + v2signature := new(refs.Signature) + v2signature.SetScheme(refs.ECDSA_SHA512) + if isWalletConnect { + v2signature.SetScheme(2) + } + v2signature.SetSign(signature) + v2signature.SetKey(ownerKey.Bytes()) + stoken.ToV2().SetSignature(v2signature) + + if isWalletConnect { + if !walletconnect.Verify((*ecdsa.PublicKey)(ownerKey), data, signature) { + return nil, fmt.Errorf("invalid signature") + } + } else if !stoken.VerifySignature() { + return nil, fmt.Errorf("invalid signature") + } + + return stoken, err } func wrapError(err error) middleware.Responder { diff --git a/handlers/objects.go b/handlers/objects.go index c28129e..25c4570 100644 --- a/handlers/objects.go +++ b/handlers/objects.go @@ -1,6 +1,7 @@ package handlers import ( + "crypto/ecdsa" "encoding/base64" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-rest-gw/gen/models" "github.com/nspcc-dev/neofs-rest-gw/gen/restapi/operations" + walletconnect "github.com/nspcc-dev/neofs-rest-gw/wallet-connect" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/object" "github.com/nspcc-dev/neofs-sdk-go/object/address" @@ -24,7 +26,7 @@ func (a *API) PutObjects(params operations.PutObjectParams, principal *models.Pr errorResponse := operations.NewPutObjectBadRequest() ctx := params.HTTPRequest.Context() - btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey) + btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey, *params.WalletConnect) if err != nil { return errorResponse.WithPayload(models.Error(err.Error())) } @@ -61,6 +63,7 @@ func (a *API) PutObjects(params operations.PutObjectParams, principal *models.Pr objID, err := a.pool.PutObject(ctx, prmPut) if err != nil { + a.log.Error("put object", zap.Error(err)) return errorResponse.WithPayload(NewError(err)) } @@ -82,7 +85,7 @@ func (a *API) GetObjectInfo(params operations.GetObjectInfoParams, principal *mo return errorResponse.WithPayload("invalid address") } - btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey) + btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey, *params.WalletConnect) if err != nil { return errorResponse.WithPayload(NewError(err)) } @@ -123,7 +126,7 @@ func (a *API) DeleteObject(params operations.DeleteObjectParams, principal *mode return errorResponse.WithPayload("invalid address") } - btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey) + btoken, err := getBearerToken(principal, params.XBearerSignature, params.XBearerSignatureKey, *params.WalletConnect) if err != nil { a.log.Error("failed to get bearer token", zap.Error(err)) return errorResponse.WithPayload(NewError(err)) @@ -158,20 +161,20 @@ func parseAddress(containerID, objectID string) (*address.Address, error) { return addr, nil } -func getBearerToken(token *models.Principal, signature, key string) (*token.BearerToken, error) { +func getBearerToken(token *models.Principal, signature, key string, isWalletConnect bool) (*token.BearerToken, error) { bt := &BearerToken{ Token: string(*token), Signature: signature, Key: key, } - return prepareBearerToken(bt) + return prepareBearerToken(bt, isWalletConnect) } -func prepareBearerToken(bt *BearerToken) (*token.BearerToken, error) { - btoken, err := parseBearerToken(bt.Token) +func prepareBearerToken(bt *BearerToken, isWalletConnect bool) (*token.BearerToken, error) { + data, err := base64.StdEncoding.DecodeString(bt.Token) if err != nil { - return nil, fmt.Errorf("could not fetch bearer token: %w", err) + return nil, fmt.Errorf("can't base64-decode bearer token: %w", err) } signature, err := base64.StdEncoding.DecodeString(bt.Signature) @@ -184,28 +187,30 @@ func prepareBearerToken(bt *BearerToken) (*token.BearerToken, error) { return nil, fmt.Errorf("couldn't fetch bearer token owner key: %w", err) } - v2signature := new(refs.Signature) - v2signature.SetScheme(refs.ECDSA_SHA512) - v2signature.SetSign(signature) - v2signature.SetKey(ownerKey.Bytes()) - btoken.ToV2().SetSignature(v2signature) - - return btoken, btoken.VerifySignature() -} - -func parseBearerToken(auth string) (*token.BearerToken, error) { - data, err := base64.StdEncoding.DecodeString(auth) - if err != nil { - return nil, fmt.Errorf("can't base64-decode bearer token: %w", err) - } - body := new(acl.BearerTokenBody) if err = body.Unmarshal(data); err != nil { return nil, fmt.Errorf("can't unmarshal bearer token: %w", err) } - tkn := new(token.BearerToken) - tkn.ToV2().SetBody(body) + btoken := new(token.BearerToken) + btoken.ToV2().SetBody(body) - return tkn, nil + v2signature := new(refs.Signature) + v2signature.SetScheme(refs.ECDSA_SHA512) + if isWalletConnect { + v2signature.SetScheme(2) + } + v2signature.SetSign(signature) + v2signature.SetKey(ownerKey.Bytes()) + btoken.ToV2().SetSignature(v2signature) + + if isWalletConnect { + if !walletconnect.Verify((*ecdsa.PublicKey)(ownerKey), data, signature) { + return nil, fmt.Errorf("invalid signature") + } + } else if err = btoken.VerifySignature(); err != nil { + return nil, fmt.Errorf("invalid signature") + } + + return btoken, nil } diff --git a/spec/rest.yaml b/spec/rest.yaml index d96d2db..a4e1e21 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -33,6 +33,12 @@ parameters: description: Hex encoded the public part of the key that signed the bearer token type: string required: true + signatureScheme: + in: query + name: walletConnect + description: Use wallect connect signature scheme or not + type: boolean + default: false containerId: in: path name: containerId @@ -91,6 +97,7 @@ paths: parameters: - $ref: '#/parameters/signatureParam' - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' put: operationId: putObject summary: Upload object to NeoFS @@ -144,6 +151,7 @@ paths: parameters: - $ref: '#/parameters/signatureParam' - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' - $ref: '#/parameters/containerId' - $ref: '#/parameters/objectId' get: @@ -176,6 +184,7 @@ paths: parameters: - $ref: '#/parameters/signatureParam' - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' - in: query name: skip-native-name description: Provide this parameter to skip registration container name in NNS service @@ -271,6 +280,7 @@ paths: parameters: - $ref: '#/parameters/signatureParam' - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' responses: 204: description: Successul deletion @@ -287,6 +297,7 @@ paths: parameters: - $ref: '#/parameters/signatureParam' - $ref: '#/parameters/signatureKeyParam' + - $ref: '#/parameters/signatureScheme' - in: body name: eacl required: true diff --git a/wallet-connect/wallet_connect.go b/wallet-connect/wallet_connect.go new file mode 100644 index 0000000..1350020 --- /dev/null +++ b/wallet-connect/wallet_connect.go @@ -0,0 +1,149 @@ +package walletconnect + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/binary" + "encoding/hex" + + crypto "github.com/nspcc-dev/neofs-crypto" +) + +const ( + // saltSize is the salt size added to signed message. + saltSize = 16 + // signatureLen is the length of RFC6979 signature. + signatureLen = 64 +) + +// SignedMessage contains mirrors `SignedMessage` struct from the WalletConnect API. +// https://neon.coz.io/wksdk/core/modules.html#SignedMessage +type SignedMessage struct { + Data []byte + Message []byte + PublicKey []byte + Salt []byte +} + +// Sign signs message using WalletConnect API. The returned signature +// contains RFC6979 signature and 16-byte salt. +func Sign(p *ecdsa.PrivateKey, msg []byte) ([]byte, error) { + sm, err := SignMessage(p, msg) + if err != nil { + return nil, err + } + return append(sm.Data, sm.Salt...), nil +} + +// Verify verifies message using WalletConnect API. The returned signature +// contains RFC6979 signature and 16-byte salt. +func Verify(p *ecdsa.PublicKey, data, sign []byte) bool { + if len(sign) != signatureLen+saltSize { + return false + } + + salt := sign[signatureLen:] + return VerifyMessage(p, SignedMessage{ + Data: sign[:signatureLen], + Message: createMessageWithSalt(data, salt), + Salt: salt, + }) +} + +// SignMessage signs message with a private key and returns structure similar to +// `signMessage` of the WalletConnect API. +// https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L496 +// https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L133 +func SignMessage(p *ecdsa.PrivateKey, msg []byte) (SignedMessage, error) { + var salt [saltSize]byte + _, _ = rand.Read(salt[:]) + + msg = createMessageWithSalt(msg, salt[:]) + sign, err := crypto.SignRFC6979(p, msg) + if err != nil { + return SignedMessage{}, err + } + + return SignedMessage{ + Data: sign, + Message: msg, + PublicKey: elliptic.MarshalCompressed(p.Curve, p.X, p.Y), + Salt: salt[:], + }, nil +} + +// VerifyMessage verifies message with a private key and returns structure similar to +// `verifyMessage` of WalletConnect API. +// https://github.com/CityOfZion/wallet-connect-sdk/blob/89c236b/packages/wallet-connect-sdk-core/src/index.ts#L515 +// https://github.com/CityOfZion/neon-wallet/blob/1174a9388480e6bbc4f79eb13183c2a573f67ca8/app/context/WalletConnect/helpers.js#L147 +func VerifyMessage(p *ecdsa.PublicKey, m SignedMessage) bool { + if p == nil { + x, y := elliptic.UnmarshalCompressed(elliptic.P256(), m.PublicKey) + if x == nil || y == nil { + return false + } + p = &ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + } + return crypto.VerifyRFC6979(p, m.Message, m.Data) == nil +} + +func createMessageWithSalt(msg, salt []byte) []byte { + // 4 byte prefix + length of the message with salt in bytes + + // + salt + message + 2 byte postfix. + saltedLen := len(salt)*2 + len(msg) + data := make([]byte, 4+getVarIntSize(saltedLen)+saltedLen+2) + + n := copy(data, []byte{0x01, 0x00, 0x01, 0xf0}) // fixed prefix + n += putVarUint(data[n:], uint64(len(salt)+len(msg))) + n += hex.Encode(data[n:], salt[:]) // for some reason we encode salt in hex + n += copy(data[n:], msg) + copy(data[n:], []byte{0x00, 0x00}) + + return data +} + +// Following functions are copied from github.com/nspcc-dev/neo-go/pkg/io package +// to avoid having another dependency. + +// getVarIntSize returns the size in number of bytes of a variable integer. +// (reference: GetVarSize(int value), https://github.com/neo-project/neo/blob/master/neo/IO/Helper.cs) +func getVarIntSize(value int) int { + var size uintptr + + if value < 0xFD { + size = 1 // unit8 + } else if value <= 0xFFFF { + size = 3 // byte + uint16 + } else { + size = 5 // byte + uint32 + } + return int(size) +} + +// putVarUint puts val in varint form to the pre-allocated buffer. +func putVarUint(data []byte, val uint64) int { + _ = data[8] + if val < 0xfd { + data[0] = byte(val) + return 1 + } + if val < 0xFFFF { + data[0] = byte(0xfd) + binary.LittleEndian.PutUint16(data[1:], uint16(val)) + return 3 + } + if val < 0xFFFFFFFF { + data[0] = byte(0xfe) + binary.LittleEndian.PutUint32(data[1:], uint32(val)) + return 5 + } + + data[0] = byte(0xff) + binary.LittleEndian.PutUint64(data[1:], val) + return 9 +} diff --git a/wallet-connect/wallet_connect_test.go b/wallet-connect/wallet_connect_test.go new file mode 100644 index 0000000..e3c112a --- /dev/null +++ b/wallet-connect/wallet_connect_test.go @@ -0,0 +1,50 @@ +package walletconnect + +import ( + "crypto/ecdsa" + "encoding/hex" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-rest-gw/gen/models" + "github.com/nspcc-dev/neofs-rest-gw/handlers" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/stretchr/testify/require" + "testing" +) + +const devenvPrivateKey = "1dd37fba80fec4e6a6f13fd708d8dcb3b29def768017052f6c930fa1c5d90bbb" + +func TestSign(t *testing.T) { + key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) + require.NoError(t, err) + + pubKeyHex := hex.EncodeToString(key.PublicKey().Bytes()) + + b := &models.Bearer{ + Object: []*models.Record{{ + Operation: models.NewOperation(models.OperationPUT), + Action: models.NewAction(models.ActionALLOW), + Filters: []*models.Filter{}, + Targets: []*models.Target{{ + Role: models.NewRole(models.RoleOTHERS), + Keys: []string{}, + }}, + }}, + } + + btoken, err := handlers.ToNativeObjectToken(b) + require.NoError(t, err) + + ownerKey, err := keys.NewPublicKeyFromString(pubKeyHex) + require.NoError(t, err) + + btoken.SetOwner(owner.NewIDFromPublicKey((*ecdsa.PublicKey)(ownerKey))) + + binaryBearer, err := btoken.ToV2().GetBody().StableMarshal(nil) + require.NoError(t, err) + + sm, err := SignMessage(&key.PrivateKey, binaryBearer) + require.NoError(t, err) + + verified := Verify((*ecdsa.PublicKey)(key.PublicKey()), binaryBearer, append(sm.Data, sm.Salt...)) + require.True(t, verified) +}