From c8e152d4648b9d65fb8bbb13b7d3ae605e0b4caa Mon Sep 17 00:00:00 2001 From: Dmitrii Kuvaiskii Date: Fri, 23 Apr 2021 00:13:03 -0700 Subject: [PATCH] [Pal/Linux-SGX] Add `sgx.protected_mr{enclave,signer}_files` manifest options Previously, only `sgx.protected_files` were available in the manifest. This kind of protected files needs a provisioned master (wrap) key. But sometimes it is enough to seal files on the same platform for later usage by the same enclave or by enclaves of the same signer: this is the SGX sealing feature. This commit adds two more options to support SGX sealing: `sgx.protected_mrenclave_files` and `sgx.protected_mrsigner_files`. Similarly to `sgx.protected_files`, these new options specify lists of files that are encrypted by the SGX key generated via SGX instruction `EGETKEY(SEAL_KEY)`, bound to the MRENCLAVE/MRSIGNER enclave measurement (so that only instances of the same enclave/only enclaves with the same signer may decrypt protected files). A corresponding LibOS test is added and documentation is updated to reflect this. Signed-off-by: Dmitrii Kuvaiskii --- Documentation/manifest-syntax.rst | 14 ++- LibOS/shim/test/regression/.gitignore | 5 +- LibOS/shim/test/regression/Makefile | 2 + LibOS/shim/test/regression/manifest.template | 3 + LibOS/shim/test/regression/protected_file.c | 99 +++++++++++++++++++ LibOS/shim/test/regression/test_libos.py | 10 ++ Pal/src/host/Linux-SGX/enclave_framework.c | 35 +++++++ Pal/src/host/Linux-SGX/enclave_pf.c | 97 +++++++++++++++--- Pal/src/host/Linux-SGX/pal_linux.h | 19 ++++ .../protected-files/protected_files.h | 2 + 10 files changed, 270 insertions(+), 16 deletions(-) create mode 100644 LibOS/shim/test/regression/protected_file.c diff --git a/Documentation/manifest-syntax.rst b/Documentation/manifest-syntax.rst index 6505c4201c..665057bb68 100644 --- a/Documentation/manifest-syntax.rst +++ b/Documentation/manifest-syntax.rst @@ -488,7 +488,9 @@ Protected files :: sgx.protected_files_key = "[16-byte hex value]" - sgx.protected_files.[identifier] = "[URI]" + sgx.protected_files.[identifier] = "[URI]" + sgx.protected_mrenclave_files.[identifier] = "[URI]" + sgx.protected_mrsigner_files.[identifier] = "[URI]" This syntax specifies the files that are encrypted on disk and transparently decrypted when accessed by Graphene or by application running inside Graphene. @@ -508,6 +510,16 @@ size is limited to 260 bytes. be used only for debugging purposes. In production environments, this key must be provisioned to the enclave using local/remote attestation. +``sgx.protected_files`` are encrypted using the wrap (master) encryption key; +they are well-suited for input files encrypted by the user and sent to the +deployment platform as well as for output files sent back to the user and +decrypted at the user side. ``sgx.protected_mrenclave_files`` are encrypted +using the SGX sealing key based on the MRENCLAVE identity of the enclave; they +are useful to allow only the same enclave (on the same platform) to unseal +files. ``sgx.protected_mrsigner_files`` are encrypted using the SGX sealing key +based on the MRSIGNER identity of the enclave; they are useful to allow all +enclaves from the same signer (but on the same platform) to unseal files. + File check policy ^^^^^^^^^^^^^^^^^ diff --git a/LibOS/shim/test/regression/.gitignore b/LibOS/shim/test/regression/.gitignore index d99fae7830..2751028376 100644 --- a/LibOS/shim/test/regression/.gitignore +++ b/LibOS/shim/test/regression/.gitignore @@ -1,6 +1,9 @@ /*.manifest /*.xml +/protected_file.dat +/testfile + /.cache /abort /abort_multithread @@ -77,6 +80,7 @@ /proc_common /proc_cpuinfo /proc_path +/protected_file /pselect /pthread_set_get_affinity /rdtsc @@ -99,7 +103,6 @@ /sysfs_common /tcp_ipv6_v6only /tcp_msg_peek -/testfile /tmp /udp /unix diff --git a/LibOS/shim/test/regression/Makefile b/LibOS/shim/test/regression/Makefile index 7a244ba8a0..16050a990e 100644 --- a/LibOS/shim/test/regression/Makefile +++ b/LibOS/shim/test/regression/Makefile @@ -68,6 +68,7 @@ c_executables = \ proc_common \ proc_cpuinfo \ proc_path \ + protected_file \ pselect \ pthread_set_get_affinity \ readdir \ @@ -182,6 +183,7 @@ clean-tmp: *.sig \ *.tmp \ *.token \ + *.dat \ *~ \ .cache \ .pytest_cache \ diff --git a/LibOS/shim/test/regression/manifest.template b/LibOS/shim/test/regression/manifest.template index 0048b74379..364130f5c9 100644 --- a/LibOS/shim/test/regression/manifest.template +++ b/LibOS/shim/test/regression/manifest.template @@ -37,6 +37,9 @@ sgx.allowed_files.tmp_dir = "file:tmp/" sgx.allowed_files.root = "file:root" # for getdents test sgx.allowed_files.testfile = "file:testfile" # for mmap_file test +# for protected_file test +sgx.protected_mrenclave_files.pffile = "file:protected_file.dat" + sgx.thread_num = 16 sgx.nonpie_binary = true diff --git a/LibOS/shim/test/regression/protected_file.c b/LibOS/shim/test/regression/protected_file.c new file mode 100644 index 0000000000..e9444384c9 --- /dev/null +++ b/LibOS/shim/test/regression/protected_file.c @@ -0,0 +1,99 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define SECRETSTRING "Secret string\n" + +static ssize_t rw_file(const char* path, char* buf, size_t bytes, bool do_write) { + size_t rv = 0; + size_t ret = 0; + + FILE* f = fopen(path, do_write ? "w" : "r"); + if (!f) { + fprintf(stderr, "opening %s failed\n", path); + return -1; + } + + while (bytes > rv) { + if (do_write) + ret = fwrite(buf + rv, /*size=*/1, /*nmemb=*/bytes - rv, f); + else + ret = fread(buf + rv, /*size=*/1, /*nmemb=*/bytes - rv, f); + + if (ret > 0) { + rv += ret; + } else { + if (feof(f)) { + if (rv) { + /* read some bytes from file, success */ + break; + } + assert(rv == 0); + fprintf(stderr, "%s failed: unexpected end of file\n", do_write ? "write" : "read"); + fclose(f); + return -1; + } + + assert(ferror(f)); + + if (errno == EAGAIN || errno == EINTR) { + continue; + } + + fprintf(stderr, "%s failed: %s\n", do_write ? "write" : "read", strerror(errno)); + fclose(f); + return -1; + } + } + + int close_ret = fclose(f); + if (close_ret) { + fprintf(stderr, "closing %s failed\n", path); + return -1; + } + return rv; +} + + +int main(int argc, char** argv) { + int ret; + ssize_t bytes; + + if (argc != 2) + errx(EXIT_FAILURE, "Usage: %s ", argv[0]); + + ret = access(argv[1], F_OK); + if (ret < 0) { + if (errno == ENOENT) { + /* file is not yet created, create with secret string */ + bytes = rw_file(argv[1], SECRETSTRING, sizeof(SECRETSTRING), /*do_write=*/true); + if (bytes != sizeof(SECRETSTRING)) { + /* error is already printed by rw_file_f() */ + return EXIT_FAILURE; + } + printf("CREATION OK\n"); + return 0; + } + err(EXIT_FAILURE, "access failed"); + } + + char buf[128]; + bytes = rw_file(argv[1], buf, sizeof(buf), /*do_write=*/false); + if (bytes <= 0) { + /* error is already printed by rw_file_f() */ + return EXIT_FAILURE; + } + buf[bytes - 1] = '\0'; + + size_t size_to_cmp = sizeof(SECRETSTRING) < bytes ? sizeof(SECRETSTRING) : bytes; + if (strncmp(SECRETSTRING, buf, size_to_cmp)) + errx(EXIT_FAILURE, "Expected '%s' but read '%s'\n", SECRETSTRING, buf); + + printf("TEST OK\n"); + return 0; +} diff --git a/LibOS/shim/test/regression/test_libos.py b/LibOS/shim/test/regression/test_libos.py index 1aa083be70..48d481bcf1 100644 --- a/LibOS/shim/test/regression/test_libos.py +++ b/LibOS/shim/test/regression/test_libos.py @@ -725,6 +725,16 @@ def test_040_sysfs(self): self.assertIn(f'{node}/hugepages/hugepages-2048kB/nr_hugepages: file', lines) self.assertIn(f'{node}/hugepages/hugepages-1048576kB/nr_hugepages: file', lines) + def test_060_protected_file(self): + pf_path = 'protected_file.dat' + if os.path.exists(pf_path): + os.remove(pf_path) + + stdout, _ = self.run_binary(['protected_file', pf_path]) + self.assertIn('CREATION OK', stdout) + stdout, _ = self.run_binary(['protected_file', pf_path]) + self.assertIn('TEST OK', stdout) + class TC_50_GDB(RegressionTestCase): def setUp(self): diff --git a/Pal/src/host/Linux-SGX/enclave_framework.c b/Pal/src/host/Linux-SGX/enclave_framework.c index 4a186ba778..008e70f7bb 100644 --- a/Pal/src/host/Linux-SGX/enclave_framework.c +++ b/Pal/src/host/Linux-SGX/enclave_framework.c @@ -184,6 +184,41 @@ int sgx_verify_report(sgx_report_t* report) { return 0; } +int sgx_get_seal_key(uint16_t key_policy, sgx_key_128bit_t* seal_key) { + assert(key_policy == KEYPOLICY_MRENCLAVE || key_policy == KEYPOLICY_MRSIGNER); + + /* get our own SGX report to obtain this enclave's isv_svn, cpu_svn, config_svn */ + __sgx_mem_aligned sgx_target_info_t empty_target_info = {0}; + __sgx_mem_aligned sgx_report_data_t empty_report_data = {0}; + __sgx_mem_aligned sgx_report_t our_sgx_report = {0}; + int ret = sgx_get_report(&empty_target_info, &empty_report_data, &our_sgx_report); + if (ret) { + log_error("Failed to get our own enclave report\n"); + return -PAL_ERROR_DENIED; + } + + /* The keyrequest struct dictates the key derivation material used to generate the sealing key. + * It includes MRENCLAVE/MRSIGNER key policy (to allow secret migration/sealing between + * instances of the same enclave or between different enclaves of the same author/signer), and + * CPU/ISV/CONFIG SVNs (to prevent secret migration to older vulnerable versions of the + * enclave). The rest of the keyrequest fields are currently zeros -- CET attributes, enclave + * ATTRIBUTES, enclave MISCSELECT bits are *not* included in key derivation. KEYID is also zero, + * to generate the same sealing key in different instances of the same enclave/same signer. */ + __sgx_mem_aligned sgx_key_request_t key_request = {0}; + key_request.key_name = SEAL_KEY; + key_request.key_policy = key_policy; + memcpy(&key_request.cpu_svn, &our_sgx_report.body.cpu_svn, sizeof(sgx_cpu_svn_t)); + memcpy(&key_request.isv_svn, &our_sgx_report.body.isv_svn, sizeof(sgx_isv_svn_t)); + memcpy(&key_request.config_svn, &our_sgx_report.body.config_svn, sizeof(sgx_config_svn_t)); + + ret = sgx_getkey(&key_request, seal_key); + if (ret) { + log_error("Failed to generate sealing key using SGX EGETKEY\n"); + return -PAL_ERROR_DENIED; + } + return 0; +} + /* For each file that requires authentication (specified in the manifest as "sgx.trusted_files"), a * SHA256 hash is generated and stored in the manifest, signed and verified as part of the enclave's * crypto measurement. When user opens such a file, Graphene loads the whole file, calculates its diff --git a/Pal/src/host/Linux-SGX/enclave_pf.c b/Pal/src/host/Linux-SGX/enclave_pf.c index 85a4d37def..cef10ff3f4 100644 --- a/Pal/src/host/Linux-SGX/enclave_pf.c +++ b/Pal/src/host/Linux-SGX/enclave_pf.c @@ -13,6 +13,14 @@ #include "spinlock.h" #include "toml.h" +/* SGX-specific keys for protected files, used for SGX sealing. The former key is bound to the + * MRENCLAVE measurement of the SGX enclave (only the same enclave can unseal secrets). The latter + * key is bound to the MRSIGNER measurement (all enclaves from the same signer can unseal secrets). + * We don't use synchronization on them since they are only set during initialization where Graphene + * runs single-threaded. */ +pf_key_t g_pf_mrenclave_key = {0}; +pf_key_t g_pf_mrsigner_key = {0}; + /* Wrap key for protected files, either hard-coded in manifest, provisioned during attestation, or * inherited from the parent process. We don't use synchronization on them since they are only set * during initialization where Graphene runs single-threaded. */ @@ -221,7 +229,7 @@ struct protected_file* find_protected_file_handle(PAL_HANDLE handle) { return ret; } -static int register_protected_path(const char* path, struct protected_file** new_pf); +static int register_protected_path(const char* path, int key_type, struct protected_file** new_pf); /* Return a registered PF that matches specified path (or the path that is contained in a registered PF directory) */ @@ -234,7 +242,7 @@ struct protected_file* get_protected_file(const char* path) { if (pf) { /* path not registered but matches registered dir */ log_debug("get_pf: registering new PF '%s' in dir '%s'\n", path, pf->path); - int ret = register_protected_path(path, &pf); + int ret = register_protected_path(path, pf->key_type, &pf); __UNUSED(ret); assert(ret == 0); /* return newly registered PF */ @@ -280,7 +288,7 @@ static int is_directory(const char* path, bool* is_dir) { } /* Register all files from the given directory recursively */ -static int register_protected_dir(const char* path) { +static int register_protected_dir(const char* path, int key_type) { int fd = -1; int ret = -PAL_ERROR_NOMEM; size_t bufsize = 1024; @@ -324,7 +332,7 @@ static int register_protected_dir(const char* path) { goto out; snprintf(sub_path, sub_path_size, URI_PREFIX_FILE "%s/%s", path, dir->d_name); - ret = register_protected_path(sub_path, NULL); + ret = register_protected_path(sub_path, key_type, NULL); if (ret != 0) { free(sub_path); goto out; @@ -344,7 +352,7 @@ static int register_protected_dir(const char* path) { } /* Register a single PF (if it's a directory, recursively) */ -static int register_protected_path(const char* path, struct protected_file** new_pf) { +static int register_protected_path(const char* path, int key_type, struct protected_file** new_pf) { int ret = -PAL_ERROR_NOMEM; struct protected_file* new = NULL; @@ -377,6 +385,8 @@ static int register_protected_path(const char* path, struct protected_file** new goto out; } + new->key_type = key_type; + new->path_len = strlen(path); /* This is never freed but so isn't the whole struct, PFs persist for the whole lifetime of the process. */ @@ -398,7 +408,7 @@ static int register_protected_path(const char* path, struct protected_file** new log_debug("register_protected_path: [%s] %s = %p\n", is_dir ? "dir" : "file", path, new); if (is_dir) - register_protected_dir(path); + register_protected_dir(path, key_type); pf_lock(); @@ -425,13 +435,30 @@ static int register_protected_path(const char* path, struct protected_file** new } /* Read PF paths from manifest and register them */ -static int register_protected_files(void) { +static int register_protected_files(int key_type) { int ret; toml_table_t* manifest_sgx = toml_table_in(g_pal_state.manifest_root, "sgx"); if (!manifest_sgx) return 0; - toml_table_t* toml_pfs = toml_table_in(manifest_sgx, "protected_files"); + char* table_name = NULL; + switch (key_type) { + case PROTECTED_FILE_KEY_WRAP: + table_name = "protected_files"; + break; + case PROTECTED_FILE_KEY_MRENCLAVE: + table_name = "protected_mrenclave_files"; + break; + case PROTECTED_FILE_KEY_MRSIGNER: + table_name = "protected_mrsigner_files"; + break; + default: + log_error("Invalid key type when registering protected files!\n"); + return -PAL_ERROR_INVAL; + } + + assert(table_name); + toml_table_t* toml_pfs = toml_table_in(manifest_sgx, table_name); if (!toml_pfs) return 0; @@ -458,7 +485,7 @@ static int register_protected_files(void) { log_error("Invalid URI [%s]: URIs of protected files must start with \'" URI_PREFIX_FILE "\'\n", toml_pf_value); } else { - register_protected_path(toml_pf_value, NULL); + register_protected_path(toml_pf_value, key_type, NULL); } free(toml_pf_value); } @@ -482,6 +509,18 @@ int init_protected_files(void) { pf_set_callbacks(cb_read, cb_write, cb_truncate, cb_aes_cmac, cb_aes_gcm_encrypt, cb_aes_gcm_decrypt, cb_random, debug_callback); + ret = sgx_get_seal_key(KEYPOLICY_MRENCLAVE, &g_pf_mrenclave_key); + if (ret < 0) { + log_error("Cannot obtain MRENCLAVE-specific protected files key\n"); + return ret; + } + + ret = sgx_get_seal_key(KEYPOLICY_MRSIGNER, &g_pf_mrsigner_key); + if (ret < 0) { + log_error("Cannot obtain MRSIGNER-specific protected files key\n"); + return ret; + } + /* if wrap key is not hard-coded in the manifest, assume that it was received from parent or * it will be provisioned after local/remote attestation; otherwise read it from manifest */ char* protected_files_key_str = NULL; @@ -515,8 +554,22 @@ int init_protected_files(void) { g_pf_wrap_key_set = true; } - if (register_protected_files() < 0) { + ret = register_protected_files(PROTECTED_FILE_KEY_WRAP); + if (ret < 0) { log_error("Malformed protected files found in manifest\n"); + return ret; + } + + ret = register_protected_files(PROTECTED_FILE_KEY_MRENCLAVE); + if (ret < 0) { + log_error("Malformed MRENCLAVE-specific protected files found in manifest\n"); + return ret; + } + + ret = register_protected_files(PROTECTED_FILE_KEY_MRSIGNER); + if (ret < 0) { + log_error("Malformed MRSIGNER-specific protected files found in manifest\n"); + return ret; } return 0; @@ -525,13 +578,29 @@ int init_protected_files(void) { /* Open/create a PF */ static int open_protected_file(const char* path, struct protected_file* pf, pf_handle_t handle, uint64_t size, pf_file_mode_t mode, bool create) { - if (!g_pf_wrap_key_set) { - log_error("pf_open(%d, %s) failed: wrap key was not provided\n", *(int*)handle, path); - return -PAL_ERROR_DENIED; + pf_key_t* pf_key = NULL; + switch (pf->key_type) { + case PROTECTED_FILE_KEY_WRAP: + if (!g_pf_wrap_key_set) { + log_error("pf_open failed: wrap key was not provided\n"); + return -PAL_ERROR_DENIED; + } + pf_key = &g_pf_wrap_key; + break; + case PROTECTED_FILE_KEY_MRENCLAVE: + pf_key = &g_pf_mrenclave_key; + break; + case PROTECTED_FILE_KEY_MRSIGNER: + pf_key = &g_pf_mrsigner_key; + break; + default: + log_error("Invalid key type when opening protected file!\n"); + return -PAL_ERROR_DENIED; } + assert(pf_key); pf_status_t pfs; - pfs = pf_open(handle, path, size, mode, create, &g_pf_wrap_key, &pf->context); + pfs = pf_open(handle, path, size, mode, create, pf_key, &pf->context); if (PF_FAILURE(pfs)) { log_error("pf_open(%d, %s) failed: %s\n", *(int*)handle, path, pf_strerror(pfs)); return -PAL_ERROR_DENIED; diff --git a/Pal/src/host/Linux-SGX/pal_linux.h b/Pal/src/host/Linux-SGX/pal_linux.h index b68f4e7e1f..8796b6ffb8 100644 --- a/Pal/src/host/Linux-SGX/pal_linux.h +++ b/Pal/src/host/Linux-SGX/pal_linux.h @@ -174,6 +174,12 @@ DEFINE_LISTP(pf_map); /* List of PF map buffers; this list is traversed on PF flush (on file close) */ extern LISTP_TYPE(pf_map) g_pf_map_list; +enum { + PROTECTED_FILE_KEY_WRAP = 0, + PROTECTED_FILE_KEY_MRENCLAVE, + PROTECTED_FILE_KEY_MRSIGNER, +}; + /* Data of a protected file */ struct protected_file { UT_hash_handle hh; @@ -182,6 +188,7 @@ struct protected_file { pf_context_t* context; /* NULL until PF is opened */ int64_t refcount; /* used for deciding when to call unload_protected_file() */ int writable_fd; /* fd of underlying file for writable PF, -1 if no writable handles are open */ + int key_type; /* one of KEY_WRAP (provisioned key), KEY_MRENCLAVE, KEY_MRSIGNER */ }; /* Initialize the PF library, register PFs from the manifest */ @@ -254,6 +261,18 @@ int sgx_verify_report(sgx_report_t* report); int sgx_get_report(const sgx_target_info_t* target_info, const sgx_report_data_t* data, sgx_report_t* report); +/*! + * \brief Obtain an enclave/signer-specific key via EGETKEY(SEAL_KEY) for secret migration/sealing + * of files. + * + * \param[in] key_policy Must be KEYPOLICY_MRENCLAVE or KEYPOLICY_MRSIGNER. Binds the sealing key + * to MRENCLAVE (only the same enclave can unseal secrets) or to MRSIGNER + * (all enclaves from the same signer can unseal secrets). + * \param[out] seal_key Output buffer to store the sealing key. + * \return 0 on success, negative error code otherwise. + */ +int sgx_get_seal_key(uint16_t key_policy, sgx_key_128bit_t* seal_key); + /*! * \brief Verify the remote enclave during SGX local attestation. * diff --git a/Pal/src/host/Linux-SGX/protected-files/protected_files.h b/Pal/src/host/Linux-SGX/protected-files/protected_files.h index 7dd8406639..c00bf46940 100644 --- a/Pal/src/host/Linux-SGX/protected-files/protected_files.h +++ b/Pal/src/host/Linux-SGX/protected-files/protected_files.h @@ -27,6 +27,8 @@ typedef uint8_t pf_mac_t[PF_MAC_SIZE]; typedef uint8_t pf_key_t[PF_KEY_SIZE]; typedef uint8_t pf_keyid_t[32]; /* key derivation material */ +extern pf_key_t g_pf_mrenclave_key; +extern pf_key_t g_pf_mrsigner_key; extern pf_key_t g_pf_wrap_key; extern bool g_pf_wrap_key_set;