From bc625f0ebef070dbc6a7087f86a64aeb663eb553 Mon Sep 17 00:00:00 2001 From: Artem Barger Date: Sun, 7 Mar 2021 17:30:46 +0200 Subject: [PATCH] Add servers' signature verification. Resolves #23 by adding method to verify signatures comming with servers' responses. Signed-off-by: Artem Barger Signed-off-by: Yoav Tock --- examples/cars/commands/init.go | 4 +- examples/cars/commands/mint.go | 9 +- examples/cars/commands/transafer.go | 13 +- examples/cars/commands/utils.go | 12 +- go.sum | 9 +- pkg/bcdb/config_tx_context_test.go | 36 ++-- pkg/bcdb/db.go | 312 +++------------------------ pkg/bcdb/dbs_tx_context_test.go | 10 +- pkg/bcdb/mocks/rest_client.go | 17 +- pkg/bcdb/mocks/signature_verifier.go | 24 +++ pkg/bcdb/mocks/signer.go | 2 +- pkg/bcdb/session.go | 272 +++++++++++++++++++++++ pkg/bcdb/tx_context.go | 37 ++-- pkg/bcdb/tx_context_test.go | 128 ++++++++++- pkg/bcdb/user_tx_context_test.go | 16 +- pkg/bcdb/verifier.go | 49 +++++ pkg/bcdb/verifier_test.go | 123 +++++++++++ 17 files changed, 712 insertions(+), 361 deletions(-) create mode 100644 pkg/bcdb/mocks/signature_verifier.go create mode 100644 pkg/bcdb/session.go create mode 100644 pkg/bcdb/verifier.go create mode 100644 pkg/bcdb/verifier_test.go diff --git a/examples/cars/commands/init.go b/examples/cars/commands/init.go index 1cb543c3..84ee6a6e 100644 --- a/examples/cars/commands/init.go +++ b/examples/cars/commands/init.go @@ -155,8 +155,8 @@ func initUsers(demoDir string, session bcdb.DBSession, logger *logger.SugarLogge DBPermission: map[string]types.Privilege_Access{CarDBName: 1}, }, }, &types.AccessControl{ - ReadWriteUsers: bcdb.UsersMap("admin"), - ReadUsers: bcdb.UsersMap("admin"), + ReadWriteUsers: usersMap("admin"), + ReadUsers: usersMap("admin"), }) if err != nil { usersTx.Abort() diff --git a/examples/cars/commands/mint.go b/examples/cars/commands/mint.go index 05ee490c..a50d48b2 100644 --- a/examples/cars/commands/mint.go +++ b/examples/cars/commands/mint.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" - "github.com/IBM-Blockchain/bcdb-sdk/pkg/bcdb" "github.com/IBM-Blockchain/bcdb-server/pkg/logger" "github.com/IBM-Blockchain/bcdb-server/pkg/types" "github.com/pkg/errors" @@ -54,8 +53,8 @@ func MintRequest(demoDir, dealerID, carRegistration string, lg *logger.SugarLogg err = dataTx.Put(CarDBName, key, recordBytes, &types.AccessControl{ - ReadUsers: bcdb.UsersMap("dmv"), - ReadWriteUsers: bcdb.UsersMap(dealerID), + ReadUsers: usersMap("dmv"), + ReadWriteUsers: usersMap(dealerID), }, ) if err != nil { @@ -146,8 +145,8 @@ func MintApprove(demoDir, dmvID, mintReqRecordKey string, lg *logger.SugarLogger err = dataTx.Put(CarDBName, carKey, carRecordBytes, &types.AccessControl{ - ReadUsers: bcdb.UsersMap(mintReqRec.Dealer), - ReadWriteUsers: bcdb.UsersMap(dmvID), + ReadUsers: usersMap(mintReqRec.Dealer), + ReadWriteUsers: usersMap(dmvID), }, ) if err != nil { diff --git a/examples/cars/commands/transafer.go b/examples/cars/commands/transafer.go index a8a6559f..24a37e94 100644 --- a/examples/cars/commands/transafer.go +++ b/examples/cars/commands/transafer.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" - "github.com/IBM-Blockchain/bcdb-sdk/pkg/bcdb" "github.com/IBM-Blockchain/bcdb-server/pkg/logger" "github.com/IBM-Blockchain/bcdb-server/pkg/types" "github.com/pkg/errors" @@ -62,8 +61,8 @@ func TransferTo(demoDir, ownerID, buyerID, carRegistration string, lg *logger.Su ttRecKey := ttRecord.Key() err = dataTx.Put(CarDBName, ttRecKey, ttRecBytes, &types.AccessControl{ - ReadUsers: bcdb.UsersMap("dmv", buyerID), - ReadWriteUsers: bcdb.UsersMap(ownerID), + ReadUsers: usersMap("dmv", buyerID), + ReadWriteUsers: usersMap(ownerID), }, ) if err != nil { @@ -152,8 +151,8 @@ func TransferReceive(demoDir, buyerID, carRegistration, transferToRecordKey stri trRecKey := trRec.Key() err = dataTx.Put(CarDBName, trRecKey, trRecBytes, &types.AccessControl{ - ReadUsers: bcdb.UsersMap("dmv", ttRec.Owner), - ReadWriteUsers: bcdb.UsersMap(buyerID), + ReadUsers: usersMap("dmv", ttRec.Owner), + ReadWriteUsers: usersMap(buyerID), }) if err != nil { return "", errors.Wrap(err, "error during data transaction") @@ -247,8 +246,8 @@ func Transfer(demoDir, dmvID, transferToRecordKey, transferRcvRecordKey string, err = dataTx.Put(CarDBName, carKey, recordBytes, &types.AccessControl{ - ReadUsers: bcdb.UsersMap(ttRec.Buyer), - ReadWriteUsers: bcdb.UsersMap(dmvID), + ReadUsers: usersMap(ttRec.Buyer), + ReadWriteUsers: usersMap(dmvID), }, ) if err != nil { diff --git a/examples/cars/commands/utils.go b/examples/cars/commands/utils.go index 31c9aaea..7ea2a846 100644 --- a/examples/cars/commands/utils.go +++ b/examples/cars/commands/utils.go @@ -6,10 +6,10 @@ import ( "io/ioutil" "path" - "github.com/golang/protobuf/jsonpb" - "github.com/golang/protobuf/proto" "github.com/IBM-Blockchain/bcdb-server/pkg/logger" "github.com/IBM-Blockchain/bcdb-server/pkg/types" + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" ) func marshalOrPanic(msg proto.Message) []byte { @@ -81,3 +81,11 @@ func loadTxEvidence(demoDir, txID string, lg *logger.SugarLogger) (*types.DataTx return env, rct, nil } + +func usersMap(users ...string) map[string]bool { + m := make(map[string]bool) + for _, u := range users { + m[u] = true + } + return m +} diff --git a/go.sum b/go.sum index 885f288a..996fdfe1 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOv github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/IBM-Blockchain/bcdb-server v0.0.0-20210609180532-d6c2c4edaed9 h1:pXBFUqoQndojNnTFd1ZgVD6amzmsYHxZogA0xuNLYu0= -github.com/IBM-Blockchain/bcdb-server v0.0.0-20210609180532-d6c2c4edaed9/go.mod h1:3/eL2aR2AxiitHtzet++d1b1kgtmfgHdoezwYXZivwc= -github.com/IBM-Blockchain/bcdb-server v0.0.0-20210617023424-769c72bf3ee7 h1:sxICIV8raTrRCXFNJ3xBQ6SJ/cv6g4TUo4PPveHxbLo= -github.com/IBM-Blockchain/bcdb-server v0.0.0-20210617023424-769c72bf3ee7/go.mod h1:mtuB0GJek4elh+cUs7DGtdsUrzcjX4JJznIeRjcDJko= -github.com/IBM-Blockchain/bcdb-server v0.0.0-20210620090414-7807cc5304c5 h1:F+hv8hDlAvyl0L5vDgtHwp4wxqGUScsBbNGi7VnD/fk= -github.com/IBM-Blockchain/bcdb-server v0.0.0-20210620090414-7807cc5304c5/go.mod h1:mtuB0GJek4elh+cUs7DGtdsUrzcjX4JJznIeRjcDJko= github.com/IBM-Blockchain/bcdb-server v0.1.0 h1:jp/x3m+l7HroeoO0t4yNKVZaTeAmQOAL3KOWFGmQKGE= github.com/IBM-Blockchain/bcdb-server v0.1.0/go.mod h1:mtuB0GJek4elh+cUs7DGtdsUrzcjX4JJznIeRjcDJko= github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= @@ -157,6 +151,7 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/jsbuiltin v0.0.0-20180426082241-50091555e127/go.mod h1:7X1acUyFRf+oVFTU6SWw9mnb57Vxn+Nbh8iPbKg95hs= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -220,6 +215,7 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -383,6 +379,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/pkg/bcdb/config_tx_context_test.go b/pkg/bcdb/config_tx_context_test.go index bb5d5f94..9cdc10f4 100644 --- a/pkg/bcdb/config_tx_context_test.go +++ b/pkg/bcdb/config_tx_context_test.go @@ -183,10 +183,7 @@ func TestConfigTxContext_DeleteAdmin(t *testing.T) { require.NoError(t, err) adminCert, _ := testutils.LoadTestClientCrypto(t, clientCryptoDir, "admin") - admin := &types.Admin{ - ID: "admin", - Certificate: adminCert.Raw, - } + admin := &types.Admin{ID: "admin", Certificate: adminCert.Raw} admin2Cert, _ := testutils.LoadTestClientCrypto(t, clientCryptoDir, "admin2") admin3Cert, _ := testutils.LoadTestClientCrypto(t, clientCryptoDir, "admin3") @@ -251,7 +248,7 @@ func TestConfigTxContext_DeleteAdmin(t *testing.T) { // session1 by removed admin cannot execute additional transactions tx4, err := session1.ConfigTx() - require.EqualError(t, err, "failed to obtain server's certificate") + require.EqualError(t, err, "error handling request, server returned: status: 401 Unauthorized, message: signature verification failed") require.Nil(t, tx4) } @@ -317,7 +314,7 @@ func TestConfigTxContext_UpdateAdmin(t *testing.T) { // session1 by updated admin cannot execute additional transactions, need to recreate session tx3, err := session1.ConfigTx() - require.EqualError(t, err, "failed to obtain server's certificate") + require.EqualError(t, err, "error handling request, server returned: status: 401 Unauthorized, message: signature verification failed") require.Nil(t, tx3) // need to recreate session with new credentials @@ -413,13 +410,14 @@ func TestConfigTxContext_DeleteClusterNode(t *testing.T) { config, err := tx1.GetClusterConfig() require.NoError(t, err) - id1 := config.Nodes[0].ID + node1 := config.Nodes[0] node2 := &types.NodeConfig{ ID: "testNode2", Address: config.Nodes[0].Address, Port: config.Nodes[0].Port + 1, Certificate: config.Nodes[0].Certificate, } + peer1 := config.ConsensusConfig.Members[0] peer2 := &types.PeerConfig{ NodeId: "testNode2", RaftId: config.ConsensusConfig.Members[0].RaftId + 1, @@ -437,7 +435,19 @@ func TestConfigTxContext_DeleteClusterNode(t *testing.T) { tx2, err := session1.ConfigTx() require.NoError(t, err) - err = tx2.DeleteClusterNode(id1) + + clusterConfig, err := tx2.GetClusterConfig() + require.NoError(t, err) + require.NotNil(t, clusterConfig) + require.Len(t, clusterConfig.Nodes, 2) + found, index := NodeExists("testNode2", clusterConfig.Nodes) + require.True(t, found) + require.Equal(t, clusterConfig.Nodes[index].Port, node2.Port) + found, index = PeerExists("testNode2", clusterConfig.ConsensusConfig.Members) + require.True(t, found) + require.Equal(t, clusterConfig.ConsensusConfig.Members[index].PeerPort, peer2.PeerPort) + + err = tx2.DeleteClusterNode(node2.ID) require.NoError(t, err) txID, receipt, err = tx2.Commit(true) @@ -449,17 +459,17 @@ func TestConfigTxContext_DeleteClusterNode(t *testing.T) { // verify tx was successfully committed. "Get" works once per Tx. tx3, err := session1.ConfigTx() require.NoError(t, err) - clusterConfig, err := tx3.GetClusterConfig() + clusterConfig, err = tx3.GetClusterConfig() require.NoError(t, err) require.NotNil(t, clusterConfig) require.Len(t, clusterConfig.Nodes, 1) - found, index := NodeExists("testNode2", clusterConfig.Nodes) + found, index = NodeExists("testNode1", clusterConfig.Nodes) require.True(t, found) - require.Equal(t, clusterConfig.Nodes[index].Port, node2.Port) - found, index = PeerExists("testNode2", clusterConfig.ConsensusConfig.Members) + require.Equal(t, clusterConfig.Nodes[index].Port, node1.Port) + found, index = PeerExists("testNode1", clusterConfig.ConsensusConfig.Members) require.True(t, found) - require.Equal(t, clusterConfig.ConsensusConfig.Members[index].PeerPort, peer2.PeerPort) + require.Equal(t, clusterConfig.ConsensusConfig.Members[index].PeerPort, peer1.PeerPort) } //TODO this test will stop working once we implement quorum rules diff --git a/pkg/bcdb/db.go b/pkg/bcdb/db.go index 8cd15354..c09030f0 100644 --- a/pkg/bcdb/db.go +++ b/pkg/bcdb/db.go @@ -3,27 +3,16 @@ package bcdb import ( - "context" - "crypto/rand" - "crypto/x509" - "encoding/base64" - "encoding/json" "encoding/pem" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "time" - "github.com/IBM-Blockchain/bcdb-sdk/pkg/config" - "github.com/IBM-Blockchain/bcdb-server/pkg/constants" + "github.com/IBM-Blockchain/bcdb-server/pkg/certificateauthority" "github.com/IBM-Blockchain/bcdb-server/pkg/crypto" - "github.com/IBM-Blockchain/bcdb-server/pkg/cryptoservice" "github.com/IBM-Blockchain/bcdb-server/pkg/logger" "github.com/IBM-Blockchain/bcdb-server/pkg/types" "github.com/golang/protobuf/proto" "github.com/pkg/errors" + "io/ioutil" + "net/url" ) // BCDB Blockchain Database interface, defines set of APIs @@ -119,28 +108,26 @@ func Create(config *config.ConnectionConfig) (BCDB, error) { } } - // Load root CA certificates - certsPool := x509.NewCertPool() + var rootCAs [][]byte for _, rootCAPath := range config.RootCAs { rootCABytes, err := ioutil.ReadFile(rootCAPath) if err != nil { dbLogger.Errorf("failed to read root CA certificate, due to %s", err) return nil, errors.Wrap(err, "failed to read root CA certificate") } - // TODO there are might be multiple PEM encoded blocks need to make - // sure we read correct one - pemBlock, _ := pem.Decode(rootCABytes) - if pemBlock == nil { - dbLogger.Error("failed decoding root CA certificate") - return nil, errors.New("failed decoding root CA certificate") - } - rootCACert, err := x509.ParseCertificate(pemBlock.Bytes) - if err != nil { - dbLogger.Errorf("failed to parse X509 root CA certificate, due to %s", err) - return nil, errors.Wrap(err, "failed to parse X509 root CA certificate") - } - certsPool.AddCert(rootCACert) + asn1Data, _ := pem.Decode(rootCABytes) + rootCAs = append(rootCAs, asn1Data.Bytes) } + rootCACerts, err := certificateauthority.NewCACertCollection(rootCAs, nil) + if err != nil { + dbLogger.Errorf("failed to create CACertCollection, due to %s", err) + return nil, err + } + if err = rootCACerts.VerifyCollection(); err != nil { + dbLogger.Errorf("verification of CA certs collection is failed, due to %s", err) + return nil, err + } + // Validate replica set URIs urls := map[string]*url.URL{} for _, uri := range config.ReplicaSet { @@ -154,14 +141,14 @@ func Create(config *config.ConnectionConfig) (BCDB, error) { return &bDB{ replicaSet: urls, - rootCAs: certsPool, + rootCAs: rootCACerts, logger: dbLogger, }, nil } type bDB struct { replicaSet map[string]*url.URL - rootCAs *x509.CertPool + rootCAs *certificateauthority.CACertCollection logger *logger.SugarLogger } @@ -184,7 +171,7 @@ func (b *bDB) Session(cfg *config.SessionConfig) (DBSession, error) { return nil, errors.Wrap(err, "cannot read user's certificate with user's private key") } - return &dbSession{ + session := &dbSession{ userID: cfg.UserConfig.UserID, signer: signer, userCert: certBytes, @@ -193,264 +180,13 @@ func (b *bDB) Session(cfg *config.SessionConfig) (DBSession, error) { txTimeout: cfg.TxTimeout, queryTimeout: cfg.QueryTimeout, logger: b.logger, - }, nil -} - -type dbSession struct { - userID string - signer Signer - userCert []byte - replicaSet map[string]*url.URL - rootCAs *x509.CertPool - txTimeout time.Duration - queryTimeout time.Duration - logger *logger.SugarLogger -} - -func (d *dbSession) getNodesCerts(replica *url.URL, httpClient *http.Client) (map[string]*x509.Certificate, error) { - nodesCerts := map[string]*x509.Certificate{} - getConfig := &url.URL{ - Path: constants.URLForGetConfig(), - } - configREST := replica.ResolveReference(getConfig) - ctx := context.TODO() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, configREST.String(), nil) - if err != nil { - return nil, err - } - - signature, err := cryptoservice.SignQuery(d.signer, &types.GetConfigQuery{ - UserID: d.userID, - }) - if err != nil { - d.logger.Errorf("failed signed transaction, %s", err) - return nil, err - } - - req.Header.Set("Accept", "application/json") - req.Header.Set(constants.UserHeader, d.userID) - req.Header.Set(constants.SignatureHeader, base64.StdEncoding.EncodeToString(signature)) - response, err := httpClient.Do(req) - if err != nil { - d.logger.Errorf("failed to send transaction to server %s, due to %s", getConfig.String(), err) - return nil, err - } - - if response.StatusCode != http.StatusOK { - d.logger.Errorf("error response from the server, %s", response.Status) - return nil, errors.New(fmt.Sprintf("error response from the server, %s", response.Status)) - } - - resEnv := &types.ResponseEnvelope{} - err = json.NewDecoder(response.Body).Decode(resEnv) - if err != nil { - return nil, err - } - - payload := &types.Payload{} - err = json.Unmarshal(resEnv.GetPayload(), payload) - if err != nil { - d.logger.Errorf("failed to unmarshal response payload, due to %s", err) - return nil, err - } - - // TODO need to validate payload's signature - // resEnv.Signature - the signature over payload - // payload.GetHeader().NodeID - the id of the node signed response - - configResponse := &types.GetConfigResponse{} - err = json.Unmarshal(payload.GetResponse(), configResponse) - if err != nil { - d.logger.Errorf("failed to unmarshal config response, due to %s", err) - return nil, err - } - - for _, node := range configResponse.GetConfig().GetNodes() { - cert, err := x509.ParseCertificate(node.Certificate) - if err != nil { - return nil, err - } - - _, err = cert.Verify(x509.VerifyOptions{ - Roots: d.rootCAs, - }) - if err != nil { - return nil, err - } - - nodesCerts[node.ID] = cert - } - - return nodesCerts, nil -} - -// UsersTx returns user's transaction context -func (d *dbSession) UsersTx() (UsersTxContext, error) { - commonCtx, err := d.newCommonTxContext() - if err != nil { - return nil, err - } - userTx := &userTxContext{ - commonTxContext: commonCtx, } - return userTx, nil -} - -// DBsTx returns database management transaction context -func (d *dbSession) DBsTx() (DBsTxContext, error) { - commonCtx, err := d.newCommonTxContext() - if err != nil { - return nil, err - } - dbsTx := &dbsTxContext{ - commonTxContext: commonCtx, - createdDBs: map[string]bool{}, - deletedDBs: map[string]bool{}, - } - return dbsTx, nil -} - -// DataTx returns data's transaction context -func (d *dbSession) DataTx() (DataTxContext, error) { - commonCtx, err := d.newCommonTxContext() - if err != nil { - return nil, err - } - dataTx := &dataTxContext{ - commonTxContext: commonCtx, - operations: make(map[string]*dbOperations), - } - return dataTx, nil -} - -// ConfigTx returns config transaction context -func (d *dbSession) ConfigTx() (ConfigTxContext, error) { - commonCtx, err := d.newCommonTxContext() - if err != nil { - return nil, err - } - configTx := &configTxContext{ - commonTxContext: commonCtx, - oldConfig: nil, - readOldConfigVersion: nil, - newConfig: nil, - } - - if err = configTx.queryClusterConfig(); err != nil { - return nil, err - } - - return configTx, nil -} - -// Provenance returns handler to access provenance -func (d *dbSession) Provenance() (Provenance, error) { - commonCtx, err := d.newCommonTxContext() + httpClient := newHTTPClient() + session.verifier, err = session.sigVerifier(httpClient) if err != nil { - return nil, err - } - return &provenance{ - commonCtx, - }, nil -} - -// Ledger returns handler to access bcdb ledger data -func (d *dbSession) Ledger() (Ledger, error) { - commonCtx, err := d.newCommonTxContext() - if err != nil { - return nil, err + b.logger.Errorf("cannot create a signature verifier, error: %s", err) + return nil, errors.Wrap(err, "cannot create a signature verifier") } - return &ledger{ - commonCtx, - }, nil -} - -func (d *dbSession) newCommonTxContext() (*commonTxContext, error) { - httpClient := d.newHTTPClient() - - nodesCerts, err := d.getServerCertificates(httpClient) - if err != nil { - return nil, err - } - commonTxContext := &commonTxContext{ - userID: d.userID, - signer: d.signer, - userCert: d.userCert, - replicaSet: d.replicaSet, - nodesCerts: nodesCerts, - restClient: NewRestClient(d.userID, httpClient, d.signer), - commitTimeout: d.txTimeout, - queryTimeout: d.queryTimeout, - logger: d.logger, - } - return commonTxContext, nil -} - -func (d *dbSession) getServerCertificates(httpClient *http.Client) (map[string]*x509.Certificate, error) { - var nodesCerts map[string]*x509.Certificate - var err error - for _, replica := range d.replicaSet { - nodesCerts, err = d.getNodesCerts(replica, httpClient) - if err != nil { - d.logger.Errorf("failed to obtain server's certificate, replica: %s", replica) - continue - } - } - - if len(nodesCerts) == 0 { - d.logger.Errorf("failed to obtain server's certificate, replicaSet: %s", d.replicaSet) - return nil, errors.New("failed to obtain server's certificate") - } - return nodesCerts, nil -} - -func (d *dbSession) newHTTPClient() *http.Client { - httpClient := &http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - } - return httpClient -} - -func ComputeTxID(userCert []byte) (string, error) { - nonce := make([]byte, 24) - _, err := rand.Read(nonce) - if err != nil { - return "", err - } - - b := append(nonce, userCert...) - - sha256Hash, err := crypto.ComputeSHA256Hash(b) - if err != nil { - return "", err - } - return base64.URLEncoding.EncodeToString(sha256Hash), err -} - -func UsersMap(users ...string) map[string]bool { - m := make(map[string]bool) - for _, u := range users { - m[u] = true - } - return m -} - -type ServerTimeout struct { - TxID string -} -func (e *ServerTimeout) Error() string { - return "timeout occurred on server side while submitting transaction, converted to asynchronous completion" + return session, nil } diff --git a/pkg/bcdb/dbs_tx_context_test.go b/pkg/bcdb/dbs_tx_context_test.go index 6ab26c8d..7137a93e 100644 --- a/pkg/bcdb/dbs_tx_context_test.go +++ b/pkg/bcdb/dbs_tx_context_test.go @@ -5,6 +5,7 @@ package bcdb import ( "errors" "fmt" + "github.com/IBM-Blockchain/bcdb-server/pkg/types" "net/http" "net/url" "path" @@ -55,6 +56,8 @@ func TestDBsContext_CreateDBAndCheckStatus(t *testing.T) { require.NoError(t, err) require.True(t, len(txId) > 0) require.NotNil(t, receipt) + require.True(t, len(receipt.GetHeader().GetValidationInfo())>0) + require.True(t, receipt.GetHeader().GetValidationInfo()[receipt.GetTxIndex()].Flag == types.Flag_VALID) // Check database status, whenever created or not tx, err = session.DBsTx() @@ -162,18 +165,15 @@ func TestDBsContext_MalformedRequest(t *testing.T) { require.NoError(t, err) // New session with admin user context - session, err := bcdb.Session(&sdkConfig.SessionConfig{ + _, err = bcdb.Session(&sdkConfig.SessionConfig{ UserConfig: &sdkConfig.UserConfig{ UserID: "adminX", CertPath: path.Join(clientCertTemDir, "admin.pem"), PrivateKeyPath: path.Join(clientCertTemDir, "admin.key"), }, }) - require.NoError(t, err) - - _, err = session.DBsTx() require.Error(t, err) - require.Contains(t, err.Error(), "failed to obtain server's certificate") + require.EqualError(t, err, "cannot create a signature verifier: failed to obtain the servers' certificates") } func TestDBsContext_ExistsFailureScenarios(t *testing.T) { diff --git a/pkg/bcdb/mocks/rest_client.go b/pkg/bcdb/mocks/rest_client.go index 82d6a010..faf179cd 100644 --- a/pkg/bcdb/mocks/rest_client.go +++ b/pkg/bcdb/mocks/rest_client.go @@ -1,12 +1,17 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery v2.5.1. DO NOT EDIT. package mocks -import context "context" -import http "net/http" -import mock "github.com/stretchr/testify/mock" -import protoiface "google.golang.org/protobuf/runtime/protoiface" -import time "time" +import ( + context "context" + http "net/http" + + mock "github.com/stretchr/testify/mock" + + protoiface "google.golang.org/protobuf/runtime/protoiface" + + time "time" +) // RestClient is an autogenerated mock type for the RestClient type type RestClient struct { diff --git a/pkg/bcdb/mocks/signature_verifier.go b/pkg/bcdb/mocks/signature_verifier.go new file mode 100644 index 00000000..4913363d --- /dev/null +++ b/pkg/bcdb/mocks/signature_verifier.go @@ -0,0 +1,24 @@ +// Code generated by mockery v2.5.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// SignatureVerifier is an autogenerated mock type for the SignatureVerifier type +type SignatureVerifier struct { + mock.Mock +} + +// Verify provides a mock function with given fields: entityID, payload, signature +func (_m *SignatureVerifier) Verify(entityID string, payload []byte, signature []byte) error { + ret := _m.Called(entityID, payload, signature) + + var r0 error + if rf, ok := ret.Get(0).(func(string, []byte, []byte) error); ok { + r0 = rf(entityID, payload, signature) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/bcdb/mocks/signer.go b/pkg/bcdb/mocks/signer.go index a606000a..88f8bb77 100644 --- a/pkg/bcdb/mocks/signer.go +++ b/pkg/bcdb/mocks/signer.go @@ -1,4 +1,4 @@ -// Code generated by mockery v1.0.0. DO NOT EDIT. +// Code generated by mockery v2.5.1. DO NOT EDIT. package mocks diff --git a/pkg/bcdb/session.go b/pkg/bcdb/session.go new file mode 100644 index 00000000..5752c7a8 --- /dev/null +++ b/pkg/bcdb/session.go @@ -0,0 +1,272 @@ +package bcdb + +import ( + "context" + "crypto/rand" + "crypto/x509" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/IBM-Blockchain/bcdb-server/pkg/certificateauthority" + "github.com/IBM-Blockchain/bcdb-server/pkg/constants" + "github.com/IBM-Blockchain/bcdb-server/pkg/crypto" + "github.com/IBM-Blockchain/bcdb-server/pkg/cryptoservice" + "github.com/IBM-Blockchain/bcdb-server/pkg/logger" + "github.com/IBM-Blockchain/bcdb-server/pkg/types" + "github.com/pkg/errors" +) + +//TODO refresh replicaSet and signature verifier when cluster config changes. +type dbSession struct { + userID string + signer Signer + verifier SignatureVerifier + userCert []byte + replicaSet map[string]*url.URL + rootCAs *certificateauthority.CACertCollection + txTimeout time.Duration + queryTimeout time.Duration + logger *logger.SugarLogger +} + +// UsersTx returns user's transaction context +func (d *dbSession) UsersTx() (UsersTxContext, error) { + commonCtx, err := d.newCommonTxContext() + if err != nil { + return nil, err + } + userTx := &userTxContext{ + commonTxContext: commonCtx, + } + return userTx, nil +} + +// DBsTx returns database management transaction context +func (d *dbSession) DBsTx() (DBsTxContext, error) { + commonCtx, err := d.newCommonTxContext() + if err != nil { + return nil, err + } + dbsTx := &dbsTxContext{ + commonTxContext: commonCtx, + createdDBs: map[string]bool{}, + deletedDBs: map[string]bool{}, + } + return dbsTx, nil +} + +// DataTx returns data's transaction context +func (d *dbSession) DataTx() (DataTxContext, error) { + commonCtx, err := d.newCommonTxContext() + if err != nil { + return nil, err + } + dataTx := &dataTxContext{ + commonTxContext: commonCtx, + operations: make(map[string]*dbOperations), + } + return dataTx, nil +} + +// ConfigTx returns config transaction context +func (d *dbSession) ConfigTx() (ConfigTxContext, error) { + commonCtx, err := d.newCommonTxContext() + if err != nil { + return nil, err + } + configTx := &configTxContext{ + commonTxContext: commonCtx, + oldConfig: nil, + readOldConfigVersion: nil, + newConfig: nil, + } + + if err = configTx.queryClusterConfig(); err != nil { + return nil, err + } + + return configTx, nil +} + +// Provenance returns handler to access provenance +func (d *dbSession) Provenance() (Provenance, error) { + commonCtx, err := d.newCommonTxContext() + if err != nil { + return nil, err + } + return &provenance{ + commonCtx, + }, nil +} + +// Ledger returns handler to access bcdb ledger data +func (d *dbSession) Ledger() (Ledger, error) { + commonCtx, err := d.newCommonTxContext() + if err != nil { + return nil, err + } + return &ledger{ + commonCtx, + }, nil +} + +func (d *dbSession) newCommonTxContext() (*commonTxContext, error) { + httpClient := newHTTPClient() + + commonTxContext := &commonTxContext{ + userID: d.userID, + signer: d.signer, + userCert: d.userCert, + replicaSet: d.replicaSet, + verifier: d.verifier, + restClient: NewRestClient(d.userID, httpClient, d.signer), + commitTimeout: d.txTimeout, + queryTimeout: d.queryTimeout, + logger: d.logger, + } + return commonTxContext, nil +} + +func (d *dbSession) sigVerifier(httpClient *http.Client) (SignatureVerifier, error) { + var verifier SignatureVerifier + var err error + for _, replica := range d.replicaSet { + //TODO choose the cert-set from the best replica - the one with the highest config version. See: + // https://github.com/IBM-Blockchain/bcdb-sdk/issues/27 + verifier, err = d.getNodesCerts(replica, httpClient) + if err == nil { + break + } + d.logger.Errorf("failed to obtain the servers' certificates from replica: %s, error: %s", replica, err) + } + + if verifier == nil { + d.logger.Errorf("failed to obtain the servers' certificates, replicaSet: %s", d.replicaSet) + return nil, errors.New("failed to obtain the servers' certificates") + } + return verifier, nil +} + +func (d *dbSession) getNodesCerts(replica *url.URL, httpClient *http.Client) (SignatureVerifier, error) { + nodesCerts := map[string]*x509.Certificate{} + getConfig := &url.URL{ + Path: constants.URLForGetConfig(), + } + configREST := replica.ResolveReference(getConfig) + ctx := context.TODO() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, configREST.String(), nil) + if err != nil { + return nil, err + } + + signature, err := cryptoservice.SignQuery(d.signer, &types.GetConfigQuery{ + UserID: d.userID, + }) + if err != nil { + d.logger.Errorf("failed signed transaction, %s", err) + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set(constants.UserHeader, d.userID) + req.Header.Set(constants.SignatureHeader, base64.StdEncoding.EncodeToString(signature)) + response, err := httpClient.Do(req) + if err != nil { + d.logger.Errorf("failed to send transaction to server %s, due to %s", getConfig.String(), err) + return nil, err + } + + if response.StatusCode != http.StatusOK { + d.logger.Errorf("error response from the server, %s", response.Status) + return nil, errors.New(fmt.Sprintf("error response from the server, %s", response.Status)) + } + + resEnv := &types.ResponseEnvelope{} + err = json.NewDecoder(response.Body).Decode(resEnv) + if err != nil { + return nil, err + } + + payload := &types.Payload{} + err = json.Unmarshal(resEnv.GetPayload(), payload) + if err != nil { + d.logger.Errorf("failed to unmarshal response payload, due to %s", err) + return nil, err + } + + configResponse := &types.GetConfigResponse{} + err = json.Unmarshal(payload.GetResponse(), configResponse) + if err != nil { + d.logger.Errorf("failed to unmarshal config response, due to %s", err) + return nil, err + } + + for _, node := range configResponse.GetConfig().GetNodes() { + err := d.rootCAs.VerifyLeafCert(node.Certificate) + if err != nil { + return nil, err + } + cert, err := x509.ParseCertificate(node.Certificate) + if err != nil { + return nil, err + } + nodesCerts[node.ID] = cert + } + + verifier, err := NewVerifier(nodesCerts, d.logger) + if err != nil { + return nil, err + } + + if err = verifier.Verify( + payload.GetHeader().GetNodeID(), + resEnv.GetPayload(), + resEnv.GetSignature()); err != nil { + d.logger.Errorf("failed to verify configuration response, error = %s", err) + return nil, errors.Errorf("failed to verify configuration response, error = %s", err) + } + + return verifier, err +} + +//TODO expose HTTP parameters, make client configurable, with good defaults. See: +// https://github.com/IBM-Blockchain/bcdb-sdk/issues/28 +func newHTTPClient() *http.Client { + httpClient := &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } + return httpClient +} + +func computeTxID(userCert []byte) (string, error) { + nonce := make([]byte, 24) + _, err := rand.Read(nonce) + if err != nil { + return "", err + } + + b := append(nonce, userCert...) + + sha256Hash, err := crypto.ComputeSHA256Hash(b) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(sha256Hash), err +} diff --git a/pkg/bcdb/tx_context.go b/pkg/bcdb/tx_context.go index 9c9daa66..8331dad9 100644 --- a/pkg/bcdb/tx_context.go +++ b/pkg/bcdb/tx_context.go @@ -4,16 +4,15 @@ package bcdb import ( "context" - "crypto/x509" "encoding/json" "net/http" "net/url" "time" - "github.com/golang/protobuf/proto" - "github.com/pkg/errors" "github.com/IBM-Blockchain/bcdb-server/pkg/logger" "github.com/IBM-Blockchain/bcdb-server/pkg/types" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" ) const ( @@ -25,7 +24,7 @@ type commonTxContext struct { signer Signer userCert []byte replicaSet map[string]*url.URL - nodesCerts map[string]*x509.Certificate + verifier SignatureVerifier restClient RestClient txEnvelope proto.Message commitTimeout time.Duration @@ -48,7 +47,7 @@ func (t *commonTxContext) commit(tx txContext, postEndpoint string, sync bool) ( replica := t.selectReplica() postEndpointResolved := replica.ResolveReference(&url.URL{Path: postEndpoint}) - txID, err := ComputeTxID(t.userCert) + txID, err := computeTxID(t.userCert) if err != nil { return "", nil, err } @@ -108,6 +107,13 @@ func (t *commonTxContext) commit(tx txContext, postEndpoint string, sync bool) ( return txID, nil, err } + nodeID := payload.GetHeader().GetNodeID() + err = t.verifier.Verify(nodeID, txResponseEnvelope.GetPayload(), txResponseEnvelope.GetSignature()) + if err != nil { + t.logger.Errorf("signature verification failed nodeID %s, due to %s", nodeID, err) + return "", nil, errors.Errorf("signature verification failed nodeID %s, due to %s", nodeID, err) + } + txResponse := &types.TxResponse{} err = json.Unmarshal(payload.GetResponse(), txResponse) if err != nil { @@ -115,10 +121,6 @@ func (t *commonTxContext) commit(tx txContext, postEndpoint string, sync bool) ( return txID, nil, err } - // TODO need to validate payload's signature - // r.Signature - the signature over payload - // payload.GetHeader().NodeID - the id of the node signed response - t.txSpent = true tx.cleanCtx() return txID, txResponse.GetReceipt(), nil @@ -187,9 +189,12 @@ func (t *commonTxContext) handleRequest(rawurl string, query, res proto.Message) return err } - // TODO need to validate payload's signature - // r.Signature - the signature over payload - // payload.GetHeader().NodeID - the id of the node signed response + nodeID := payload.GetHeader().GetNodeID() + err = t.verifier.Verify(nodeID, r.GetPayload(), r.GetSignature()) + if err != nil { + t.logger.Errorf("signature verification failed nodeID %s, due to %s", nodeID, err) + return errors.Errorf("signature verification failed nodeID %s, due to %s", nodeID, err) + } err = json.Unmarshal(payload.GetResponse(), res) if err != nil { @@ -208,3 +213,11 @@ func (t *commonTxContext) TxEnvelope() (proto.Message, error) { } var ErrTxNotFinalized = errors.New("can't access tx envelope, transaction not finalized") + +type ServerTimeout struct { + TxID string +} + +func (e *ServerTimeout) Error() string { + return "timeout occurred on server side while submitting transaction, converted to asynchronous completion, TxID: " + e.TxID +} diff --git a/pkg/bcdb/tx_context_test.go b/pkg/bcdb/tx_context_test.go index 56e6e0a4..9aaf4f9f 100644 --- a/pkg/bcdb/tx_context_test.go +++ b/pkg/bcdb/tx_context_test.go @@ -24,8 +24,13 @@ func TestTxCommit(t *testing.T) { emptySigner := &mocks.Signer{} emptySigner.On("Sign", mock.Anything).Return([]byte{1}, nil) - logger := createTestLogger(t) + verifier := &mocks.SignatureVerifier{} + verifier.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(nil) + + verifierFails := &mocks.SignatureVerifier{} + verifierFails.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("bad-mock-signature")) + logger := createTestLogger(t) tests := []struct { name string txCtx TxContext @@ -45,6 +50,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: asyncSubmit, resp: okResponseAsync(), @@ -66,6 +72,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: asyncSubmit, resp: serverBadRequestResponse(), @@ -88,6 +95,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: okResponse(), @@ -111,6 +119,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: serverTimeoutResponse(), @@ -121,7 +130,7 @@ func TestTxCommit(t *testing.T) { }, syncCommit: true, wantErr: true, - errMsg: "timeout occurred on server side while submitting transaction, converted to asynchronous completion", + errMsg: "timeout occurred on server side while submitting transaction, converted to asynchronous completion, TxID:", }, { name: "dataTx error submit", @@ -135,6 +144,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: submitErr, resp: nil, @@ -145,6 +155,29 @@ func TestTxCommit(t *testing.T) { wantErr: true, errMsg: "submit error", }, + { + name: "dataTx sig verifier fails", + txCtx: &dataTxContext{ + commonTxContext: &commonTxContext{ + userID: "testUser", + signer: emptySigner, + userCert: []byte{1, 2, 3}, + replicaSet: map[string]*url.URL{ + "node1": { + Path: "http://localhost:8888", + }, + }, + verifier: verifierFails, + restClient: NewRestClient("testUser", &mockHttpClient{ + process: asyncSubmit, + resp: okResponseAsync(), + }, emptySigner), + logger: logger, + }, + }, + wantErr: true, + errMsg: "signature verification failed nodeID node1, due to bad-mock-signature", + }, { name: "configTx correct async", txCtx: &configTxContext{ @@ -157,6 +190,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: asyncSubmit, resp: okResponseAsync(), @@ -179,6 +213,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: okResponse(), @@ -203,6 +238,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: serverTimeoutResponse(), @@ -216,6 +252,30 @@ func TestTxCommit(t *testing.T) { wantErr: true, errMsg: "timeout occurred on server side while submitting transaction, converted to asynchronous completion", }, + { + name: "configTx sig verifier failed", + txCtx: &configTxContext{ + commonTxContext: &commonTxContext{ + userID: "testUser", + signer: emptySigner, + userCert: []byte{1, 2, 3}, + replicaSet: map[string]*url.URL{ + "node1": { + Path: "http://localhost:8888", + }, + }, + verifier: verifierFails, + restClient: NewRestClient("testUser", &mockHttpClient{ + process: asyncSubmit, + resp: okResponseAsync(), + }, emptySigner), + logger: logger, + }, + oldConfig: &types.ClusterConfig{}, + }, + wantErr: true, + errMsg: "signature verification failed nodeID node1, due to bad-mock-signature", + }, { name: "userTx correct async", txCtx: &userTxContext{ @@ -228,6 +288,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: asyncSubmit, resp: okResponseAsync(), @@ -249,6 +310,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: okResponse(), @@ -272,6 +334,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: serverTimeoutResponse(), @@ -284,6 +347,29 @@ func TestTxCommit(t *testing.T) { wantErr: true, errMsg: "timeout occurred on server side while submitting transaction, converted to asynchronous completion", }, + { + name: "userTx sig verifier fails", + txCtx: &userTxContext{ + commonTxContext: &commonTxContext{ + userID: "testUser", + signer: emptySigner, + userCert: []byte{1, 2, 3}, + replicaSet: map[string]*url.URL{ + "node1": { + Path: "http://localhost:8888", + }, + }, + verifier: verifierFails, + restClient: NewRestClient("testUser", &mockHttpClient{ + process: asyncSubmit, + resp: okResponseAsync(), + }, emptySigner), + logger: logger, + }, + }, + wantErr: true, + errMsg: "signature verification failed nodeID node1, due to bad-mock-signature", + }, { name: "dbsTx correct async", txCtx: &dbsTxContext{ @@ -296,6 +382,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: asyncSubmit, resp: okResponseAsync(), @@ -317,6 +404,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: okResponse(), @@ -340,6 +428,7 @@ func TestTxCommit(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: syncSubmit, resp: serverTimeoutResponse(), @@ -352,6 +441,29 @@ func TestTxCommit(t *testing.T) { wantErr: true, errMsg: "timeout occurred on server side while submitting transaction, converted to asynchronous completion", }, + { + name: "dbsTx sig verifier fails", + txCtx: &dbsTxContext{ + commonTxContext: &commonTxContext{ + userID: "testUser", + signer: emptySigner, + userCert: []byte{1, 2, 3}, + replicaSet: map[string]*url.URL{ + "node1": { + Path: "http://localhost:8888", + }, + }, + verifier: verifierFails, + restClient: NewRestClient("testUser", &mockHttpClient{ + process: asyncSubmit, + resp: okResponseAsync(), + }, emptySigner), + logger: logger, + }, + }, + wantErr: true, + errMsg: "signature verification failed nodeID node1, due to bad-mock-signature", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -362,7 +474,7 @@ func TestTxCommit(t *testing.T) { _, receipt, err := tt.txCtx.Commit(tt.syncCommit) if tt.wantErr { require.Error(t, err) - require.Contains(t, tt.errMsg, err.Error()) + require.Contains(t, err.Error(),tt.errMsg) return } require.NoError(t, err) @@ -382,6 +494,9 @@ func TestTxQuery(t *testing.T) { emptySigner := &mocks.Signer{} emptySigner.On("Sign", mock.Anything).Return([]byte{1}, nil) + verifier := &mocks.SignatureVerifier{} + verifier.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(nil) + logger := createTestLogger(t) tests := []struct { @@ -401,6 +516,7 @@ func TestTxQuery(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: querySleep100, resp: okDataQueryResponse(), @@ -421,6 +537,7 @@ func TestTxQuery(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: querySleep100, resp: okDataQueryResponse(), @@ -442,6 +559,7 @@ func TestTxQuery(t *testing.T) { Path: "http://localhost:8888", }, }, + verifier: verifier, restClient: NewRestClient("testUser", &mockHttpClient{ process: querySleep10, resp: okDataQueryResponse(), @@ -548,7 +666,7 @@ func serverTimeoutResponse() *http.Response { ErrMsg: "Transaction processing commitTimeout", } errPbJson, _ := json.Marshal(errResp) - errRespReader := ioutil.NopCloser(bytes.NewReader([]byte(errPbJson))) + errRespReader := ioutil.NopCloser(bytes.NewReader(errPbJson)) return &http.Response{ StatusCode: http.StatusAccepted, Status: http.StatusText(http.StatusAccepted), @@ -561,7 +679,7 @@ func serverBadRequestResponse() *http.Response { ErrMsg: "Bad request error", } errPbJson, _ := json.Marshal(errResp) - errRespReader := ioutil.NopCloser(bytes.NewReader([]byte(errPbJson))) + errRespReader := ioutil.NopCloser(bytes.NewReader(errPbJson)) return &http.Response{ StatusCode: http.StatusBadRequest, Status: http.StatusText(http.StatusBadRequest), diff --git a/pkg/bcdb/user_tx_context_test.go b/pkg/bcdb/user_tx_context_test.go index e0d7ec8a..61c4184e 100644 --- a/pkg/bcdb/user_tx_context_test.go +++ b/pkg/bcdb/user_tx_context_test.go @@ -105,21 +105,14 @@ func TestUserContext_MalformedRequest(t *testing.T) { bcdb, _ := connectAndOpenAdminSession(t, testServer, clientCertTemDir) // New session with admin user context - session, err := bcdb.Session(&sdkConfig.SessionConfig{ + _, err = bcdb.Session(&sdkConfig.SessionConfig{ UserConfig: &sdkConfig.UserConfig{ UserID: "adminX", CertPath: path.Join(clientCertTemDir, "admin.pem"), PrivateKeyPath: path.Join(clientCertTemDir, "admin.key"), }, }) - require.NoError(t, err) - - // transaction init should fail since wrong user id was configured - // in the session config, therefore it will fail to fetch node - // certificate and fail to start transaction - _, err = session.UsersTx() - require.Error(t, err) - require.Contains(t, err.Error(), "failed to obtain server's certificate") + require.EqualError(t, err, "cannot create a signature verifier: failed to obtain the servers' certificates") } func TestUserContext_GetUserFailureScenarios(t *testing.T) { @@ -185,6 +178,10 @@ func TestUserContext_GetUserFailureScenarios(t *testing.T) { func TestUserContext_TxSubmissionFullScenario(t *testing.T) { signer := &mocks.Signer{} signer.On("Sign", mock.Anything).Return([]byte{0}, nil) + + verifier := &mocks.SignatureVerifier{} + verifier.On("Verify", mock.Anything, mock.Anything, mock.Anything).Return(nil) + restClient := &mocks.RestClient{} expectedUser := &types.User{ @@ -236,6 +233,7 @@ func TestUserContext_TxSubmissionFullScenario(t *testing.T) { userID: "testUserId", restClient: restClient, logger: logger, + verifier: verifier, replicaSet: map[string]*url.URL{ "node1": { Path: "http://localhost:8888", diff --git a/pkg/bcdb/verifier.go b/pkg/bcdb/verifier.go new file mode 100644 index 00000000..2eacda90 --- /dev/null +++ b/pkg/bcdb/verifier.go @@ -0,0 +1,49 @@ +package bcdb + +import ( + "crypto/x509" + + "github.com/IBM-Blockchain/bcdb-server/pkg/logger" + "github.com/pkg/errors" +) + +//go:generate mockery --dir . --name SignatureVerifier --case underscore --output mocks/ + +// SignatureVerifier verifies servers' signatures provided within response +type SignatureVerifier interface { + // Verify signature created by entityID for given payload + Verify(entityID string, payload, signature []byte) error +} + +type sigVerifier struct { + nodesCerts map[string]*x509.Certificate + logger *logger.SugarLogger +} + +// NewVerifier creates instance of the SignatureVerifier +func NewVerifier(certs map[string]*x509.Certificate, logger *logger.SugarLogger) (SignatureVerifier, error) { + if len(certs) == 0 { + logger.Error("no servers' certificates provided") + return nil, errors.New("no servers' certificates provided") + } + + return &sigVerifier{ + nodesCerts: certs, + logger: logger, + }, nil +} + +// Verify signature created by entityID for given payload +func (s *sigVerifier) Verify(entityID string, payload, signature []byte) error { + cert, exists := s.nodesCerts[entityID] + if !exists { + return errors.Errorf("there is no certificate for entityID = %s", entityID) + } + + err := cert.CheckSignature(cert.SignatureAlgorithm, payload, signature) + if err != nil { + s.logger.Errorf("signature verification failed entityID %s, due to %s", entityID, err) + return errors.Errorf("signature verification failed entityID %s, due to %s", entityID, err) + } + return nil +} diff --git a/pkg/bcdb/verifier_test.go b/pkg/bcdb/verifier_test.go new file mode 100644 index 00000000..1597e964 --- /dev/null +++ b/pkg/bcdb/verifier_test.go @@ -0,0 +1,123 @@ +package bcdb + +import ( + crypto2 "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/IBM-Blockchain/bcdb-server/pkg/crypto" + "github.com/IBM-Blockchain/bcdb-server/pkg/server/testutils" + "github.com/stretchr/testify/require" +) + +func TestSigVerifier_Verify(t *testing.T) { + rootCAPemCert, caPrivKey, err := testutils.GenerateRootCA("Clients RootCA", "127.0.0.1") + require.NoError(t, err) + require.NotNil(t, rootCAPemCert) + require.NotNil(t, caPrivKey) + + keyPair, err := tls.X509KeyPair(rootCAPemCert, caPrivKey) + require.NoError(t, err) + require.NotNil(t, keyPair) + + pemCert, privKey, err := testutils.IssueCertificate("BCDB Client Test", "127.0.0.1", keyPair) + require.NoError(t, err) + + payload := []byte{0} + message, err := crypto.ComputeSHA256Hash(payload) + require.NoError(t, err) + + keyLoader := &crypto.KeyLoader{} + signerKey, err := keyLoader.Load(privKey) + require.NoError(t, err) + + sig, err := signerKey.(*ecdsa.PrivateKey).Sign(rand.Reader, message, crypto2.SHA256) + require.NoError(t, err) + + asn1Cert, _ := pem.Decode(pemCert) + cert, err := x509.ParseCertificate(asn1Cert.Bytes) + require.NoError(t, err) + + tests := []struct { + name string + certs map[string]*x509.Certificate + payload []byte + signature []byte + nodeName string + isErrorExpected bool + errorMessage string + }{ + { + name: "valid signature", + nodeName: "node1", + certs: map[string]*x509.Certificate{ + "node1": cert, + }, + payload: payload, + signature: sig, + }, + { + name: "wrong signature", + nodeName: "node1", + certs: map[string]*x509.Certificate{ + "node1": cert, + }, + payload: payload, + signature: []byte{0}, + isErrorExpected: true, + errorMessage: "signature verification failed", + }, + { + name: "wrong payload", + nodeName: "node1", + certs: map[string]*x509.Certificate{ + "node1": cert, + }, + payload: []byte{1}, + signature: sig, + isErrorExpected: true, + errorMessage: "signature verification failed", + }, + { + name: "missing cert", + nodeName: "node1", + certs: map[string]*x509.Certificate{ + "node2": cert, + }, + payload: payload, + signature: sig, + isErrorExpected: true, + errorMessage: "there is no certificate for entityID", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + logger := createTestLogger(t) + + verifier, err := NewVerifier(test.certs, logger) + require.NoError(t, err) + err = verifier.Verify(test.nodeName, test.payload, test.signature) + if !test.isErrorExpected { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), test.errorMessage) + } + }) + } +} + +func TestFailToCreateVerifier(t *testing.T) { + t.Parallel() + logger := createTestLogger(t) + + _, err := NewVerifier(map[string]*x509.Certificate{}, logger) + require.Error(t, err) + require.Contains(t, err.Error(), "no servers' certificates provided") + +}