From 59513595458c07ff3dff88a7e80b4ca3b872fa37 Mon Sep 17 00:00:00 2001 From: zhangl Date: Tue, 25 May 2021 13:43:07 -0400 Subject: [PATCH] Issue 63 - Add data verification in Sync Service SPIs Signed-off-by: zhangl --- .gitignore | 2 + common/common.go | 7 + core/base/apiModule.go | 119 +++------- core/base/apiModule_test.go | 4 + core/base/base.go | 2 + core/communications/httpCommunication.go | 56 ++++- core/dataURI/dataURI.go | 50 ++++ core/dataVerifier/dataVerifier.go | 162 +++++++++++++ core/dataVerifier/dataVerifier_test.go | 279 +++++++++++++++++++++++ core/storage/boltStorage.go | 6 + core/storage/mongoStorage.go | 6 +- runCoverage.sh | 3 +- 12 files changed, 603 insertions(+), 93 deletions(-) create mode 100644 core/dataVerifier/dataVerifier.go create mode 100644 core/dataVerifier/dataVerifier_test.go diff --git a/.gitignore b/.gitignore index 7a61425..54a875e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ build/* core/base/persist core/communications/persist core/storage/persist +core/dataVerifier/dataURITmp +core/dataVerifier/persist .DS_STORE vendor/*/ diff --git a/common/common.go b/common/common.go index 298dff6..bb7f8c4 100644 --- a/common/common.go +++ b/common/common.go @@ -901,6 +901,13 @@ func BlockUntilNoRunningGoRoutines() { waitingOnBlockChannel = false } +func IsValidHashAlgorithm(hashAlgorithm string) bool { + if hashAlgorithm == Sha1 || hashAlgorithm == Sha256 { + return true + } + return false +} + // IsValidName checks if the string only contains letters, digits, and !@#%^*-_.~ var IsValidName = regexp.MustCompile(`^[a-zA-Z0-9|!|@|#|$|^|*|\-|_|.|~|\pL|\pN]+$`).MatchString diff --git a/core/base/apiModule.go b/core/base/apiModule.go index 44095d3..60d796b 100644 --- a/core/base/apiModule.go +++ b/core/base/apiModule.go @@ -2,8 +2,6 @@ package base import ( "bytes" - "crypto/rsa" - "crypto/x509" "encoding/base64" "fmt" "io" @@ -17,6 +15,7 @@ import ( "github.com/open-horizon/edge-sync-service/common" "github.com/open-horizon/edge-sync-service/core/communications" "github.com/open-horizon/edge-sync-service/core/dataURI" + "github.com/open-horizon/edge-sync-service/core/dataVerifier" "github.com/open-horizon/edge-sync-service/core/storage" "github.com/open-horizon/edge-utilities/logger" "github.com/open-horizon/edge-utilities/logger/log" @@ -258,13 +257,21 @@ func UpdateObject(orgID string, objectType string, objectID string, metaData com } else if data != nil { // data signature verification if metadata has both publicKey and signature // data is nil for metaOnly object. Meta-only object will not apply data verification - if metaData.HashAlgorithm != "" && metaData.PublicKey != "" && metaData.Signature != "" { - dataReader := bytes.NewReader(data) + if common.IsValidHashAlgorithm(metaData.HashAlgorithm) && metaData.PublicKey != "" && metaData.Signature != "" { // will no store data if object metadata not exist - if success, err := VerifyAndStoreData(dataReader, orgID, metaData.ObjectType, metaData.ObjectID, metaData.HashAlgorithm, metaData.PublicKey, metaData.Signature, false); err != nil || !success { + dataReader := bytes.NewReader(data) + dataVf := dataVerifier.NewDataVerifier(metaData.HashAlgorithm, metaData.PublicKey, metaData.Signature) + if success, err := dataVf.VerifyDataSignature(dataReader, orgID, objectType, objectID, ""); !success || err != nil { + if trace.IsLogging(logger.ERROR) { + trace.Error("Failed to verify data for object %s %s, remove temp data\n", objectType, objectID) + } + dataVf.RemoveTempData(orgID, objectType, objectID, "") + common.ObjectLocks.Unlock(lockIndex) return err } + dataVf.RemoveTempData(orgID, objectType, objectID, "") + } metaData.ObjectSize = int64(len(data)) @@ -533,16 +540,35 @@ func PutObjectData(orgID string, objectType string, objectID string, dataReader return false, &common.InvalidRequest{Message: "Can't update data, the NoData flag is set to true"} } - if metaData.HashAlgorithm != "" && metaData.PublicKey != "" && metaData.Signature != "" { + var dataVf *dataVerifier.DataVerifier + if common.IsValidHashAlgorithm(metaData.HashAlgorithm) && metaData.PublicKey != "" && metaData.Signature != "" { //start data verification if trace.IsLogging(logger.DEBUG) { trace.Debug("In PutObjectData. Start data verification %s %s\n", objectType, objectID) } - if success, err := VerifyAndStoreData(dataReader, orgID, objectType, objectID, metaData.HashAlgorithm, metaData.PublicKey, metaData.Signature, true); !success || err != nil { + dataVf = dataVerifier.NewDataVerifier(metaData.HashAlgorithm, metaData.PublicKey, metaData.Signature) + if success, err := dataVf.VerifyDataSignature(dataReader, orgID, objectType, objectID, ""); !success || err != nil { + if trace.IsLogging(logger.ERROR) { + trace.Error("Failed to verify data for object %s %s, remove temp data\n", objectType, objectID) + } + dataVf.RemoveTempData(orgID, objectType, objectID, "") common.ObjectLocks.Unlock(lockIndex) return false, &common.InvalidRequest{Message: "Failed to verify and store data, Error: " + err.Error()} } + if trace.IsLogging(logger.DEBUG) { + trace.Debug("In PutObjectData. data verified for object %s %s\n", objectType, objectID) + } + + } + + // If the data has been verified, then we retrieve the temp data, store in DB, and delete temp data + if dataVf != nil { + if err := dataVf.StoreVerifiedData(orgID, objectType, objectID, ""); err != nil { + dataVf.RemoveTempData(orgID, objectType, objectID, "") + common.ObjectLocks.Unlock(lockIndex) + return false, err + } } else { if exists, err := store.StoreObjectData(orgID, objectType, objectID, dataReader); err != nil || !exists { common.ObjectLocks.Unlock(lockIndex) @@ -1269,82 +1295,3 @@ func RetrieveACLsInOrg(aclType string, orgID string) ([]string, common.SyncServi defer apiLock.Unlock() return store.RetrieveACLsInOrg(aclType, orgID) } - -func VerifyAndStoreData(data io.Reader, orgID string, objectType string, objectID string, hashAlgo string, publicKey string, signature string, storeData bool) (bool, common.SyncServiceError) { - if hashAlgo == "" || publicKey == "" || signature == "" { - message := fmt.Sprintf("hash algorithm, public key or signature is empty") - return false, &common.InvalidRequest{Message: message} - - } - - var dataReader io.Reader - var err error - if publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKey); err != nil { - return false, &common.InvalidRequest{Message: "PublicKey is not base64 encoded. Error: " + err.Error()} - } else if signatureBytes, err := base64.StdEncoding.DecodeString(signature); err != nil { - return false, &common.InvalidRequest{Message: "Signature is not base64 encoded. Error: " + err.Error()} - } else { - if trace.IsLogging(logger.DEBUG) { - trace.Debug("In VerifyAndStoreData, starting data hash\n") - } - dataHash, cryptoHash, err := common.GetHash(hashAlgo) - if err != nil { - return false, &common.InvalidRequest{Message: "Failed to get hash. Error: " + err.Error()} - } - - dr := io.TeeReader(data, dataHash) - - if trace.IsLogging(logger.DEBUG) { - trace.Debug("In VerifyAndStoreData, storing temp data for object %s %s\n", objectType, objectID) - } - - if exists, err := store.StoreObjectTempData(orgID, objectType, objectID, dr); err != nil || !exists { - return false, err - } - - dataHashSum := dataHash.Sum(nil) - - if pubKey, err := x509.ParsePKIXPublicKey(publicKeyBytes); err != nil { - return false, &common.InvalidRequest{Message: "Failed to parse public key, Error: " + err.Error()} - } else { - pubKeyToUse := pubKey.(*rsa.PublicKey) - if err = rsa.VerifyPSS(pubKeyToUse, cryptoHash, dataHashSum, signatureBytes, nil); err != nil { - store.RemoveObjectTempData(orgID, objectType, objectID) - return false, &common.InvalidRequest{Message: "Failed to verify data with public key and data signature, Error: " + err.Error()} - } - } - } - - if trace.IsLogging(logger.DEBUG) { - trace.Debug("In VerifyAndStoreData, data verification is done, retrieve temp data for object %s %s\n", objectType, objectID) - } - - if storeData { - dataReader, err = store.RetrieveTempObjectData(orgID, objectType, objectID) - if err != nil { - return false, &common.InvalidRequest{Message: "Failed to read temp data fro, Error: " + err.Error()} - } else if dataReader == nil { - return false, &common.InvalidRequest{Message: "Read empty temp data, Error: " + err.Error()} - } - - if trace.IsLogging(logger.DEBUG) { - trace.Debug("In VerifyAndStoreData, storing data for object %s %s\n", objectType, objectID) - } - - if exists, err := store.StoreObjectData(orgID, objectType, objectID, dataReader); err != nil || !exists { - return false, err - } - store.CloseDataReader(dataReader) - } - - if trace.IsLogging(logger.DEBUG) { - trace.Debug("In VerifyAndStoreData, remove temp data for object %s %s\n", objectType, objectID) - } - - if err = store.RemoveObjectTempData(orgID, objectType, objectID); err != nil { - if trace.IsLogging(logger.ERROR) { - trace.Error("In VerifyAndStoreData. Failed to remove temp data for object\n") - } - } - return true, nil -} diff --git a/core/base/apiModule_test.go b/core/base/apiModule_test.go index 1bede30..b6631e9 100644 --- a/core/base/apiModule_test.go +++ b/core/base/apiModule_test.go @@ -17,6 +17,7 @@ import ( "github.com/open-horizon/edge-sync-service/common" "github.com/open-horizon/edge-sync-service/core/communications" + "github.com/open-horizon/edge-sync-service/core/dataVerifier" "github.com/open-horizon/edge-sync-service/core/storage" ) @@ -81,6 +82,8 @@ func TestObjectAPI(t *testing.T) { func testObjectAPI(store storage.Storage, t *testing.T) { communications.Store = store + dataVerifier.Store = store + common.InitObjectLocks() dests := []string{"device:dev1", "device2:dev", "device2:dev1"} @@ -654,6 +657,7 @@ func TestESSObjectDeletedAPI(t *testing.T) { func testESSObjectDeletedAPI(store storage.Storage, t *testing.T) { communications.Store = store + dataVerifier.Store = store common.InitObjectLocks() if err := store.Init(); err != nil { diff --git a/core/base/base.go b/core/base/base.go index 58fe65e..0625f08 100644 --- a/core/base/base.go +++ b/core/base/base.go @@ -9,6 +9,7 @@ import ( "github.com/open-horizon/edge-sync-service/common" "github.com/open-horizon/edge-sync-service/core/communications" + "github.com/open-horizon/edge-sync-service/core/dataVerifier" "github.com/open-horizon/edge-sync-service/core/leader" "github.com/open-horizon/edge-sync-service/core/security" "github.com/open-horizon/edge-sync-service/core/storage" @@ -112,6 +113,7 @@ func Start(swaggerFile string, registerHandlers bool) common.SyncServiceError { } communications.Store = store security.Store = store + dataVerifier.Store = store leader.StartLeaderDetermination(store) diff --git a/core/communications/httpCommunication.go b/core/communications/httpCommunication.go index ca724e9..5af6087 100644 --- a/core/communications/httpCommunication.go +++ b/core/communications/httpCommunication.go @@ -16,6 +16,7 @@ import ( "github.com/open-horizon/edge-sync-service/common" "github.com/open-horizon/edge-sync-service/core/dataURI" + "github.com/open-horizon/edge-sync-service/core/dataVerifier" "github.com/open-horizon/edge-sync-service/core/security" "github.com/open-horizon/edge-utilities/logger" "github.com/open-horizon/edge-utilities/logger/log" @@ -603,7 +604,26 @@ func (communication *HTTP) GetData(metaData common.MetaData, offset int64) commo lockIndex := common.HashStrings(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID) common.ObjectLocks.Lock(lockIndex) - if metaData.DestinationDataURI != "" { + var dataVf *dataVerifier.DataVerifier + if common.IsValidHashAlgorithm(metaData.HashAlgorithm) && metaData.PublicKey != "" && metaData.Signature != "" { + dataVf = dataVerifier.NewDataVerifier(metaData.HashAlgorithm, metaData.PublicKey, metaData.Signature) + if dataVerified, err := dataVf.VerifyDataSignature(response.Body, metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI); !dataVerified || err != nil { + if trace.IsLogging(logger.ERROR) { + trace.Error("Failed to verify data for object %s %s, remove temp data\n", metaData.ObjectType, metaData.ObjectID) + } + dataVf.RemoveTempData(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI) + common.ObjectLocks.Unlock(lockIndex) + return err + } + } + + if dataVf != nil { + if err := dataVf.StoreVerifiedData(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI); err != nil { + dataVf.RemoveTempData(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI) + common.ObjectLocks.Unlock(lockIndex) + return err + } + } else if metaData.DestinationDataURI != "" { if _, err := dataURI.StoreData(metaData.DestinationDataURI, response.Body, 0); err != nil { common.ObjectLocks.Unlock(lockIndex) return err @@ -618,6 +638,7 @@ func (communication *HTTP) GetData(metaData common.MetaData, offset int64) commo return &Error{"Failed to store object's data."} } } + if err := Store.UpdateObjectStatus(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, common.CompletelyReceived); err != nil { common.ObjectLocks.Unlock(lockIndex) return &Error{fmt.Sprintf("Error in GetData: %s\n", err)} @@ -908,13 +929,44 @@ func (communication *HTTP) handlePutData(orgID string, objectType string, object lockIndex := common.HashStrings(orgID, objectType, objectID) common.ObjectLocks.Lock(lockIndex) - if found, err := Store.StoreObjectData(orgID, objectType, objectID, request.Body); err != nil { + // retrieve metadata and check if this data need to be verified + metaData, err := Store.RetrieveObject(orgID, objectType, objectID) + if metaData == nil { + common.ObjectLocks.Unlock(lockIndex) + return &common.InvalidRequest{Message: "Failed to find object to set data"} + } + if err != nil { + common.ObjectLocks.Unlock(lockIndex) + return err + } + + var dataVf *dataVerifier.DataVerifier + if common.IsValidHashAlgorithm(metaData.HashAlgorithm) && metaData.PublicKey != "" && metaData.Signature != "" { + dataVf = dataVerifier.NewDataVerifier(metaData.HashAlgorithm, metaData.PublicKey, metaData.Signature) + if dataVerified, err := dataVf.VerifyDataSignature(request.Body, metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI); !dataVerified || err != nil { + if trace.IsLogging(logger.ERROR) { + trace.Error("Failed to verify data for object %s %s, remove temp data\n", metaData.ObjectType, metaData.ObjectID) + } + dataVf.RemoveTempData(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI) + common.ObjectLocks.Unlock(lockIndex) + return err + } + } + + if dataVf != nil { + if err := dataVf.StoreVerifiedData(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI); err != nil { + dataVf.RemoveTempData(metaData.DestOrgID, metaData.ObjectType, metaData.ObjectID, metaData.DestinationDataURI) + common.ObjectLocks.Unlock(lockIndex) + return err + } + } else if found, err := Store.StoreObjectData(orgID, objectType, objectID, request.Body); err != nil { // No data verification applied, then store data directly common.ObjectLocks.Unlock(lockIndex) return err } else if !found { common.ObjectLocks.Unlock(lockIndex) return &common.InvalidRequest{Message: "Failed to find object to set data"} } + if err := Store.UpdateObjectStatus(orgID, objectType, objectID, common.CompletelyReceived); err != nil { common.ObjectLocks.Unlock(lockIndex) return err diff --git a/core/dataURI/dataURI.go b/core/dataURI/dataURI.go index e2b3f5d..5b50575 100644 --- a/core/dataURI/dataURI.go +++ b/core/dataURI/dataURI.go @@ -92,6 +92,56 @@ func StoreData(uri string, dataReader io.Reader, dataLength uint32) (int64, comm return written, nil } +// StoreTempData writes the data to the tmp file stored at the given URI +func StoreTempData(uri string, dataReader io.Reader, dataLength uint32) (int64, common.SyncServiceError) { + if trace.IsLogging(logger.TRACE) { + trace.Trace("Storing data at %s", uri) + } + dataURI, err := url.Parse(uri) + if err != nil || !strings.EqualFold(dataURI.Scheme, "file") { + return 0, &Error{"Invalid data URI"} + } + + filePath := dataURI.Path + ".tmp" + file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return 0, common.CreateError(err, fmt.Sprintf("Failed to open file %s to write data. Error: ", dataURI.Path)) + } + defer file.Close() + + if _, err = file.Seek(0, io.SeekStart); err != nil { + return 0, &common.IOError{Message: "Failed to seek to the start of a file. Error: " + err.Error()} + } + + written, err := io.Copy(file, dataReader) + if err != nil && err != io.EOF { + return 0, &common.IOError{Message: "Failed to write to file. Error: " + err.Error()} + } + if written != int64(dataLength) && dataLength != 0 { + return 0, &common.IOError{Message: "Failed to write all the data to file."} + } + return written, nil +} + +// StoreDataFromTempData rename {dataURI.Path}.tmp to {dataURI.Path} +func StoreDataFromTempData(uri string) common.SyncServiceError { + if trace.IsLogging(logger.TRACE) { + trace.Trace("Storing data from temp data at %s", uri) + } + dataURI, err := url.Parse(uri) + if err != nil || !strings.EqualFold(dataURI.Scheme, "file") { + return &Error{"Invalid data URI"} + } + + tmpFilePath := dataURI.Path + ".tmp" + + if err := os.Rename(tmpFilePath, dataURI.Path); err != nil { + return &common.IOError{Message: "Failed to rename data file. Error: " + err.Error()} + } + + return nil +} + // GetData retrieves the data stored at the given URI. // After reading, the reader has to be closed. func GetData(uri string) (io.Reader, common.SyncServiceError) { diff --git a/core/dataVerifier/dataVerifier.go b/core/dataVerifier/dataVerifier.go new file mode 100644 index 0000000..4a53dc3 --- /dev/null +++ b/core/dataVerifier/dataVerifier.go @@ -0,0 +1,162 @@ +package dataVerifier + +import ( + "crypto" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "fmt" + "hash" + "io" + + "github.com/open-horizon/edge-sync-service/common" + "github.com/open-horizon/edge-sync-service/core/dataURI" + "github.com/open-horizon/edge-sync-service/core/storage" + "github.com/open-horizon/edge-utilities/logger" + "github.com/open-horizon/edge-utilities/logger/trace" +) + +type DataVerifier struct { + dataHash hash.Hash + cryptoHashType crypto.Hash + publicKey string + signature string + writeThrough bool +} + +// Store is a reference to the Storage being used +var Store storage.Storage + +func NewDataVerifier(hashAlgorithm string, publicKey string, signature string) *DataVerifier { + // default is to verify data (writeThrough == false) + writeThrough := false + var dataHash hash.Hash + var cryptoHashType crypto.Hash + var err error + + if !common.IsValidHashAlgorithm(hashAlgorithm) || publicKey == "" || signature == "" { + writeThrough = true + } + + if dataHash, cryptoHashType, err = common.GetHash(hashAlgorithm); err != nil { + writeThrough = true + } + + return &DataVerifier{ + dataHash: dataHash, + cryptoHashType: cryptoHashType, + publicKey: publicKey, + signature: signature, + writeThrough: writeThrough, + } +} + +// VerifyDataSignature is to verify the data. This function will generate the tmp data in storage. Call RemoveTempData() after verification to remove the tmp data +func (dataVerifier *DataVerifier) VerifyDataSignature(data io.Reader, orgID string, objectType string, objectID string, destinationDataURI string) (bool, common.SyncServiceError) { + if dataVerifier.writeThrough { + return true, nil + } + + if publicKeyBytes, err := base64.StdEncoding.DecodeString(dataVerifier.publicKey); err != nil { + return false, &common.InternalError{Message: "PublicKey is not base64 encoded. Error: " + err.Error()} + } else if signatureBytes, err := base64.StdEncoding.DecodeString(dataVerifier.signature); err != nil { + return false, &common.InternalError{Message: "Signature is not base64 encoded. Error: " + err.Error()} + } else { + dr := io.TeeReader(data, dataVerifier.dataHash) + if trace.IsLogging(logger.DEBUG) { + trace.Debug("DataVerifier - In VerifyDataSignature, verifying and storing temp data for object %s %s\n", objectType, objectID) + } + + if destinationDataURI != "" { + if _, err := dataURI.StoreTempData(destinationDataURI, dr, 0); err != nil { + return false, err + } + } else { + if exists, err := Store.StoreObjectTempData(orgID, objectType, objectID, dr); err != nil || !exists { + return false, err + } + } + + return dataVerifier.verifyHelper(publicKeyBytes, signatureBytes) + } +} + +// StoreVerifiedData will store the data from temp data that generated during data verification. And remove temp data +func (dataVerifier *DataVerifier) StoreVerifiedData(orgID string, objectType string, objectID string, destinationDataURI string) common.SyncServiceError { + if dataVerifier.writeThrough { + return nil + } + + if destinationDataURI != "" { + if trace.IsLogging(logger.DEBUG) { + trace.Debug("DataVerifier - In StoreVerifiedData, store data from tmp data for object %s %s at URI %s\n", objectType, objectID, destinationDataURI) + } + // rename the {file}.tmp to {file} + if err := dataURI.StoreDataFromTempData(destinationDataURI); err != nil { + return err + } + } else { + // 1. Retrieve temp data, 2. Store object data, 3. Remove temp data + if trace.IsLogging(logger.DEBUG) { + trace.Debug("DataVerifier - In StoreVerifiedData, retrieve temp data for object %s %s\n", objectType, objectID) + } + + dataReader, err := Store.RetrieveTempObjectData(orgID, objectType, objectID) + if err != nil { + return &common.InvalidRequest{Message: "Failed to read temp data fro, Error: " + err.Error()} + } else if dataReader == nil { + return &common.InvalidRequest{Message: "Read empty temp data, Error: " + err.Error()} + } + + if trace.IsLogging(logger.DEBUG) { + trace.Debug("DataVerifier - In StoreVerifiedData, storing data for object %s %s\n", objectType, objectID) + } + + if exists, err := Store.StoreObjectData(orgID, objectType, objectID, dataReader); err != nil { + Store.CloseDataReader(dataReader) + return err + } else if !exists { + Store.CloseDataReader(dataReader) + message := fmt.Sprintf("Object metadata is not found for object %s %s %s, Error: %s\n", orgID, objectType, objectID, err.Error()) + return &common.InternalError{Message: message} + } + Store.CloseDataReader(dataReader) + + if trace.IsLogging(logger.DEBUG) { + trace.Debug("DataVerifier - In StoreVerifiedData, remove temp data for object %s %s\n", objectType, objectID) + } + + if err := Store.RemoveObjectTempData(orgID, objectType, objectID); err != nil { + return err + } + } + + return nil + +} + +// CleanUp function is to clean up the temp file created during data verification +func (dataVerifier *DataVerifier) RemoveTempData(orgID string, objectType string, objectID string, destinationDataURI string) common.SyncServiceError { + if destinationDataURI != "" { + tmpFilePath := destinationDataURI + ".tmp" + if err := dataURI.DeleteStoredData(tmpFilePath); err != nil { + return err + } + } else if err := Store.RemoveObjectTempData(orgID, objectType, objectID); err != nil { + return err + } + return nil +} + +func (dataVerifier *DataVerifier) verifyHelper(publicKeyBytes []byte, signatureBytes []byte) (bool, common.SyncServiceError) { + dataHashSum := dataVerifier.dataHash.Sum(nil) + if pubKey, err := x509.ParsePKIXPublicKey(publicKeyBytes); err != nil { + return false, &common.InternalError{Message: "Failed to parse public key, Error: " + err.Error()} + } else { + pubKeyToUse := pubKey.(*rsa.PublicKey) + if err = rsa.VerifyPSS(pubKeyToUse, dataVerifier.cryptoHashType, dataHashSum, signatureBytes, nil); err != nil { + return false, &common.InternalError{Message: "Failed to verify data with public key and data signature, Error: " + err.Error()} + } + } + return true, nil +} diff --git a/core/dataVerifier/dataVerifier_test.go b/core/dataVerifier/dataVerifier_test.go new file mode 100644 index 0000000..ffc6736 --- /dev/null +++ b/core/dataVerifier/dataVerifier_test.go @@ -0,0 +1,279 @@ +package dataVerifier + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "fmt" + "hash" + "io" + "os" + "testing" + + "github.com/open-horizon/edge-sync-service/common" + "github.com/open-horizon/edge-sync-service/core/storage" +) + +const destinationURI = "/dataURITmp" + +var destinationURIDir string +var destinationURIDirFileVerified string +var destinationURIDirFileWrong string + +var dataToSign, wrongDataToSign []byte +var orgID, objectType, objectID string + +func TestNewDataVerifier(t *testing.T) { + testNewDataVerifier(common.Sha1, t) + testNewDataVerifier(common.Sha256, t) +} + +func TestVerifyDataSignature(t *testing.T) { + setupTestVars() + + if status := setupDB(common.Mongo); status != "" { + t.Errorf("Failed to setup %s storage, error: %s", common.Mongo, status) + } + defer Store.Stop() + testVerifyDataSignature(common.Sha1, t) + testVerifyDataSignature(common.Sha256, t) + + if status := setupDB(common.Bolt); status != "" { + t.Errorf("Failed to setup %s storage, error: %s", common.Bolt, status) + } + defer Store.Stop() + testVerifyDataSignature(common.Sha1, t) + testVerifyDataSignature(common.Sha256, t) +} + +func testNewDataVerifier(hashAlgo string, t *testing.T) { + dataToSign := []byte("dataVerifier test") + + var publicKey, signature string + var err error + if publicKey, signature, err = setupDataSignature(dataToSign, hashAlgo); err != nil { + t.Errorf("Failed to set up publicKey and signature with %s for data. Error: %s\n", hashAlgo, err.Error()) + } + + var dataVerifierToTest *DataVerifier + dataVerifierToTest = NewDataVerifier("", publicKey, signature) + if !dataVerifierToTest.writeThrough { + t.Error("\"writeThrough\" field should be true if hash algorithm is not SHA1 and SHA256") + } + + dataVerifierToTest = NewDataVerifier(hashAlgo, "", signature) + if !dataVerifierToTest.writeThrough { + t.Error("\"writeThrough\" field should be true if publicKey is empty") + } + + dataVerifierToTest = NewDataVerifier(hashAlgo, publicKey, "") + if !dataVerifierToTest.writeThrough { + t.Error("\"writeThrough\" field should be true if signature is empty") + } + + dataVerifierToTest = NewDataVerifier(hashAlgo, publicKey, signature) + if dataVerifierToTest.writeThrough { + t.Error("\"writeThrough\" field should be false with valid input") + } +} + +func testVerifyDataSignature(hashAlgo string, t *testing.T) { + var publicKey, signature string + var err error + if publicKey, signature, err = setupDataSignature(dataToSign, hashAlgo); err != nil { + t.Errorf("Failed to set up publicKey and signature with %s for data. Error: %s\n", hashAlgo, err.Error()) + } + + dataVerifier := NewDataVerifier(hashAlgo, publicKey, signature) + if verified, err := dataVerifier.VerifyDataSignature(bytes.NewReader(wrongDataToSign), orgID, objectType, objectID, ""); err == nil || verified { + t.Errorf("Error verifying data, wrong data should not pass verification. verified: %t, error: %s\n", verified, err.Error()) + } + + // Need another dataVerifier object because re-use old object will make the hash calculated on top of the hash from old object + dataVerifier = NewDataVerifier(hashAlgo, publicKey, signature) + if verified, err := dataVerifier.VerifyDataSignature(bytes.NewReader(dataToSign), orgID, objectType, objectID, ""); err != nil || !verified { + t.Errorf("Error verifying data, data should pass verification. verified: %t, error: %s\n", verified, err.Error()) + } + + var reader io.Reader + if reader, err = Store.RetrieveTempObjectData(orgID, objectType, objectID); err != nil { + Store.CloseDataReader(reader) + t.Errorf("Error get temp object data for %s %s %s, error: %s\n", orgID, objectType, objectID, err.Error()) + } + Store.CloseDataReader(reader) + + // Store object metadata + objMetaData := common.MetaData{ + ObjectID: objectID, + ObjectType: objectType, + DestOrgID: orgID, + HashAlgorithm: hashAlgo, + PublicKey: publicKey, + Signature: signature, + } + + // Store object metadata + if _, err := Store.StoreObject(objMetaData, []byte{}, ""); err != nil { + t.Errorf("Failed to store object metadata, error: %s", err.Error()) + } + + // Store verified data + if err = dataVerifier.StoreVerifiedData(orgID, objectType, objectID, ""); err != nil { + t.Errorf("Error storeing verified data for %s %s %s, error: %s\n", orgID, objectType, objectID, err.Error()) + } + + if reader, err = Store.RetrieveTempObjectData(orgID, objectType, objectID); err != nil { + t.Errorf("Error retrieve verified data for %s %s %s, error: %s\n", orgID, objectType, objectID, err.Error()) + } else if reader != nil { + Store.CloseDataReader(reader) + t.Errorf("Temp object data for %s %s %s should be deleted\n", orgID, objectType, objectID) + } + + if reader, err = Store.RetrieveObjectData(orgID, objectType, objectID); err != nil { + Store.CloseDataReader(reader) + t.Errorf("Error get object data for %s %s %s, error: %s\n", orgID, objectType, objectID, err.Error()) + } + Store.CloseDataReader(reader) + +} + +func TestVerifyDataSignatureWithDestintionDataURI(t *testing.T) { + setupDataURIPath() + setupTestVars() + destinationURIDirFileVerified = "file:///" + destinationURIDir + "/" + "test_verified.txt" + destinationURIDirFileWrong = "file:///" + destinationURIDir + "/" + "test_wrong.txt" + + testVerifyDataSignatureWithDestintionDataURI(common.Sha1, t) + testVerifyDataSignatureWithDestintionDataURI(common.Sha256, t) + +} + +func testVerifyDataSignatureWithDestintionDataURI(hashAlgo string, t *testing.T) { + var publicKey, signature string + var err error + if publicKey, signature, err = setupDataSignature(dataToSign, hashAlgo); err != nil { + t.Errorf("Failed to set up publicKey and signature with SHA1 for data. Error: %s\n", err.Error()) + } + + // Verify Signature + dataVerifier := NewDataVerifier(hashAlgo, publicKey, signature) + if verified, err := dataVerifier.VerifyDataSignature(bytes.NewReader(dataToSign), orgID, objectType, objectID, destinationURIDirFileVerified); err != nil || !verified { + t.Errorf("Error verifying data, data should pass verification. verified: %t, error: %s\n", verified, err.Error()) + } + + if verified, err := dataVerifier.VerifyDataSignature(bytes.NewReader(wrongDataToSign), orgID, objectType, objectID, destinationURIDirFileWrong); err == nil || verified { + t.Errorf("Error verifying data, wrong data should not pass verification. verified: %t, error: %s\n", verified, err.Error()) + } + + // check .tmp file is created + if _, err := os.Stat(destinationURIDir + "/test_verified.txt.tmp"); err != nil { + t.Errorf("Error checking files at destinationURI %s, error: %s\n", destinationURIDirFileVerified, err.Error()) + } + + if _, err := os.Stat(destinationURIDir + "/test_wrong.txt.tmp"); err != nil { + t.Errorf("Error checking files at destinationURI %s.tmp, error: %s\n", destinationURIDirFileWrong, err.Error()) + } + + // check file is created from .tmp file + if err := dataVerifier.StoreVerifiedData(orgID, objectType, objectID, destinationURIDirFileVerified); err != nil { + t.Errorf("Error storing verified data %s %s %s at destinationURI %s, error: %s\n", orgID, objectType, objectID, destinationURIDirFileVerified, err.Error()) + } + if _, err := os.Stat(destinationURIDir + "/test_verified.txt.tmp"); !os.IsNotExist(err) { + t.Errorf("The .tmp file at destinationURI %s should be removed, error: %s\n", destinationURIDir, err.Error()) + } + if _, err := os.Stat(destinationURIDir + "/test_verified.txt"); err != nil { + t.Errorf("Error checking files at destinationURI %s, error: %s\n", destinationURIDirFileVerified, err.Error()) + } + if err = dataVerifier.RemoveTempData(orgID, objectType, objectID, destinationURIDirFileWrong); err != nil { + t.Errorf("Error remove tmp data for %s %s %s at %s, error: %s\n", orgID, objectType, objectID, destinationURIDirFileWrong, err.Error()) + } + +} + +func setupTestVars() { + dataToSign = []byte("dataVerifier test") + wrongDataToSign = []byte("wrong data") + orgID = "testDVOrg" + objectType = "testDVObjType" + objectID = "testDVObjID" +} + +func setupDB(dbType string) string { + if dbType == common.Mongo { + common.Configuration.MongoDbName = "d_test_db" + Store = &storage.MongoStorage{} + } else if dbType == common.Bolt { + dir, _ := os.Getwd() + common.Configuration.PersistenceRootPath = dir + "/persist" + boltStore := &storage.BoltStorage{} + boltStore.Cleanup(true) + Store = boltStore + } else { + fmt.Println("set inmemory storage") + Store = &storage.InMemoryStorage{} + } + + if err := Store.Init(); err != nil { + return fmt.Sprintf("Failed to initialize storage driver. Error: %s\n", err.Error()) + } + return "" +} + +func setupDataURIPath() string { + dir, err := os.Getwd() + if err != nil { + return fmt.Sprintf("Failed to get current directory. Error: %s\n", err.Error()) + } + + destinationURIDir = dir + destinationURI + err = os.MkdirAll(destinationURIDir, 0750) + if err != nil { + return fmt.Sprintf("Failed to initialize dataURI temp folder. Error: %s\n", err.Error()) + } + + destinationURIDirFileVerified = "file:///" + destinationURIDir + "/" + "test_verified.txt" + destinationURIDirFileWrong = "file:///" + destinationURIDir + "/" + "test_wrong.txt" + + return "" +} + +func setupDataSignature(data []byte, hashAlgo string) (string, string, error) { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", "", err + } + publicKey := &privateKey.PublicKey + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", "", err + } + publicKeyString := base64.StdEncoding.EncodeToString(publicKeyBytes) + + var dataHash hash.Hash + var cyrptoHash crypto.Hash + if hashAlgo == common.Sha1 { + dataHash = sha1.New() + cyrptoHash = crypto.SHA1 + } else { + dataHash = sha256.New() + cyrptoHash = crypto.SHA256 + } + + _, err = dataHash.Write(data) + if err != nil { + return "", "", err + } + dataHashSum := dataHash.Sum(nil) + + signature, err := rsa.SignPSS(rand.Reader, privateKey, cyrptoHash, dataHashSum, nil) + if err != nil { + return "", "", err + } + signatureString := base64.StdEncoding.EncodeToString(signature) + return publicKeyString, signatureString, nil +} diff --git a/core/storage/boltStorage.go b/core/storage/boltStorage.go index 69efb32..bcd3f0e 100644 --- a/core/storage/boltStorage.go +++ b/core/storage/boltStorage.go @@ -382,6 +382,9 @@ func (store *BoltStorage) StoreObjectTempData(orgID string, objectType string, o func (store *BoltStorage) RemoveObjectTempData(orgID string, objectType string, objectID string) common.SyncServiceError { tmpDataPath := createDataPathForTempData(store.localDataPath, orgID, objectType, objectID) if err := dataURI.DeleteStoredData(tmpDataPath); err != nil { + if common.IsNotFound(err) { + return nil + } return err } return nil @@ -392,6 +395,9 @@ func (store *BoltStorage) RetrieveTempObjectData(orgID string, objectType string tmpDataPath := createDataPathForTempData(store.localDataPath, orgID, objectType, objectID) dataReader, err := dataURI.GetData(tmpDataPath) if err != nil { + if common.IsNotFound(err) { + return nil, nil + } return nil, err } return dataReader, nil diff --git a/core/storage/mongoStorage.go b/core/storage/mongoStorage.go index aecd5b7..6297484 100644 --- a/core/storage/mongoStorage.go +++ b/core/storage/mongoStorage.go @@ -458,9 +458,7 @@ func (store *MongoStorage) UpdateObjectDeliveryStatus(status string, message str allDeleted = true for i, d := range result.Destinations { if !found && d.Destination.DestType == destType && d.Destination.DestID == destID { - if message != "" || d.Status == common.Error { - d.Message = message - } + d.Message = message if status != "" { d.Status = status } @@ -1034,7 +1032,7 @@ func (store *MongoStorage) StoreObjectTempData(orgID string, objectType string, func (store *MongoStorage) RemoveObjectTempData(orgID string, objectType string, objectID string) common.SyncServiceError { id := createTempObjectCollectionID(orgID, objectType, objectID) - if err := store.removeFile(id); err != nil { + if err := store.removeFile(id); err != nil && err != mgo.ErrNotFound { return err } return nil diff --git a/runCoverage.sh b/runCoverage.sh index f2804fa..3fcaa51 100755 --- a/runCoverage.sh +++ b/runCoverage.sh @@ -10,7 +10,8 @@ echo "mode: set" >> coverage.out PACKAGE_BASE=github.com/open-horizon/edge-sync-service for PKG in ${PACKAGE_BASE}/common ${PACKAGE_BASE}/core/base ${PACKAGE_BASE}/core/communications \ - ${PACKAGE_BASE}/core/dataURI ${PACKAGE_BASE}/core/security ${PACKAGE_BASE}/core/storage; do + ${PACKAGE_BASE}/core/dataURI ${PACKAGE_BASE}/core/security ${PACKAGE_BASE}/core/storage \ + ${PACKAGE_BASE}/core/dataVerifier; do go test -v -cover ${PKG} -coverprofile=coverage.tmp.out rc=$?