Repository: vegaprotocol/go-wallet
Pull request #111 (2021-06-28 16:02:30)

Add verify endpoint
close #111 #110 


Patch:
diff --git a/crypto/hash.go b/crypto/hash.go
index f6e7e84..110f4a9 100644
--- a/crypto/hash.go
+++ b/crypto/hash.go
@@ -2,6 +2,7 @@ package crypto
 
 import (
 	"math/rand"
+	"time"
 
 	"golang.org/x/crypto/sha3"
 )
@@ -27,3 +28,7 @@ func RandomStr(length int) string {
 func RandomBytes(n int) []byte {
 	return []byte(RandomStr(n))
 }
+
+func init() {
+	rand.Seed(time.Now().UnixNano())
+}
diff --git a/service/mocks/wallet_handler_mock.go b/service/mocks/wallet_handler_mock.go
index 716ba98..cefe8e4 100644
--- a/service/mocks/wallet_handler_mock.go
+++ b/service/mocks/wallet_handler_mock.go
@@ -108,6 +108,18 @@ func (mr *MockWalletHandlerMockRecorder) LoginWallet(arg0, arg1 interface{}) *go
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginWallet", reflect.TypeOf((*MockWalletHandler)(nil).LoginWallet), arg0, arg1)
 }
 
+// LogoutWallet mocks base method
+func (m *MockWalletHandler) LogoutWallet(arg0 string) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "LogoutWallet", arg0)
+}
+
+// LogoutWallet indicates an expected call of LogoutWallet
+func (mr *MockWalletHandlerMockRecorder) LogoutWallet(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogoutWallet", reflect.TypeOf((*MockWalletHandler)(nil).LogoutWallet), arg0)
+}
+
 // SecureGenerateKeyPair mocks base method
 func (m *MockWalletHandler) SecureGenerateKeyPair(arg0, arg1 string) (string, error) {
 	m.ctrl.T.Helper()
@@ -195,3 +207,18 @@ func (mr *MockWalletHandlerMockRecorder) UpdateMeta(arg0, arg1, arg2, arg3 inter
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMeta", reflect.TypeOf((*MockWalletHandler)(nil).UpdateMeta), arg0, arg1, arg2, arg3)
 }
+
+// VerifyAny mocks base method
+func (m *MockWalletHandler) VerifyAny(arg0, arg1, arg2, arg3 string) (bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "VerifyAny", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// VerifyAny indicates an expected call of VerifyAny
+func (mr *MockWalletHandlerMockRecorder) VerifyAny(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyAny", reflect.TypeOf((*MockWalletHandler)(nil).VerifyAny), arg0, arg1, arg2, arg3)
+}
diff --git a/service/service.go b/service/service.go
index 12a8718..5409466 100644
--- a/service/service.go
+++ b/service/service.go
@@ -148,17 +148,13 @@ func ParseUpdateMetaRequest(r *http.Request, keyID string) (*UpdateMetaRequest,
 	return req, errs
 }
 
-// SignTxRequest describes the request for SignTx.
-type SignTxRequest struct {
-	Tx        string `json:"tx"`
-	PubKey    string `json:"pubKey"`
-	Propagate bool   `json:"propagate"`
-}
-
 // SignAnyRequest describes the request for SignAny.
 type SignAnyRequest struct {
+	// InputData is the payload to generate a signature from. I should be
+	// base 64 encoded.
 	InputData string `json:"inputData"`
-	PubKey    string `json:"pubKey"`
+	// PubKey is used to retrieve the private key to sign the InputDate.
+	PubKey string `json:"pubKey"`
 }
 
 func ParseSignAnyRequest(r *http.Request) (*SignAnyRequest, commands.Errors) {
@@ -184,6 +180,44 @@ func ParseSignAnyRequest(r *http.Request) (*SignAnyRequest, commands.Errors) {
 	return req, errs
 }
 
+// VerifyAnyRequest describes the request for VerifyAny.
+type VerifyAnyRequest struct {
+	// InputData is the payload to be verified. It should be base64 encoded.
+	InputData string `json:"inputData"`
+	// Signature is the signature to check against the InputData. It should be
+	// base64 encoded.
+	Signature string `json:"signature"`
+	// PubKey is the public key used along the signature to check the InputData.
+	PubKey string `json:"pubKey"`
+}
+
+func ParseVerifyAnyRequest(r *http.Request) (*VerifyAnyRequest, commands.Errors) {
+	errs := commands.NewErrors()
+
+	req := &VerifyAnyRequest{}
+	if err := unmarshalBody(r, &req); err != nil {
+		return nil, errs.FinalAdd(err)
+	}
+
+	if len(req.InputData) == 0 {
+		errs.AddForProperty("input_data", commands.ErrIsRequired)
+	}
+
+	if len(req.Signature) == 0 {
+		errs.AddForProperty("signature", commands.ErrIsRequired)
+	}
+
+	if len(req.PubKey) == 0 {
+		errs.AddForProperty("pub_key", commands.ErrIsRequired)
+	}
+
+	if !errs.Empty() {
+		return nil, errs
+	}
+
+	return req, errs
+}
+
 func ParseSubmitTransactionRequest(r *http.Request) (*walletpb.SubmitTransactionRequest, commands.Errors) {
 	errs := commands.NewErrors()
 
@@ -237,12 +271,14 @@ type TokenResponse struct {
 type WalletHandler interface {
 	CreateWallet(name, passphrase string) error
 	LoginWallet(name, passphrase string) error
+	LogoutWallet(name string)
 	SecureGenerateKeyPair(name, passphrase string) (string, error)
 	GetPublicKey(name, pubKey string) (*wallet.PublicKey, error)
 	ListPublicKeys(name string) ([]wallet.PublicKey, error)
 	SignTx(name, tx, pubKey string, height uint64) (wallet.SignedBundle, error)
 	SignTxV2(name string, req *walletpb.SubmitTransactionRequest, height uint64) (*commandspb.Transaction, error)
 	SignAny(name, inputData, pubKey string) ([]byte, error)
+	VerifyAny(name, inputData, sig, pubKey string) (bool, error)
 	TaintKey(name, pubKey, passphrase string) error
 	UpdateMeta(name, pubKey, passphrase string, meta []wallet.Meta) error
 	GetWalletPath(name string) (string, error)
@@ -295,16 +331,17 @@ func NewServiceWith(log *zap.Logger, cfg *config.Config, h WalletHandler, a Auth
 	}
 
 	s.POST("/api/v1/auth/token", s.Login)
+	s.DELETE("/api/v1/auth/token", ExtractToken(s.Revoke))
 	s.GET("/api/v1/status", s.health)
 	s.POST("/api/v1/wallets", s.CreateWallet)
 
-	s.DELETE("/api/v1/auth/token", ExtractToken(s.Revoke))
 	s.GET("/api/v1/keys", ExtractToken(s.ListPublicKeys))
 	s.POST("/api/v1/keys", ExtractToken(s.GenerateKeypair))
 	s.GET("/api/v1/keys/:keyid", ExtractToken(s.GetPublicKey))
 	s.PUT("/api/v1/keys/:keyid/taint", ExtractToken(s.TaintKey))
 	s.PUT("/api/v1/keys/:keyid/metadata", ExtractToken(s.UpdateMeta))
 	s.POST("/api/v1/sign", ExtractToken(s.SignAny))
+	s.POST("/api/v1/verify", ExtractToken(s.VerifyAny))
 	s.POST("/api/v1/command", ExtractToken(s.SignTxV2))
 	s.POST("/api/v1/command/sync", ExtractToken(s.SignTxSyncV2))
 	s.POST("/api/v1/command/commit", ExtractToken(s.SignTxCommitV2))
@@ -392,12 +429,20 @@ func (s *Service) DownloadWallet(token string, w http.ResponseWriter, r *http.Re
 }
 
 func (s *Service) Revoke(t string, w http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
-	err := s.auth.Revoke(t)
+	name, err := s.auth.VerifyToken(t)
+	if err != nil {
+		writeForbiddenError(w, err)
+		return
+	}
+
+	err = s.auth.Revoke(t)
 	if err != nil {
 		writeForbiddenError(w, err)
 		return
 	}
 
+	s.handler.LogoutWallet(name)
+
 	writeSuccess(w, SuccessResponse{Success: true}, http.StatusOK)
 }
 
@@ -549,6 +594,28 @@ func (s *Service) SignAny(t string, w http.ResponseWriter, r *http.Request, _ ht
 	writeSuccess(w, res, http.StatusOK)
 }
 
+func (s *Service) VerifyAny(t string, w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
+	req, errs := ParseVerifyAnyRequest(r)
+	if !errs.Empty() {
+		s.writeBadRequest(w, errs)
+		return
+	}
+
+	name, err := s.auth.VerifyToken(t)
+	if err != nil {
+		writeForbiddenError(w, err)
+		return
+	}
+
+	verified, err := s.handler.VerifyAny(name, req.InputData, req.Signature, req.PubKey)
+	if err != nil {
+		writeForbiddenError(w, err)
+		return
+	}
+
+	writeSuccess(w, SuccessResponse{Success: verified}, http.StatusOK)
+}
+
 func (s *Service) SignTxSyncV2(token string, w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 	s.signTxV2(token, w, r, p, api.SubmitTransactionV2Request_TYPE_SYNC)
 }
diff --git a/service/service_deprecated.go b/service/service_deprecated.go
index 029874f..bba8a0f 100644
--- a/service/service_deprecated.go
+++ b/service/service_deprecated.go
@@ -13,6 +13,13 @@ import (
 	"google.golang.org/grpc/status"
 )
 
+// SignTxRequest describes the request for SignTx.
+type SignTxRequest struct {
+	Tx        string `json:"tx"`
+	PubKey    string `json:"pubKey"`
+	Propagate bool   `json:"propagate"`
+}
+
 func (s *Service) SignTxAsync(t string, w http.ResponseWriter, r *http.Request, p httprouter.Params) {
 	s.signTx(t, w, r, p, api.SubmitTransactionRequest_TYPE_ASYNC)
 }
diff --git a/wallet/handler.go b/wallet/handler.go
index 7b72e84..00baf03 100644
--- a/wallet/handler.go
+++ b/wallet/handler.go
@@ -78,6 +78,10 @@ func (h *Handler) LoginWallet(name, passphrase string) error {
 	return nil
 }
 
+func (h *Handler) LogoutWallet(name string) {
+	h.loggedWallets.Remove(name)
+}
+
 func (h *Handler) GenerateKeyPair(name, passphrase string) (KeyPair, error) {
 	h.mu.Lock()
 	defer h.mu.Unlock()
@@ -340,10 +344,14 @@ func (w wallets) Add(wallet Wallet) {
 	w[wallet.Owner] = wallet
 }
 
-func (w wallets) Get(owner string) (Wallet, error) {
-	wallet, ok := w[owner]
+func (w wallets) Get(name string) (Wallet, error) {
+	wallet, ok := w[name]
 	if !ok {
 		return Wallet{}, ErrWalletDoesNotExists
 	}
 	return wallet, nil
 }
+
+func (w wallets) Remove(name string) {
+	delete(w, name)
+}


Test patch:
diff --git a/service/service_test.go b/service/service_test.go
index 3d936fe..b9dcdd1 100644
--- a/service/service_test.go
+++ b/service/service_test.go
@@ -79,6 +79,8 @@ func TestService(t *testing.T) {
 	t.Run("Signing transaction without pub-key fails", testSigningTransactionWithoutPubKeyFails)
 	t.Run("Signing transaction without command fails", testSigningTransactionWithoutCommandFails)
 	t.Run("Signing anything succeeds", testSigningAnythingSucceeds)
+	t.Run("Verifying anything succeeds", testVerifyingAnythingSucceeds)
+	t.Run("Failed verification fails", testVerifyingAnythingFails)
 }
 
 func testServiceCreateWalletOK(t *testing.T) {
@@ -213,7 +215,9 @@ func testServiceRevokeTokenOK(t *testing.T) {
 	s := getTestService(t)
 	defer s.ctrl.Finish()
 
+	s.auth.EXPECT().VerifyToken("eyXXzA").Times(1).Return("jeremy", nil)
 	s.auth.EXPECT().Revoke(gomock.Any()).Times(1).Return(nil)
+	s.handler.EXPECT().LogoutWallet("jeremy").Times(1)
 
 	r := httptest.NewRequest("POST", "scheme://host/path", nil)
 	r.Header.Add("Authorization", "Bearer eyXXzA")
@@ -503,7 +507,6 @@ func testServiceTaintFailInvalidRequest(t *testing.T) {
 
 	resp = w.Result()
 	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
-
 }
 
 func testServiceUpdateMetaOK(t *testing.T) {
@@ -569,7 +572,6 @@ func testServiceUpdateMetaFailInvalidRequest(t *testing.T) {
 
 	resp = w.Result()
 	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
-
 }
 
 func testSigningTransactionSucceeds(t *testing.T) {
@@ -766,7 +768,7 @@ func testSigningAnythingSucceeds(t *testing.T) {
 	defer s.ctrl.Finish()
 
 	s.auth.EXPECT().VerifyToken("eyXXzA").Times(1).Return("jeremy", nil)
-	s.handler.EXPECT().SignAny(gomock.Any(), gomock.Any(), gomock.Any()).
+	s.handler.EXPECT().SignAny("jeremy", "some data", "asdasasdasd").
 		Times(1).Return([]byte("some sig"), nil)
 	payload := `{"inputData": "some data", "pubKey": "asdasasdasd"}`
 	r := httptest.NewRequest("POST", "scheme://host/path", bytes.NewBufferString(payload))
@@ -780,8 +782,64 @@ func testSigningAnythingSucceeds(t *testing.T) {
 	assert.Equal(t, http.StatusOK, resp.StatusCode)
 }
 
+func testVerifyingAnythingSucceeds(t *testing.T) {
+	s := getTestService(t)
+	defer s.ctrl.Finish()
+
+	s.auth.EXPECT().VerifyToken("eyXXzA").Times(1).Return("jeremy", nil)
+	s.handler.EXPECT().VerifyAny("jeremy", "some data", "some sig", "asdasasdasd").
+		Times(1).Return(true, nil)
+	payload := `{"inputData": "some data", "pubKey": "asdasasdasd", "signature": "some sig"}`
+	r := httptest.NewRequest("POST", "scheme://host/path", bytes.NewBufferString(payload))
+	r.Header.Set("Authorization", "Bearer eyXXzA")
+
+	w := httptest.NewRecorder()
+
+	service.ExtractToken(s.VerifyAny)(w, r, nil)
+
+	httpResponse := w.Result()
+	resp := service.SuccessResponse{}
+	assert.Equal(t, http.StatusOK, httpResponse.StatusCode)
+	unmarshalResponse(httpResponse, &resp)
+	assert.True(t, resp.Success)
+}
+
+func testVerifyingAnythingFails(t *testing.T) {
+	s := getTestService(t)
+	defer s.ctrl.Finish()
+
+	s.auth.EXPECT().VerifyToken("eyXXzA").Times(1).Return("jeremy", nil)
+	s.handler.EXPECT().VerifyAny("jeremy", "some data", "some sig", "asdasasdasd").
+		Times(1).Return(false, nil)
+	payload := `{"inputData": "some data", "pubKey": "asdasasdasd", "signature": "some sig"}`
+	r := httptest.NewRequest("POST", "scheme://host/path", bytes.NewBufferString(payload))
+	r.Header.Set("Authorization", "Bearer eyXXzA")
+
+	w := httptest.NewRecorder()
+
+	service.ExtractToken(s.VerifyAny)(w, r, nil)
+
+	httpResponse := w.Result()
+	resp := service.SuccessResponse{}
+	assert.Equal(t, http.StatusOK, httpResponse.StatusCode)
+	unmarshalResponse(httpResponse, &resp)
+	assert.False(t, resp.Success)
+}
+
 func newAuthenticatedRequest(payload string) *http.Request {
 	r := httptest.NewRequest("POST", "scheme://host/path", bytes.NewBufferString(payload))
 	r.Header.Set("Authorization", "Bearer eyXXzA")
 	return r
 }
+
+func unmarshalResponse(r *http.Response, into interface{}) {
+	defer r.Body.Close()
+	body, err := ioutil.ReadAll(r.Body)
+	if err != nil {
+		panic(err)
+	}
+	err = json.Unmarshal(body, into)
+	if err != nil {
+		panic(err)
+	}
+}