From 345d271e663725d58ce941fc49f08ebc02c32fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Wojdy=C5=82a?= Date: Sun, 17 May 2020 21:33:46 +0200 Subject: [PATCH] [Pal/Linux-SGX] Test malicious modifications to protected files This commit adds a new PF utility `pf_tamper` that tampers with valid protected files and uses this utility to test that the PF logic in Linux-SGX detects such malicious modifications. This commit also moves out the PF-format macros and structs from `protected_files_internal.h` to `protected_files_format.h` for better readability. Co-authored-by: Dmitrii Kuvaiskii --- LibOS/shim/test/fs/test_pf.py | 22 +- .../protected-files/protected_files.c | 16 +- .../protected-files/protected_files_format.h | 139 +++++ .../protected_files_internal.h | 111 +--- Pal/src/host/Linux-SGX/tools/Makefile | 1 + .../host/Linux-SGX/tools/pf_tamper/.gitignore | 1 + .../host/Linux-SGX/tools/pf_tamper/Makefile | 31 ++ .../Linux-SGX/tools/pf_tamper/pf_tamper.c | 476 ++++++++++++++++++ 8 files changed, 658 insertions(+), 139 deletions(-) create mode 100644 Pal/src/host/Linux-SGX/protected-files/protected_files_format.h create mode 100644 Pal/src/host/Linux-SGX/tools/pf_tamper/.gitignore create mode 100644 Pal/src/host/Linux-SGX/tools/pf_tamper/Makefile create mode 100644 Pal/src/host/Linux-SGX/tools/pf_tamper/pf_tamper.c diff --git a/LibOS/shim/test/fs/test_pf.py b/LibOS/shim/test/fs/test_pf.py index e430e02a..7b1621d7 100644 --- a/LibOS/shim/test/fs/test_pf.py +++ b/LibOS/shim/test/fs/test_pf.py @@ -193,24 +193,20 @@ class TC_50_ProtectedFiles(TC_00_FileSystem): def __corrupt_file(self, input_path, output_path): cmd = [self.PF_TAMPER, '-w', self.WRAP_KEY, '-i', input_path, '-o', output_path] - return self.run_native_binary(cmd) + return self.run_native_binary(cmd, libpath=os.path.join(os.getcwd(), 'lib')) # invalid/corrupted files - @expectedFailureIf(HAS_SGX) - # pylint: disable=fixme def test_500_invalid(self): - # TODO: port these to the new file format invalid_dir = os.path.join(self.TEST_DIR, 'pf_invalid') - # files below should work normally (benign modifications) - should_pass = ['chunk_padding_1_fixed', 'chunk_padding_2_fixed', 'chunk_data_3', - 'chunk_data_3_fixed', 'chunk_data_4', 'chunk_data_4_fixed'] if not os.path.exists(invalid_dir): os.mkdir(invalid_dir) + # prepare valid encrypted file (largest one for maximum possible corruptions) original_input = self.OUTPUT_FILES[-1] self.__encrypt_file(self.INPUT_FILES[-1], original_input) # generate invalid files based on the above self.__corrupt_file(original_input, invalid_dir) + # try to decrypt invalid files for name in os.listdir(invalid_dir): invalid = os.path.join(invalid_dir, name) @@ -218,17 +214,13 @@ class TC_50_ProtectedFiles(TC_00_FileSystem): input_path = os.path.join(invalid_dir, os.path.basename(original_input)) # copy the file so it has the original file name (for allowed path check) shutil.copy(invalid, input_path) - should_pass = any(s in name for s in should_pass) try: args = ['decrypt', '-V', '-w', self.WRAP_KEY, '-i', input_path, '-o', output_path] self.__pf_crypt(args) except subprocess.CalledProcessError as exc: - if should_pass: - self.assertEqual(exc.returncode, 0) - else: - self.assertNotEqual(exc.returncode, 0) + # decryption of invalid file must fail with -1 (wrapped to 255) + self.assertEqual(exc.returncode, 255) else: - if not should_pass: - print('[!] Fail: successfully decrypted file: ' + name) - self.fail() + print('[!] Fail: successfully decrypted file: ' + name) + self.fail() diff --git a/Pal/src/host/Linux-SGX/protected-files/protected_files.c b/Pal/src/host/Linux-SGX/protected-files/protected_files.c index 4f0c59ba..d020a1e2 100644 --- a/Pal/src/host/Linux-SGX/protected-files/protected_files.c +++ b/Pal/src/host/Linux-SGX/protected-files/protected_files.c @@ -5,6 +5,8 @@ */ #include "api.h" +#include "protected_files.h" +#include "protected_files_format.h" #include "protected_files_internal.h" #ifndef IN_PAL @@ -65,20 +67,6 @@ static pf_random_f g_cb_random = NULL; static pf_iv_t g_empty_iv = {0}; static bool g_initialized = false; -#define METADATA_KEY_NAME "SGX-PROTECTED-FS-METADATA-KEY" -#define MAX_LABEL_SIZE 64 - -static_assert(sizeof(METADATA_KEY_NAME) <= MAX_LABEL_SIZE, "label too long"); - -#pragma pack(push, 1) -typedef struct { - uint32_t index; - char label[MAX_LABEL_SIZE]; // must be NULL terminated - pf_keyid_t nonce; - uint32_t output_len; // in bits -} kdf_input_t; -#pragma pack(pop) - // The key derivation function follow recommendations from NIST Special Publication 800-108: // Recommendation for Key Derivation Using Pseudorandom Functions // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-108.pdf diff --git a/Pal/src/host/Linux-SGX/protected-files/protected_files_format.h b/Pal/src/host/Linux-SGX/protected-files/protected_files_format.h new file mode 100644 index 00000000..2394e53e --- /dev/null +++ b/Pal/src/host/Linux-SGX/protected-files/protected_files_format.h @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2019-2020 Invisible Things Lab + * Rafal Wojdyla + * Copyright (C) 2011-2020 Intel Corporation + */ + +#ifndef PROTECTED_FILES_FORMAT_H_ +#define PROTECTED_FILES_FORMAT_H_ + +#include + +#include "assert.h" +#include "list.h" +#include "protected_files.h" + +#define PF_FILE_ID 0x46505f4850415247 /* GRAPH_PF */ +#define PF_MAJOR_VERSION 0x01 +#define PF_MINOR_VERSION 0x00 + +#define METADATA_KEY_NAME "SGX-PROTECTED-FS-METADATA-KEY" +#define MAX_LABEL_SIZE 64 + +static_assert(sizeof(METADATA_KEY_NAME) <= MAX_LABEL_SIZE, "label too long"); + +#pragma pack(push, 1) + +typedef struct _metadata_plain { + uint64_t file_id; + uint8_t major_version; + uint8_t minor_version; + pf_keyid_t metadata_key_id; + pf_mac_t metadata_gmac; /* GCM mac */ +} metadata_plain_t; + +#define PATH_MAX_SIZE (260 + 512) + +// these are all defined as relative to node size, so we can decrease node size in tests +// and have deeper tree +#define MD_USER_DATA_SIZE (PF_NODE_SIZE * 3 / 4) // 3072 +static_assert(MD_USER_DATA_SIZE == 3072, "bad struct size"); + +typedef struct _metadata_encrypted { + char path[PATH_MAX_SIZE]; + uint64_t size; + pf_key_t mht_key; + pf_mac_t mht_gmac; + uint8_t data[MD_USER_DATA_SIZE]; +} metadata_encrypted_t; + +typedef uint8_t metadata_encrypted_blob_t[sizeof(metadata_encrypted_t)]; + +#define METADATA_NODE_SIZE PF_NODE_SIZE + +typedef uint8_t metadata_padding_t[METADATA_NODE_SIZE - + (sizeof(metadata_plain_t) + sizeof(metadata_encrypted_blob_t))]; + +typedef struct _metadata_node { + metadata_plain_t plain_part; + metadata_encrypted_blob_t encrypted_part; + metadata_padding_t padding; +} metadata_node_t; + +static_assert(sizeof(metadata_node_t) == PF_NODE_SIZE, "sizeof(metadata_node_t)"); + +typedef struct _data_node_crypto { + pf_key_t key; + pf_mac_t gmac; +} gcm_crypto_data_t; + +// for PF_NODE_SIZE == 4096, we have 96 attached data nodes and 32 mht child nodes +// for PF_NODE_SIZE == 2048, we have 48 attached data nodes and 16 mht child nodes +// for PF_NODE_SIZE == 1024, we have 24 attached data nodes and 8 mht child nodes +// 3/4 of the node size is dedicated to data nodes +#define ATTACHED_DATA_NODES_COUNT ((PF_NODE_SIZE / sizeof(gcm_crypto_data_t)) * 3 / 4) +static_assert(ATTACHED_DATA_NODES_COUNT == 96, "ATTACHED_DATA_NODES_COUNT"); +// 1/4 of the node size is dedicated to child mht nodes +#define CHILD_MHT_NODES_COUNT ((PF_NODE_SIZE / sizeof(gcm_crypto_data_t)) * 1 / 4) +static_assert(CHILD_MHT_NODES_COUNT == 32, "CHILD_MHT_NODES_COUNT"); + +typedef struct _mht_node { + gcm_crypto_data_t data_nodes_crypto[ATTACHED_DATA_NODES_COUNT]; + gcm_crypto_data_t mht_nodes_crypto[CHILD_MHT_NODES_COUNT]; +} mht_node_t; + +static_assert(sizeof(mht_node_t) == PF_NODE_SIZE, "sizeof(mht_node_t)"); + +typedef struct _data_node { + uint8_t data[PF_NODE_SIZE]; +} data_node_t; + +static_assert(sizeof(data_node_t) == PF_NODE_SIZE, "sizeof(data_node_t)"); + +typedef struct _encrypted_node { + uint8_t cipher[PF_NODE_SIZE]; +} encrypted_node_t; + +static_assert(sizeof(encrypted_node_t) == PF_NODE_SIZE, "sizeof(encrypted_node_t)"); + +#define MAX_PAGES_IN_CACHE 48 + +typedef enum { + FILE_MHT_NODE_TYPE = 1, + FILE_DATA_NODE_TYPE = 2, +} mht_node_type_e; + +// make sure these are the same size +static_assert(sizeof(mht_node_t) == sizeof(data_node_t), + "sizeof(mht_node_t) == sizeof(data_node_t)"); + +DEFINE_LIST(_file_node); +typedef struct _file_node { + LIST_TYPE(_file_node) list; + uint8_t type; + uint64_t node_number; + struct _file_node* parent; + bool need_writing; + bool new_node; + struct { + uint64_t physical_node_number; + encrypted_node_t encrypted; // the actual data from the disk + }; + union { // decrypted data + mht_node_t mht; + data_node_t data; + } decrypted; +} file_node_t; +DEFINE_LISTP(_file_node); + +typedef struct { + uint32_t index; + char label[MAX_LABEL_SIZE]; // must be NULL terminated + pf_keyid_t nonce; + uint32_t output_len; // in bits +} kdf_input_t; + +#pragma pack(pop) + +#endif /* PROTECTED_FILES_FORMAT_H_ */ + diff --git a/Pal/src/host/Linux-SGX/protected-files/protected_files_internal.h b/Pal/src/host/Linux-SGX/protected-files/protected_files_internal.h index 4a31261d..c06d1ecf 100644 --- a/Pal/src/host/Linux-SGX/protected-files/protected_files_internal.h +++ b/Pal/src/host/Linux-SGX/protected-files/protected_files_internal.h @@ -13,116 +13,7 @@ #include "list.h" #include "lru_cache.h" #include "protected_files.h" - -#define PF_FILE_ID 0x46505f4850415247 /* GRAPH_PF */ -#define PF_MAJOR_VERSION 0x01 -#define PF_MINOR_VERSION 0x00 - -#pragma pack(push, 1) - -typedef struct _metadata_plain { - uint64_t file_id; - uint8_t major_version; - uint8_t minor_version; - pf_keyid_t metadata_key_id; - pf_mac_t metadata_gmac; /* GCM mac */ -} metadata_plain_t; - -#define PATH_MAX_SIZE (260 + 512) - -// these are all defined as relative to node size, so we can decrease node size in tests -// and have deeper tree -#define MD_USER_DATA_SIZE (PF_NODE_SIZE * 3 / 4) // 3072 -static_assert(MD_USER_DATA_SIZE == 3072, "bad struct size"); - -typedef struct _metadata_encrypted { - char path[PATH_MAX_SIZE]; - uint64_t size; - pf_key_t mht_key; - pf_mac_t mht_gmac; - uint8_t data[MD_USER_DATA_SIZE]; -} metadata_encrypted_t; - -typedef uint8_t metadata_encrypted_blob_t[sizeof(metadata_encrypted_t)]; - -#define METADATA_NODE_SIZE PF_NODE_SIZE - -typedef uint8_t metadata_padding_t[METADATA_NODE_SIZE - - (sizeof(metadata_plain_t) + sizeof(metadata_encrypted_blob_t))]; - -typedef struct _metadata_node { - metadata_plain_t plain_part; - metadata_encrypted_blob_t encrypted_part; - metadata_padding_t padding; -} metadata_node_t; - -static_assert(sizeof(metadata_node_t) == PF_NODE_SIZE, "sizeof(metadata_node_t)"); - -typedef struct _data_node_crypto { - pf_key_t key; - pf_mac_t gmac; -} gcm_crypto_data_t; - -// for PF_NODE_SIZE == 4096, we have 96 attached data nodes and 32 mht child nodes -// for PF_NODE_SIZE == 2048, we have 48 attached data nodes and 16 mht child nodes -// for PF_NODE_SIZE == 1024, we have 24 attached data nodes and 8 mht child nodes -// 3/4 of the node size is dedicated to data nodes -#define ATTACHED_DATA_NODES_COUNT ((PF_NODE_SIZE / sizeof(gcm_crypto_data_t)) * 3 / 4) -static_assert(ATTACHED_DATA_NODES_COUNT == 96, "ATTACHED_DATA_NODES_COUNT"); -// 1/4 of the node size is dedicated to child mht nodes -#define CHILD_MHT_NODES_COUNT ((PF_NODE_SIZE / sizeof(gcm_crypto_data_t)) * 1 / 4) -static_assert(CHILD_MHT_NODES_COUNT == 32, "CHILD_MHT_NODES_COUNT"); - -typedef struct _mht_node { - gcm_crypto_data_t data_nodes_crypto[ATTACHED_DATA_NODES_COUNT]; - gcm_crypto_data_t mht_nodes_crypto[CHILD_MHT_NODES_COUNT]; -} mht_node_t; - -static_assert(sizeof(mht_node_t) == PF_NODE_SIZE, "sizeof(mht_node_t)"); - -typedef struct _data_node { - uint8_t data[PF_NODE_SIZE]; -} data_node_t; - -static_assert(sizeof(data_node_t) == PF_NODE_SIZE, "sizeof(data_node_t)"); - -typedef struct _encrypted_node { - uint8_t cipher[PF_NODE_SIZE]; -} encrypted_node_t; - -static_assert(sizeof(encrypted_node_t) == PF_NODE_SIZE, "sizeof(encrypted_node_t)"); - -#define MAX_PAGES_IN_CACHE 48 - -typedef enum { - FILE_MHT_NODE_TYPE = 1, - FILE_DATA_NODE_TYPE = 2, -} mht_node_type_e; - -// make sure these are the same size -static_assert(sizeof(mht_node_t) == sizeof(data_node_t), - "sizeof(mht_node_t) == sizeof(data_node_t)"); - -DEFINE_LIST(_file_node); -typedef struct _file_node { - LIST_TYPE(_file_node) list; - uint8_t type; - uint64_t node_number; - struct _file_node* parent; - bool need_writing; - bool new_node; - struct { - uint64_t physical_node_number; - encrypted_node_t encrypted; // the actual data from the disk - }; - union { // decrypted data - mht_node_t mht; - data_node_t data; - } decrypted; -} file_node_t; -DEFINE_LISTP(_file_node); - -#pragma pack(pop) +#include "protected_files_format.h" struct pf_context { metadata_node_t file_metadata; // actual data from disk's meta data node diff --git a/Pal/src/host/Linux-SGX/tools/Makefile b/Pal/src/host/Linux-SGX/tools/Makefile index a5903ad9..87bc7812 100644 --- a/Pal/src/host/Linux-SGX/tools/Makefile +++ b/Pal/src/host/Linux-SGX/tools/Makefile @@ -12,3 +12,4 @@ $(targets): $(MAKE) -C verify-ias-report $@ $(MAKE) -C ra-tls $@ $(MAKE) -C pf_crypt $@ + $(MAKE) -C pf_tamper $@ diff --git a/Pal/src/host/Linux-SGX/tools/pf_tamper/.gitignore b/Pal/src/host/Linux-SGX/tools/pf_tamper/.gitignore new file mode 100644 index 00000000..a52536d8 --- /dev/null +++ b/Pal/src/host/Linux-SGX/tools/pf_tamper/.gitignore @@ -0,0 +1 @@ +/pf_tamper diff --git a/Pal/src/host/Linux-SGX/tools/pf_tamper/Makefile b/Pal/src/host/Linux-SGX/tools/pf_tamper/Makefile new file mode 100644 index 00000000..94c826e4 --- /dev/null +++ b/Pal/src/host/Linux-SGX/tools/pf_tamper/Makefile @@ -0,0 +1,31 @@ +include ../../../../../../Scripts/Makefile.configs +include ../../../../../../Scripts/Makefile.rules + +CFLAGS += -I../.. \ + -I../common \ + -I../../protected-files \ + -I../../../../../include/lib \ + -D_GNU_SOURCE + +LDLIBS += -L../common \ + -L../../../../../lib/crypto/mbedtls/install/lib \ + -lsgx_util -lmbedcrypto + +PREFIX ?= /usr/local + +pf_tamper: pf_tamper.o + $(call cmd,csingle) + +.PHONY: all +all: pf_tamper + +.PHONY: install +install: + install -D pf_tamper -t ${PREFIX}/bin + +.PHONY: clean +clean: + $(RM) *.o pf_tamper + +.PHONY: distclean +distclean: clean diff --git a/Pal/src/host/Linux-SGX/tools/pf_tamper/pf_tamper.c b/Pal/src/host/Linux-SGX/tools/pf_tamper/pf_tamper.c new file mode 100644 index 00000000..4d39a0d5 --- /dev/null +++ b/Pal/src/host/Linux-SGX/tools/pf_tamper/pf_tamper.c @@ -0,0 +1,476 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later */ +/* Copyright (C) 2019-2020 Invisible Things Lab + * Rafal Wojdyla + */ + +#include +#include +#include +#include +#include + +#include "pf_util.h" +#include "protected_files.h" +#include "protected_files_format.h" +#include "util.h" + +/* Tamper with a PF in various ways for testing purposes. The PF is assumed to be valid and have at + * least enough data to contain two MHT nodes. */ + +/* Command line options */ +struct option g_options[] = { + { "input", required_argument, 0, 'i' }, + { "output", required_argument, 0, 'o' }, + { "wrap-key", required_argument, 0, 'w' }, + { "verbose", no_argument, 0, 'v' }, + { "help", no_argument, 0, 'h' }, + { 0, 0, 0, 0 } +}; + +static void usage(void) { + INFO("\nUsage: pf_tamper [options]\n"); + INFO("\nAvailable options:\n"); + INFO(" --help, -h Display this help\n"); + INFO(" --verbose, -v Enable verbose output\n"); + INFO(" --wrap-key, -w PATH Path to wrap key file\n"); + INFO(" --input, -i PATH Source file to be tampered with (must be a valid PF)\n"); + INFO(" --output, -o PATH Directory where modified files will be written to\n"); +} + +#define FATAL(fmt, ...) do { \ + ERROR(fmt, ##__VA_ARGS__); \ + exit(-1); \ +} while (0) + +ssize_t g_input_size = 0; +char* g_input_name = NULL; +void* g_input_data = MAP_FAILED; +char* g_output_dir = NULL; +char* g_output_path = NULL; +size_t g_output_path_size = 0; +pf_key_t g_wrap_key; +pf_key_t g_meta_key; + +static pf_iv_t g_empty_iv = {0}; + +static void derive_main_key(const pf_key_t* kdk, const pf_keyid_t* key_id, pf_key_t* out_key) { + kdf_input_t buf = {0}; + pf_status_t status; + + buf.index = 1; + strncpy(buf.label, METADATA_KEY_NAME, MAX_LABEL_SIZE); + memcpy(&buf.nonce, key_id, sizeof(buf.nonce)); + buf.output_len = 0x80; + + status = mbedtls_aes_gcm_encrypt(kdk, &g_empty_iv, &buf, sizeof(buf), NULL, 0, NULL, out_key); + if (PF_FAILURE(status)) + FATAL("key derivation failed\n"); +} + +static void make_output_path(const char* suffix) { + snprintf(g_output_path, g_output_path_size, "%s/%s.%s", g_output_dir, g_input_name, suffix); + INFO("[*] %s\n", g_output_path); +} + +/* PF layout (node size is PF_NODE_SIZE): + * - Node 0: metadata (metadata_node_t) + * - metadata_plain_t + * - metadata_encrypted_t (may include MD_USER_DATA_SIZE bytes of data) + * - metadata_padding_t + * - Node 1: MHT (mht_node_t) + * - Node 2-97: data (ATTACHED_DATA_NODES_COUNT == 96) + * - Node 98: MHT + * - Node 99-195: data + * - ... + */ +static void truncate_file(const char* suffix, size_t output_size) { + int ret; + + make_output_path(suffix); + + if (output_size < g_input_size) { + ret = write_file(g_output_path, output_size, g_input_data); + } else { + ret = write_file(g_output_path, g_input_size, g_input_data); + if (ret < 0) + goto out; + ret = truncate(g_output_path, output_size); + } +out: + if (ret < 0) + FATAL("truncate_file failed: %d\n", ret); +} + +#define FIELD_SIZEOF(t, f) (sizeof(((t*)0)->f)) +#define FIELD_TRUNCATED(t, f) (offsetof(t, f) + (FIELD_SIZEOF(t, f) / 2)) +#define DATA_CRYPTO_SIZE (FIELD_SIZEOF(mht_node_t, data_nodes_crypto)) + +static void tamper_truncate(void) { + size_t mdps = sizeof(metadata_plain_t); + DBG("size(metadata_plain_t) = 0x%04lx\n", sizeof(metadata_plain_t)); + DBG("metadata_plain_t.file_id : 0x%04lx (0x%04lx)\n", + offsetof(metadata_plain_t, file_id), FIELD_SIZEOF(metadata_plain_t, file_id)); + DBG("metadata_plain_t.major_version : 0x%04lx (0x%04lx)\n", + offsetof(metadata_plain_t, major_version), FIELD_SIZEOF(metadata_plain_t, major_version)); + DBG("metadata_plain_t.minor_version : 0x%04lx (0x%04lx)\n", + offsetof(metadata_plain_t, minor_version), FIELD_SIZEOF(metadata_plain_t, minor_version)); + DBG("metadata_plain_t.metadata_key_id : 0x%04lx (0x%04lx)\n", + offsetof(metadata_plain_t, metadata_key_id), + FIELD_SIZEOF(metadata_plain_t, metadata_key_id)); + DBG("metadata_plain_t.metadata_gmac : 0x%04lx (0x%04lx)\n", + offsetof(metadata_plain_t, metadata_gmac), + FIELD_SIZEOF(metadata_plain_t, metadata_gmac)); + + DBG("size(metadata_encrypted_t) = 0x%04lx\n", sizeof(metadata_encrypted_t)); + DBG("metadata_encrypted_t.path : 0x%04lx (0x%04lx)\n", + mdps + offsetof(metadata_encrypted_t, path), + FIELD_SIZEOF(metadata_encrypted_t, path)); + DBG("metadata_encrypted_t.size : 0x%04lx (0x%04lx)\n", + mdps + offsetof(metadata_encrypted_t, size), FIELD_SIZEOF(metadata_encrypted_t, size)); + DBG("metadata_encrypted_t.mht_key : 0x%04lx (0x%04lx)\n", + mdps + offsetof(metadata_encrypted_t, mht_key), + FIELD_SIZEOF(metadata_encrypted_t, mht_key)); + DBG("metadata_encrypted_t.mht_gmac : 0x%04lx (0x%04lx)\n", + mdps + offsetof(metadata_encrypted_t, mht_gmac), + FIELD_SIZEOF(metadata_encrypted_t, mht_gmac)); + DBG("metadata_encrypted_t.data : 0x%04lx (0x%04lx)\n", + mdps + offsetof(metadata_encrypted_t, data), FIELD_SIZEOF(metadata_encrypted_t, data)); + + DBG("size(metadata_padding_t) = 0x%04lx\n", sizeof(metadata_padding_t)); + DBG("metadata_padding_t : 0x%04lx (0x%04lx)\n", + mdps + sizeof(metadata_encrypted_t), sizeof(metadata_padding_t)); + + /* node 0: metadata + 3k of user data */ + /* plain metadata */ + truncate_file("trunc_meta_plain_0", 0); + truncate_file("trunc_meta_plain_1", FIELD_TRUNCATED(metadata_plain_t, file_id)); + truncate_file("trunc_meta_plain_2", offsetof(metadata_plain_t, major_version)); + truncate_file("trunc_meta_plain_3", offsetof(metadata_plain_t, minor_version)); + truncate_file("trunc_meta_plain_4", offsetof(metadata_plain_t, metadata_key_id)); + truncate_file("trunc_meta_plain_5", FIELD_TRUNCATED(metadata_plain_t, metadata_key_id)); + truncate_file("trunc_meta_plain_6", offsetof(metadata_plain_t, metadata_gmac)); + truncate_file("trunc_meta_plain_7", FIELD_TRUNCATED(metadata_plain_t, metadata_gmac)); + + /* encrypted metadata */ + truncate_file("trunc_meta_enc_0", mdps + offsetof(metadata_encrypted_t, path)); + truncate_file("trunc_meta_enc_1", mdps + FIELD_TRUNCATED(metadata_encrypted_t, path)); + truncate_file("trunc_meta_enc_2", mdps + offsetof(metadata_encrypted_t, size)); + truncate_file("trunc_meta_enc_3", mdps + FIELD_TRUNCATED(metadata_encrypted_t, size)); + truncate_file("trunc_meta_enc_4", mdps + offsetof(metadata_encrypted_t, mht_key)); + truncate_file("trunc_meta_enc_5", mdps + FIELD_TRUNCATED(metadata_encrypted_t, mht_key)); + truncate_file("trunc_meta_enc_6", mdps + offsetof(metadata_encrypted_t, mht_gmac)); + truncate_file("trunc_meta_enc_7", mdps + FIELD_TRUNCATED(metadata_encrypted_t, mht_gmac)); + truncate_file("trunc_meta_enc_8", mdps + offsetof(metadata_encrypted_t, data)); + truncate_file("trunc_meta_enc_9", mdps + FIELD_TRUNCATED(metadata_encrypted_t, data)); + + /* padding */ + truncate_file("trunc_meta_pad_0", mdps + sizeof(metadata_encrypted_t)); + truncate_file("trunc_meta_pad_1", mdps + sizeof(metadata_encrypted_t) + + sizeof(metadata_padding_t) / 2); + + /* node 1: mht root */ + /* after node 0 */ + truncate_file("trunc_mht_0", PF_NODE_SIZE); + /* middle of data_nodes_crypto[0].key */ + truncate_file("trunc_mht_1", PF_NODE_SIZE + PF_KEY_SIZE / 2); + /* after data_nodes_crypto[0].key */ + truncate_file("trunc_mht_2", PF_NODE_SIZE + PF_KEY_SIZE); + /* middle of data_nodes_crypto[0].gmac */ + truncate_file("trunc_mht_3", PF_NODE_SIZE + PF_KEY_SIZE + PF_MAC_SIZE / 2); + /* after data_nodes_crypto[0].gmac */ + truncate_file("trunc_mht_4", PF_NODE_SIZE + PF_KEY_SIZE + PF_MAC_SIZE); + /* after data_nodes_crypto */ + truncate_file("trunc_mht_5", PF_NODE_SIZE + DATA_CRYPTO_SIZE); + /* middle of mht_nodes_crypto[0].key */ + truncate_file("trunc_mht_6", PF_NODE_SIZE + DATA_CRYPTO_SIZE + PF_KEY_SIZE / 2); + /* after mht_nodes_crypto[0].key */ + truncate_file("trunc_mht_7", PF_NODE_SIZE + DATA_CRYPTO_SIZE + PF_KEY_SIZE); + /* middle of mht_nodes_crypto[0].gmac */ + truncate_file("trunc_mht_8", PF_NODE_SIZE + DATA_CRYPTO_SIZE + PF_KEY_SIZE + PF_MAC_SIZE / 2); + /* after mht_nodes_crypto[0].gmac */ + truncate_file("trunc_mht_9", PF_NODE_SIZE + DATA_CRYPTO_SIZE + PF_KEY_SIZE + PF_MAC_SIZE); + + /* node 2-3: data #0, #1 */ + /* after mht root */ + truncate_file("trunc_data_0", 2 * PF_NODE_SIZE); + /* middle of data #0 */ + truncate_file("trunc_data_1", 2 * PF_NODE_SIZE + PF_NODE_SIZE / 2); + /* after data #0 */ + truncate_file("trunc_data_2", 3 * PF_NODE_SIZE); + /* middle of data #1 */ + truncate_file("trunc_data_3", 3 * PF_NODE_SIZE + PF_NODE_SIZE / 2); + + /* extend */ + truncate_file("extend_0", g_input_size + 1); + truncate_file("extend_1", g_input_size + PF_NODE_SIZE / 2); + truncate_file("extend_2", g_input_size + PF_NODE_SIZE); + truncate_file("extend_3", g_input_size + PF_NODE_SIZE + PF_NODE_SIZE / 2); +} + +/* returns mmap'd output contents */ +static void* create_output(const char* path) { + void* mem = MAP_FAILED; + int fd = open(path, O_RDWR|O_CREAT, 0664); + if (fd < 0) + FATAL("Failed to open output file '%s': %s\n", path, strerror(errno)); + + if (ftruncate(fd, g_input_size) < 0) + FATAL("Failed to ftruncate output file '%s': %s\n", path, strerror(errno)); + + mem = mmap(NULL, g_input_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + FATAL("Failed to mmap output file '%s': %s\n", path, strerror(errno)); + + memcpy(mem, g_input_data, g_input_size); + + close(fd); + return mem; +} + +static void pf_decrypt(const void* encrypted, size_t size, const pf_key_t* key, const pf_mac_t* mac, + void* decrypted, const char* msg) { + pf_status_t status = mbedtls_aes_gcm_decrypt(key, &g_empty_iv, NULL, 0, + encrypted, size, + decrypted, mac); + if (PF_FAILURE(status)) + FATAL("decrypting %s failed\n", msg); +} + +static void pf_encrypt(const void* decrypted, size_t size, const pf_key_t* key, pf_mac_t* mac, + void* encrypted, const char* msg) { + pf_status_t status = mbedtls_aes_gcm_encrypt(key, &g_empty_iv, NULL, 0, + decrypted, size, + encrypted, mac); + if (PF_FAILURE(status)) + FATAL("encrypting %s failed\n", msg); +} + +/* copy input PF and apply some modifications */ +#define __BREAK_PF(suffix, ...) do { \ + make_output_path(suffix); \ + meta = create_output(g_output_path); \ + out = (uint8_t*)meta; \ + pf_decrypt(&meta->encrypted_part, sizeof(meta->encrypted_part), &g_meta_key, \ + &meta->plain_part.metadata_gmac, meta_dec, "metadata"); \ + mht_enc = (mht_node_t*)(out + PF_NODE_SIZE); \ + pf_decrypt(mht_enc, sizeof(*mht_enc), &meta_dec->mht_key, &meta_dec->mht_gmac, mht_dec, \ + "mht"); \ + __VA_ARGS__ \ + munmap(meta, g_input_size); \ +} while (0) + +/* if update is true, also create a file with correct metadata MAC */ +#define BREAK_PF(suffix, update, ...) do { \ + __BREAK_PF(suffix, __VA_ARGS__); \ + if (update) { \ + __BREAK_PF(suffix "_fixed", __VA_ARGS__ { \ + pf_encrypt(meta_dec, sizeof(*meta_dec), &g_meta_key, \ + &meta->plain_part.metadata_gmac, meta->encrypted_part, \ + "metadata"); \ + } ); \ + } \ +} while (0) + +#define BREAK_MHT(suffix, ...) do { \ + __BREAK_PF(suffix, __VA_ARGS__ { \ + pf_encrypt(mht_dec, sizeof(*mht_dec), &meta_dec->mht_key, &meta_dec->mht_gmac, \ + mht_enc, "mht"); \ + } ); \ +} while (0) + +#define LAST_BYTE(array) (((uint8_t*)&array)[sizeof(array) - 1]) + +static void tamper_modify(void) { + metadata_node_t* meta = NULL; + uint8_t* out = NULL; + metadata_encrypted_t* meta_dec = malloc(sizeof(*meta_dec)); + if (!meta_dec) + FATAL("Out of memory\n"); + mht_node_t* mht_enc = NULL; + mht_node_t* mht_dec = malloc(sizeof(*mht_dec)); + if (!mht_dec) + FATAL("Out of memory\n"); + + /* plain part of the metadata isn't covered by the MAC so no point updating it */ + BREAK_PF("meta_plain_id_0", /*update=*/false, + { meta->plain_part.file_id = 0; }); + BREAK_PF("meta_plain_id_1", /*update=*/false, + { meta->plain_part.file_id = UINT64_MAX; }); + BREAK_PF("meta_plain_version_0", /*update=*/false, + { meta->plain_part.major_version = 0; }); + BREAK_PF("meta_plain_version_1", /*update=*/false, + { meta->plain_part.major_version = 0xff; }); + BREAK_PF("meta_plain_version_2", /*update=*/false, + { meta->plain_part.minor_version = 0xff; }); + + /* metadata_key_id is the keying material for encrypted metadata key derivation, so create also + * PFs with updated MACs */ + BREAK_PF("meta_plain_keyid_0", /*update=*/true, + { meta->plain_part.metadata_key_id[0] ^= 1; }); + BREAK_PF("meta_plain_keyid_1", /*update=*/true, + { LAST_BYTE(meta->plain_part.metadata_key_id) ^= 0xfe; }); + BREAK_PF("meta_plain_mac_0", /*update=*/true, + { meta->plain_part.metadata_gmac[0] ^= 0xfe; }); + BREAK_PF("meta_plain_mac_1", /*update=*/true, + { LAST_BYTE(meta->plain_part.metadata_gmac) &= 1; }); + + BREAK_PF("meta_enc_filename_0", /*update=*/true, + { meta_dec->path[0] = 0; }); + BREAK_PF("meta_enc_filename_1", /*update=*/true, + { meta_dec->path[0] ^= 1; }); + BREAK_PF("meta_enc_filename_2", /*update=*/true, + { LAST_BYTE(meta_dec->path) ^= 0xfe; }); + BREAK_PF("meta_enc_size_0", /*update=*/true, + { meta_dec->size = 0; }); + BREAK_PF("meta_enc_size_1", /*update=*/true, + { meta_dec->size = g_input_size - 1; }); + BREAK_PF("meta_enc_size_2", /*update=*/true, + { meta_dec->size = g_input_size + 1; }); + BREAK_PF("meta_enc_size_3", /*update=*/true, + { meta_dec->size = UINT64_MAX; }); + BREAK_PF("meta_enc_mht_key_0", /*update=*/true, + { meta_dec->mht_key[0] ^= 1; }); + BREAK_PF("meta_enc_mht_key_1", /*update=*/true, + { LAST_BYTE(meta_dec->mht_key) ^= 0xfe; }); + BREAK_PF("meta_enc_mht_mac_0", /*update=*/true, + { meta_dec->mht_gmac[0] ^= 1; }); + BREAK_PF("meta_enc_mht_mac_1", /*update=*/true, + { LAST_BYTE(meta_dec->mht_gmac) ^= 0xfe; }); + BREAK_PF("meta_enc_data_0", /*update=*/true, + { meta_dec->data[0] ^= 0xfe; }); + BREAK_PF("meta_enc_data_1", /*update=*/true, + { LAST_BYTE(meta_dec->data) ^= 1; }); + + /* padding is ignored */ + BREAK_PF("meta_padding_0", /*update=*/false, + { meta->padding[0] ^= 1; }); + BREAK_PF("meta_padding_1", /*update=*/false, + { LAST_BYTE(meta->padding) ^= 0xfe; }); + + BREAK_MHT("mht_0", { mht_dec->data_nodes_crypto[0].key[0] ^= 1; }); + BREAK_MHT("mht_1", { mht_dec->data_nodes_crypto[0].gmac[0] ^= 1; }); + BREAK_MHT("mht_2", { mht_dec->mht_nodes_crypto[0].key[0] ^= 1; }); + BREAK_MHT("mht_3", { mht_dec->mht_nodes_crypto[0].gmac[0] ^= 1; }); + BREAK_MHT("mht_4", { mht_dec->data_nodes_crypto[ATTACHED_DATA_NODES_COUNT - 1].key[0] ^= 1; }); + BREAK_MHT("mht_5", { mht_dec->data_nodes_crypto[ATTACHED_DATA_NODES_COUNT - 1].gmac[0] ^= 1; }); + BREAK_MHT("mht_6", { mht_dec->mht_nodes_crypto[CHILD_MHT_NODES_COUNT - 1].key[0] ^= 1; }); + BREAK_MHT("mht_7", { mht_dec->mht_nodes_crypto[CHILD_MHT_NODES_COUNT - 1].gmac[0] ^= 1; }); + BREAK_MHT("mht_8", { + gcm_crypto_data_t crypto; + memcpy(&crypto, &mht_dec->data_nodes_crypto[0], sizeof(crypto)); + memcpy(&mht_dec->data_nodes_crypto[0], &mht_dec->data_nodes_crypto[1], sizeof(crypto)); + memcpy(&mht_dec->data_nodes_crypto[1], &crypto, sizeof(crypto)); + }); + BREAK_MHT("mht_9", { + gcm_crypto_data_t crypto; + memcpy(&crypto, &mht_dec->mht_nodes_crypto[0], sizeof(crypto)); + memcpy(&mht_dec->mht_nodes_crypto[0], &mht_dec->mht_nodes_crypto[1], sizeof(crypto)); + memcpy(&mht_dec->mht_nodes_crypto[1], &crypto, sizeof(crypto)); + }); + + /* data nodes start from node #2 */ + BREAK_PF("data_0", /*update=*/false, + { *(out + 2 * PF_NODE_SIZE) ^= 1; }); + BREAK_PF("data_1", /*update=*/false, + { *(out + 3 * PF_NODE_SIZE - 1) ^= 1; }); + BREAK_PF("data_2", /*update=*/false, { + /* swap data nodes */ + memcpy(out + 2 * PF_NODE_SIZE, g_input_data + 3 * PF_NODE_SIZE, PF_NODE_SIZE); + memcpy(out + 3 * PF_NODE_SIZE, g_input_data + 2 * PF_NODE_SIZE, PF_NODE_SIZE); + }); + + free(mht_dec); + free(meta_dec); +} + +int main(int argc, char* argv[]) { + int ret = -1; + + int option = 0; + char* input_path = NULL; + char* wrap_key_path = NULL; + int input_fd = -1; + + while (true) { + option = getopt_long(argc, argv, "i:o:w:vh", g_options, NULL); + if (option == -1) + break; + + switch (option) { + case 'i': + input_path = optarg; + break; + case 'o': + g_output_dir = optarg; + break; + case 'w': + wrap_key_path = optarg; + break; + case 'v': + set_verbose(true); + break; + case 'h': + usage(); + return 0; + default: + ERROR("Unknown option: %c\n", option); + usage(); + } + } + + if (!input_path) { + ERROR("Input path not specified\n"); + usage(); + goto out; + } + + if (!g_output_dir) { + ERROR("Output path not specified\n"); + usage(); + goto out; + } + + if (!wrap_key_path) { + ERROR("Wrap key path not specified\n"); + usage(); + goto out; + } + + input_fd = open(input_path, O_RDONLY); + if (input_fd < 0) { + ERROR("Failed to open input file '%s': %s\n", input_path, strerror(errno)); + goto out; + } + + g_input_size = get_file_size(input_fd); + if (g_input_size < 0) { + ERROR("Failed to stat input file '%s': %s\n", input_path, strerror(errno)); + goto out; + } + + g_input_data = mmap(NULL, g_input_size, PROT_READ, MAP_PRIVATE, input_fd, 0); + if (g_input_data == MAP_FAILED) { + ERROR("Failed to mmap input file '%s': %s\n", input_path, strerror(errno)); + goto out; + } + + load_wrap_key(wrap_key_path, &g_wrap_key); + derive_main_key(&g_wrap_key, &((metadata_plain_t*)g_input_data)->metadata_key_id, + &g_meta_key); + + g_input_name = basename(input_path); + g_output_path_size = strlen(g_input_name) + strlen(g_output_dir) + 256; + g_output_path = malloc(g_output_path_size); + if (!g_output_path) { + ERROR("No memory\n"); + goto out; + } + + tamper_truncate(); + tamper_modify(); + ret = 0; + +out: + /* skip cleanup as we are in main() */ + return ret; +}