Implement permissible file integrity verification
authorFilip Skrzeczkowski <f.skrzeczkow@samsung.com>
Wed, 23 Aug 2023 13:01:00 +0000 (15:01 +0200)
committerTomasz Swierczek <t.swierczek@samsung.com>
Wed, 29 Nov 2023 10:07:23 +0000 (11:07 +0100)
Permissible files have a SHA-1 hash attached at the beginning.
Upon opening it is compared with a new hash calculated from the
file content in order to verify if the file's integrity is intact.
An error is thrown should the hashes differ. Files with no hash are
still supported but reading/updating the rules stored in them
will cause them to automatically switch to the new system.

Change-Id: I5ec379b58cc78e63bcde084ada43273237d61beb

src/client/client-label-monitor.cpp
src/common/include/permissible-set.h
src/common/permissible-set.cpp
src/include/security-manager-types.h
test/CMakeLists.txt
test/test_permissible-set.cpp [new file with mode: 0644]

index 6b40e05211ed97b29756d8d3f647b33322b88a05..195837617549bb739cd50e17caa7eca70b717d62 100644 (file)
@@ -87,6 +87,9 @@ static lib_retcode apply_relabel_list(const std::string &global_label_file,
     } catch (PermissibleSet::PermissibleSetException::FileReadError &e) {
         LogError("Failed to read the configuration files");
         return SECURITY_MANAGER_ERROR_UNKNOWN;
+    } catch (PermissibleSet::PermissibleSetException::FileIntegrityError &e) {
+        LogError("File integrity verification failed - the file may be corrupted");
+        return SECURITY_MANAGER_ERROR_FILE_INTEGRITY;
     }
     return SECURITY_MANAGER_SUCCESS;
 }
index 9c2889ae6e317d676dc1154aaa119218d08e49d5..aaa6d5b00374602a6a5bec06ab20e23f187be8dc 100644 (file)
@@ -48,6 +48,7 @@ public:
     DECLARE_EXCEPTION_TYPE(Base, FileWriteError)
     DECLARE_EXCEPTION_TYPE(Base, FileInitError)
     DECLARE_EXCEPTION_TYPE(Base, FileRemoveError)
+    DECLARE_EXCEPTION_TYPE(Base, FileIntegrityError)
 };
 
 /**
@@ -60,12 +61,32 @@ public:
  */
 std::string getPermissibleFileLocation(uid_t uid, int installationType);
 
+/**
+ * Calculate a SHA-1 hash of given string
+ *
+ * @param[in] content content string to be hashed
+ * @return a 40 char long string that contains the hash encoded in hex
+ */
+std::string calculateHash(const std::string& content);
+
+/**
+ * Hash the file content and compare the result with the hash at the beginning
+ * of the file (if present) in order to detect possible corruption.
+ * @throws FileReadError
+ * @throws FileIntegrityError
+ *
+ * @param[in] nameFile path to the labels file
+ * @param[in] fstream open ifstream from the labe;s file
+ */
+void verifyFileIntegrity(const std::string &nameFile, std::ifstream &fstream);
+
 /**
  * Update permissible file, 1st removing some labels from the list and then,
  * adding new labels to the list (in this particular order).
  * @throws FileLockError
  * @throws FileOpenError
  * @throws FileWriteError
+ * @throws FileIntegrityError
  *
  * @param[in] uid user id
  * @param[in] installationType type of installation (global or local)
@@ -81,6 +102,7 @@ void updatePermissibleFile(uid_t uid, int installationType,
  * @throws FileLockError
  * @throws FileOpenError
  * @throws FileReadError
+ * @throws FileIntegrityError
  *
  * @param[in] nameFile path to the labels file
  * @param[out] appLabels vector to which application labels are added
index cf53100abda7f1a979b1e54e90b3d0029b854162..b8e826ef9845cb1366c00cdd95c83a0cd6c03480 100644 (file)
 
 #include <cstdio>
 #include <cstring>
+#include <filesystem>
 #include <fstream>
 #include <linux/xattr.h>
 #include <memory>
 #include <pwd.h>
+#include <sstream>
 #include <string>
 #include <sys/file.h>
 #include <sys/smack.h>
@@ -52,6 +54,7 @@
 #include <filesystem.h>
 #include <permissible-set.h>
 #include <privilege_db.h>
+#include <openssl/sha.h>
 #include <security-manager-types.h>
 #include <smack-labels.h>
 #include <tzplatform_config.h>
 namespace SecurityManager {
 namespace PermissibleSet {
 
+static const char*  hashMarker = "-SHA-1";
+static const int    hashMarkerLength = strlen(hashMarker);
+static const int    hashLength = 40;
+
 template <typename T>
 static inline int getFd(T &fstream)
 {
@@ -96,6 +103,77 @@ static void markPermissibleFileValid(int fd, const std::string &nameFile, bool v
         LogAndThrowErrno(PermissibleSetException::FileWriteError, "fchmod " << nameFile);
 }
 
+std::string calculateHash(const std::string& content)
+{
+    unsigned char hashBinary[hashLength / 2];
+    SHA1(reinterpret_cast<const unsigned char*>(content.c_str()), content.length(), hashBinary);
+
+    // write the 160 bits of data as 40 hex digits
+    std::stringstream ss;
+    for (int i = 0; i < hashLength / 2; ++i)
+        ss << std::setw(2) << std::setfill('0') << std::hex << static_cast<unsigned int>(hashBinary[i]);
+
+    return ss.str();
+}
+
+void verifyFileIntegrity(const std::string &nameFile, std::ifstream &fstream)
+{
+    auto fileSize = std::filesystem::file_size(nameFile);
+
+    if (0 == fileSize) {
+        // empty file is considered valid
+        return;
+    }
+
+    std::string markerRead;
+    fstream.seekg(0, std::ios::beg);
+    fstream >> markerRead;
+    if (!fstream.good()) {
+        LogAndThrowErrno(PermissibleSetException::FileReadError, "reading hash marker " << nameFile);
+    }
+
+    // check if the file has a hash at all (for backward compatibility)
+    if (markerRead == hashMarker) {
+
+        std::string hashRead;
+        fstream >> hashRead >> std::ws;
+        if (!fstream) {
+            LogAndThrowErrno(PermissibleSetException::FileReadError, "reading hash " << nameFile);
+        }
+
+        if (hashRead.length() != hashLength) {
+            LogAndThrowErrno(PermissibleSetException::FileIntegrityError, "incorrect hash length: " << hashRead << " " << nameFile);
+        }
+
+        if (fstream.eof()) {
+            if (hashRead != calculateHash(""))
+                LogAndThrowErrno(PermissibleSetException::FileIntegrityError, "incorrect hash for empty content: " << hashRead << " " << nameFile);
+            else
+                return;
+        }
+
+        auto labelsStart = fstream.tellg();
+        auto contentSize = fileSize - labelsStart;
+
+        std::string content(contentSize, ' ');
+        fstream.read(&content[0], contentSize);
+        if (!fstream.good()) {
+            LogAndThrowErrno(PermissibleSetException::FileReadError, "read file content, only " << fstream.gcount() << " characters read " << nameFile);
+        }
+
+        // hash the file content and compare it with the hash that has been read
+        auto hashCalculated = calculateHash(content);
+
+        if (hashRead != hashCalculated) {
+            LogAndThrowErrno(PermissibleSetException::FileIntegrityError, "hash does not match. Read:" << hashRead << " Calculated from file " << hashCalculated << " " << nameFile);
+        }
+
+        fstream.seekg(labelsStart, std::ios::beg);
+    } else {
+        fstream.seekg(0, std::ios::beg);
+    }
+}
+
 void updatePermissibleFile(uid_t uid, int installationType,
                            const std::vector<std::string> &labelsToAddForUser,
                            const std::vector<std::string> &labelsToRemoveForUser)
@@ -107,6 +185,8 @@ void updatePermissibleFile(uid_t uid, int installationType,
         try {
             openAndLockNameFile(nameFile, fstream);
 
+            verifyFileIntegrity(nameFile, fstream);
+
             std::string line;
             while (std::getline(fstream, line)) {
                 bool flag = true;
@@ -129,12 +209,19 @@ void updatePermissibleFile(uid_t uid, int installationType,
     openAndLockNameFile(nameFile, fstream);
     markPermissibleFileValid(getFd(fstream), nameFile, false);
 
+    std::stringstream ss;
     for (auto &label : labelsForUser) {
-        fstream << label << '\n';
-        if (fstream.bad())
+        ss << label << '\n';
+    }
+
+    auto content = ss.str();
+    auto hash = calculateHash(content);
+
+    fstream << hashMarker << "\n" << hash << '\n' << content;
+
+    if (fstream.bad())
             LogAndThrowErrno(PermissibleSetException::PermissibleSetException::FileWriteError,
                     "write to file " << nameFile);
-    }
     if (fstream.flush().fail())
         LogAndThrowErrno(PermissibleSetException::FileWriteError, "fflush " << nameFile);
     if (fsync(getFd(fstream)) == -1)
@@ -147,6 +234,8 @@ void readLabelsFromPermissibleFile(const std::string &nameFile, std::vector<std:
     std::ifstream fstream;
     openAndLockNameFile(nameFile, fstream);
 
+    verifyFileIntegrity(nameFile, fstream);
+
     std::string line;
     while (std::getline(fstream, line))
         appLabels.push_back(line);
index bed8b1b073d9ca1c66c4bdcf2939e8425adadb1d..c152546a08ba149e8de62fa3f8417a70fc0d0ef5 100644 (file)
@@ -55,7 +55,8 @@ enum lib_retcode {
     SECURITY_MANAGER_ERROR_FILE_CREATE_FAILED,
     SECURITY_MANAGER_ERROR_FILE_DELETE_FAILED,
     SECURITY_MANAGER_ERROR_MOUNT_ERROR,
-    SECURITY_MANAGER_ERROR_NOT_PATH_OWNER
+    SECURITY_MANAGER_ERROR_NOT_PATH_OWNER,
+    SECURITY_MANAGER_ERROR_FILE_INTEGRITY
 };
 
 /*! \brief accesses types for application installation paths*/
index 592deaff87ef56fbdc7abd5148cb38c78b2aefe0..34029cb6677b47b5c3c6bbdb79d0be18f9bc2a88 100644 (file)
@@ -82,6 +82,7 @@ SET(SM_TESTS_SOURCES
     ${SM_TEST_SRC}/test_log.cpp
     ${SM_TEST_SRC}/test_filesystem.cpp
     ${SM_TEST_SRC}/test_file-lock.cpp
+    ${SM_TEST_SRC}/test_permissible-set.cpp
     ${SM_TEST_SRC}/test_privilege_db_transactions.cpp
     ${SM_TEST_SRC}/test_privilege_db_app_pkg_getters.cpp
     ${SM_TEST_SRC}/test_privilege_db_add_app.cpp
@@ -113,6 +114,7 @@ SET(SM_TESTS_SOURCES
     ${PROJECT_SOURCE_DIR}/src/common/credentials.cpp
     ${PROJECT_SOURCE_DIR}/src/common/filesystem.cpp
     ${PROJECT_SOURCE_DIR}/src/common/file-lock.cpp
+    ${PROJECT_SOURCE_DIR}/src/common/permissible-set.cpp
     ${PROJECT_SOURCE_DIR}/src/common/privilege_db.cpp
     ${PROJECT_SOURCE_DIR}/src/common/service_impl_utils.cpp
     ${PROJECT_SOURCE_DIR}/src/common/smack-accesses.cpp
diff --git a/test/test_permissible-set.cpp b/test/test_permissible-set.cpp
new file mode 100644 (file)
index 0000000..16d86ad
--- /dev/null
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ * This file is licensed under the terms of MIT License or the Apache License
+ * Version 2.0 of your choice. See the LICENSE.MIT file for MIT license details.
+ * See the LICENSE file or the notice below for Apache License Version 2.0
+ * details.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ * @file       test_permissible-set.cpp
+ * @author     Filip Skrzeczkowski (f.skrzeczkow@samsung.com)
+ * @version    1.0
+ */
+#include <boost/test/results_reporter.hpp>
+
+#include <filesystem>
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <dpl/log/log.h>
+#include <permissible-set.h>
+#include <testmacros.h>
+
+using namespace SecurityManager::PermissibleSet;
+
+static constexpr int validHashLength = 40;
+static const std::string hashMarker = "-SHA-1";
+
+class PermissibleFileFixture
+{
+private:
+    void removeFile(const std::string& filename)
+    {
+        BOOST_REQUIRE_NO_THROW(std::filesystem::remove(filename));
+    }
+
+public:
+    inline static const std::string existentFile = "/tmp/SecurityManagerPermissibleFileLockExisting";
+    inline static const std::string nonExistentFile = "/tmp/SecurityManagerPermissibleFileLockNoExisting";
+
+    PermissibleFileFixture()
+    {
+        // create the file that's supposed to exist and delete the other
+        BOOST_REQUIRE_NO_THROW(std::ofstream os(existentFile));
+        removeFile(nonExistentFile);
+    }
+
+    virtual ~PermissibleFileFixture()
+    {
+        removeFile(existentFile);
+        removeFile(nonExistentFile);
+    }
+};
+
+BOOST_AUTO_TEST_SUITE(PERMISSIBLE_SET_TEST)
+
+POSITIVE_TEST_CASE(T1700_calculate_hash_valid_content_length)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    auto hash = calculateHash(content);
+    BOOST_REQUIRE(hash.length() == validHashLength);
+}
+
+POSITIVE_TEST_CASE(T1701_calculate_hash_hex_characters)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    auto hash = calculateHash(content);
+    BOOST_REQUIRE(hash.find_first_not_of("0123456789abcdef") == std::string::npos);
+}
+
+POSITIVE_TEST_CASE(T1702_calculate_hash_deterministic)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    auto hash1 = calculateHash(content);
+    auto hash2 = calculateHash(content);
+    BOOST_REQUIRE(hash1 == hash2);
+}
+
+POSITIVE_TEST_CASE(T1703_calculate_hash_empty_content)
+{
+    std::string content = "";
+    auto hash = calculateHash(content);
+    BOOST_REQUIRE_MESSAGE(hash.length() == validHashLength, "calculated hash: " << hash);
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1710_verify_integrity_valid_hash, PermissibleFileFixture)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    auto hash = calculateHash(content);
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << hash << "\n" << content;
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_NO_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is));
+}
+
+NEGATIVE_FIXTURE_TEST_CASE(T1711_verify_integrity_hash_not_matching, PermissibleFileFixture)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    std::string hash(validHashLength, 'g'); // 'g' is not hex, so this won't ever match
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << hash << "\n" << content;
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is), PermissibleSetException::FileIntegrityError);
+}
+
+NEGATIVE_FIXTURE_TEST_CASE(T1712_verify_integrity_hash_too_short, PermissibleFileFixture)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    std::string hash(validHashLength - 1, '0');
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << hash << "\n" << content;
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is), PermissibleSetException::FileIntegrityError);
+}
+
+NEGATIVE_FIXTURE_TEST_CASE(T1713_verify_integrity_hash_too_long, PermissibleFileFixture)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    std::string hash(validHashLength + 1, '0');
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << hash << "\n" << content;
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is), PermissibleSetException::FileIntegrityError);
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1714_verify_integrity_no_hash, PermissibleFileFixture)
+{
+    std::string content = "Subject_label Object_label Access_type\n";
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << content;
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_NO_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is));
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1715_verify_integrity_empty_file, PermissibleFileFixture)
+{
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_NO_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is));
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1716_verify_integrity_valid_hash_without_content, PermissibleFileFixture)
+{
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << calculateHash("") << "\n";
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_NO_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is));
+}
+
+NEGATIVE_FIXTURE_TEST_CASE(T1717_verify_integrity_hash_not_matching_without_content, PermissibleFileFixture)
+{
+    std::string hash(validHashLength, 'g');
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << hash << "\n";
+    }
+    std::ifstream is(PermissibleFileFixture::existentFile);
+    BOOST_REQUIRE_THROW(verifyFileIntegrity(PermissibleFileFixture::existentFile, is), PermissibleSetException::FileIntegrityError);
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1720_read_valid_file_valid_hash, PermissibleFileFixture)
+{
+    std::vector<std::string> labelsRead;
+    std::vector<std::string> labelsWritten = {
+        "Subject_label1 Object_label1 Access_type1",
+        "Subject_label2 Object_label2 Access_type3",
+        "Subject_label3 Object_label3 Access_type3"
+    };
+
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+
+        std::stringstream ss;
+        for (auto& label : labelsWritten) {
+            ss << label << "\n";
+        }
+
+        auto content = ss.str();
+        auto hash = calculateHash(content);
+
+        BOOST_REQUIRE_NO_THROW(os << hashMarker << "\n" << hash << '\n' << content);
+    }
+    BOOST_REQUIRE_NO_THROW(readLabelsFromPermissibleFile(PermissibleFileFixture::existentFile, labelsRead));
+    BOOST_CHECK_EQUAL_COLLECTIONS(labelsWritten.begin(), labelsWritten.end(), labelsRead.begin(), labelsRead.end());
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1721_read_valid_file_no_hash, PermissibleFileFixture)
+{
+    std::vector<std::string> labelsRead;
+    std::vector<std::string> labelsWritten = {
+        "Subject_label1 Object_label1 Access_type1",
+        "Subject_label2 Object_label2 Access_type3",
+        "Subject_label3 Object_label3 Access_type3"
+    };
+
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        for (auto& label : labelsWritten) {
+            BOOST_REQUIRE_NO_THROW(os << label << "\n");
+        }
+    }
+    BOOST_REQUIRE_NO_THROW(readLabelsFromPermissibleFile(PermissibleFileFixture::existentFile, labelsRead));
+    BOOST_CHECK_EQUAL_COLLECTIONS(labelsWritten.begin(), labelsWritten.end(), labelsRead.begin(), labelsRead.end());
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1722_read_empty_file, PermissibleFileFixture)
+{
+    std::vector<std::string> appLabels;
+    BOOST_REQUIRE_NO_THROW(readLabelsFromPermissibleFile(PermissibleFileFixture::existentFile, appLabels));
+    BOOST_REQUIRE(0 == appLabels.size());
+}
+
+POSITIVE_FIXTURE_TEST_CASE(T1723_read_file_valid_hash_without_content, PermissibleFileFixture)
+{
+    std::vector<std::string> appLabels;
+    {
+        std::ofstream os(PermissibleFileFixture::existentFile);
+        os << hashMarker << " " << calculateHash("") << "\n";
+    }
+    BOOST_REQUIRE_NO_THROW(readLabelsFromPermissibleFile(PermissibleFileFixture::existentFile, appLabels));
+    BOOST_REQUIRE(0 == appLabels.size());
+}
+
+NEGATIVE_FIXTURE_TEST_CASE(T1724_read_non_existent_file, PermissibleFileFixture)
+{
+    std::vector<std::string> appLabels;
+    BOOST_REQUIRE_THROW(readLabelsFromPermissibleFile(PermissibleFileFixture::nonExistentFile, appLabels), PermissibleSetException::FileOpenError);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file