From 2ed3b55949931da4feecd23791bd1622ca392d98 Mon Sep 17 00:00:00 2001 From: Dmitrii Kuvaiskii Date: Thu, 14 Mar 2024 02:08:05 -0700 Subject: [PATCH] [LibOS] Move trusted and allowed files logic to LibOS Now `gramine-direct` behaves similarly to `gramine-sgx`: it reads `sgx.allowed_files` and `sgx.trusted_files` arrays, as well as `sgx.file_check_policy`, and applies the corresponding file-access rules. Note that the names of the manifest options are kept the same for backward compatibility; a future commit will introduce new, more appropriate aliases. Several tests that were previously SGX PAL-specific only are now enabled on `gramine-direct` as well. All LTP tests, even though they are run under `gramine-direct` only, check the allowed/trusted files logic now. As the primary effect of this commit, the SGX PAL code is significantly simplified and is closer to the native Linux (`direct`) PAL code. Signed-off-by: Dmitrii Kuvaiskii --- CI-Examples/blender/Makefile | 4 +- CI-Examples/busybox/Makefile | 4 +- CI-Examples/helloworld/Makefile | 4 +- CI-Examples/lighttpd/Makefile | 4 +- CI-Examples/memcached/Makefile | 4 +- CI-Examples/nginx/Makefile | 10 +- CI-Examples/ra-tls-mbedtls/Makefile | 12 +- CI-Examples/ra-tls-secret-prov/Makefile | 13 +- CI-Examples/redis/Makefile | 16 +- CI-Examples/rust/Makefile | 4 +- Documentation/manpages/gramine-manifest.rst | 11 + libos/include/libos_fs.h | 28 + libos/src/fs/chroot/allowed.c | 203 ++++++ libos/src/fs/chroot/encrypted.c | 6 +- libos/src/fs/chroot/file_check_policy.c | 39 ++ libos/src/fs/chroot/fs.c | 412 ++++++++++-- libos/src/fs/chroot/trusted.c | 437 +++++++++++++ libos/src/fs/libos_fs.c | 13 + libos/src/fs/libos_fs_encrypted.c | 2 +- libos/src/libos_init.c | 10 + libos/src/meson.build | 3 + libos/test/ltp/ltp.cfg | 2 +- libos/test/ltp/manifest.template | 15 +- libos/test/regression/fork_and_access_file.c | 11 +- .../test/regression/fork_and_access_file.gdb | 6 +- libos/test/regression/test_libos.py | 24 +- .../host/linux-common/pal_flags_conv.h | 2 +- pal/include/pal/pal.h | 3 +- pal/src/host/linux-sgx/enclave_framework.c | 597 ------------------ pal/src/host/linux-sgx/enclave_ocalls.c | 4 +- pal/src/host/linux-sgx/enclave_tf.h | 80 --- pal/src/host/linux-sgx/enclave_tf_structs.h | 39 -- pal/src/host/linux-sgx/pal_devices.c | 13 - pal/src/host/linux-sgx/pal_files.c | 156 +---- pal/src/host/linux-sgx/pal_host.h | 8 +- pal/src/host/linux-sgx/pal_linux.h | 3 +- pal/src/host/linux-sgx/pal_linux_defs.h | 2 - pal/src/host/linux-sgx/pal_main.c | 41 +- pal/src/host/linux-sgx/pal_misc.c | 1 + pal/src/host/linux-sgx/pal_streams.c | 3 - python/gramine-manifest | 6 +- 41 files changed, 1239 insertions(+), 1016 deletions(-) create mode 100644 libos/src/fs/chroot/allowed.c create mode 100644 libos/src/fs/chroot/file_check_policy.c create mode 100644 libos/src/fs/chroot/trusted.c delete mode 100644 pal/src/host/linux-sgx/enclave_tf.h delete mode 100644 pal/src/host/linux-sgx/enclave_tf_structs.h diff --git a/CI-Examples/blender/Makefile b/CI-Examples/blender/Makefile index b3830f9485..8720d6c367 100644 --- a/CI-Examples/blender/Makefile +++ b/CI-Examples/blender/Makefile @@ -41,7 +41,7 @@ $(BLENDER_DIR)/blender: $(RUN_DIR): mkdir -p $@ -blender.manifest: blender.manifest.template | $(RUN_DIR) +blender.manifest: blender.manifest.template $(BLENDER_DIR)/blender | $(RUN_DIR) gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -56,7 +56,7 @@ blender.sig blender.manifest.sgx: sgx_outputs @: .INTERMEDIATE: sgx_outputs -sgx_outputs: $(BLENDER_DIR)/blender blender.manifest | $(RUN_DIR) +sgx_outputs: blender.manifest | $(RUN_DIR) gramine-sgx-sign \ --output blender.manifest.sgx \ --manifest blender.manifest diff --git a/CI-Examples/busybox/Makefile b/CI-Examples/busybox/Makefile index a9307e91d6..865df78386 100644 --- a/CI-Examples/busybox/Makefile +++ b/CI-Examples/busybox/Makefile @@ -42,7 +42,7 @@ $(SRCDIR)/.config: $(SRCDIR)/Makefile $(SRCDIR)/busybox: $(SRCDIR)/.config $(MAKE) -C $(SRCDIR) -busybox.manifest: busybox.manifest.template +busybox.manifest: busybox.manifest.template busybox gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -57,7 +57,7 @@ busybox.manifest.sgx busybox.sig: sgx_sign @: .INTERMEDIATE: sgx_sign -sgx_sign: busybox.manifest busybox +sgx_sign: busybox.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/CI-Examples/helloworld/Makefile b/CI-Examples/helloworld/Makefile index d20feefb70..ca918c78dc 100644 --- a/CI-Examples/helloworld/Makefile +++ b/CI-Examples/helloworld/Makefile @@ -21,7 +21,7 @@ helloworld: helloworld.o helloworld.o: helloworld.c -helloworld.manifest: helloworld.manifest.template +helloworld.manifest: helloworld.manifest.template helloworld gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ $< $@ @@ -44,7 +44,7 @@ helloworld.sig helloworld.manifest.sgx: sgx_sign @: .INTERMEDIATE: sgx_sign -sgx_sign: helloworld.manifest helloworld +sgx_sign: helloworld.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/CI-Examples/lighttpd/Makefile b/CI-Examples/lighttpd/Makefile index 21bb13d507..7aeff16365 100644 --- a/CI-Examples/lighttpd/Makefile +++ b/CI-Examples/lighttpd/Makefile @@ -47,7 +47,7 @@ $(LIGHTTPD_SRC).tar.gz: ../common_tools/download --output $@ --sha256 $(LIGHTTPD_HASH) \ $(foreach mirror,$(LIGHTTPD_MIRRORS),--url $(mirror)/$(LIGHTTPD_SRC).tar.gz) -lighttpd.manifest: lighttpd.manifest.template +lighttpd.manifest: lighttpd.manifest.template $(INSTALL_DIR)/sbin/lighttpd gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -60,7 +60,7 @@ lighttpd.manifest.sgx lighttpd.sig: sgx_sign @: .INTERMEDIATE: sgx_sign -sgx_sign: lighttpd.manifest $(INSTALL_DIR)/sbin/lighttpd +sgx_sign: lighttpd.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/CI-Examples/memcached/Makefile b/CI-Examples/memcached/Makefile index 88b74b8ffc..2dba3f1971 100644 --- a/CI-Examples/memcached/Makefile +++ b/CI-Examples/memcached/Makefile @@ -33,7 +33,7 @@ $(SRCDIR)/memcached: $(SRCDIR)/configure cd $(SRCDIR) && ./configure $(MAKE) -C $(SRCDIR) -memcached.manifest: memcached.manifest.template +memcached.manifest: memcached.manifest.template memcached gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -45,7 +45,7 @@ memcached.manifest.sgx memcached.sig: sgx_sign @: .INTERMEDIATE: sgx_sign -sgx_sign: memcached.manifest memcached +sgx_sign: memcached.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/CI-Examples/nginx/Makefile b/CI-Examples/nginx/Makefile index 4a87a0c4a5..223d0168f8 100644 --- a/CI-Examples/nginx/Makefile +++ b/CI-Examples/nginx/Makefile @@ -50,7 +50,10 @@ $(NGINX_SRC).tar.gz: ../common_tools/download --output $@ --sha256 $(NGINX_SHA256) \ $(foreach mirror,$(NGINX_MIRRORS),--url $(mirror)/$(NGINX_SRC).tar.gz) -nginx.manifest: nginx.manifest.template +nginx.manifest: nginx.manifest.template $(INSTALL_DIR)/sbin/nginx \ + $(INSTALL_DIR)/conf/nginx-gramine.conf \ + $(TEST_DATA) nginx_args \ + $(INSTALL_DIR)/conf/server.crt gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -64,10 +67,7 @@ nginx.manifest.sgx nginx.sig: sgx_sign @: .INTERMEDIATE: sgx_sign -sgx_sign: nginx.manifest $(INSTALL_DIR)/sbin/nginx \ - $(INSTALL_DIR)/conf/nginx-gramine.conf \ - $(TEST_DATA) \ - $(INSTALL_DIR)/conf/server.crt +sgx_sign: nginx.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/CI-Examples/ra-tls-mbedtls/Makefile b/CI-Examples/ra-tls-mbedtls/Makefile index f8a3fbbfb2..db7b7f5d2b 100644 --- a/CI-Examples/ra-tls-mbedtls/Makefile +++ b/CI-Examples/ra-tls-mbedtls/Makefile @@ -56,7 +56,7 @@ client: src/client.c ############################### SERVER MANIFEST ############################### -server.manifest: server.manifest.template +server.manifest: server.manifest.template server gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -71,14 +71,14 @@ server.manifest.sgx server.sig: sgx_sign_server @: .INTERMEDIATE: sgx_sign_server -sgx_sign_server: server.manifest server +sgx_sign_server: server.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx ########################### CLIENT (DCAP) MANIFEST ############################ -client_dcap.manifest: client.manifest.template +client_dcap.manifest: client.manifest.template client gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -88,14 +88,14 @@ client_dcap.manifest.sgx client_dcap.sig: sgx_sign_client_dcap @: .INTERMEDIATE: sgx_sign_client_dcap -sgx_sign_client_dcap: client_dcap.manifest client +sgx_sign_client_dcap: client_dcap.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx ########################### CLIENT (EPID) MANIFEST ############################ -client_epid.manifest: client.manifest.template +client_epid.manifest: client.manifest.template client gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -105,7 +105,7 @@ client_epid.manifest.sgx client_epid.sig: sgx_sign_client_epid @: .INTERMEDIATE: sgx_sign_client_epid -sgx_sign_client_epid: client_epid.manifest client +sgx_sign_client_epid: client_epid.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/CI-Examples/ra-tls-secret-prov/Makefile b/CI-Examples/ra-tls-secret-prov/Makefile index 4dcc3d5bcf..68c8c37dc0 100644 --- a/CI-Examples/ra-tls-secret-prov/Makefile +++ b/CI-Examples/ra-tls-secret-prov/Makefile @@ -76,7 +76,8 @@ secret_prov_pf/client: secret_prov_pf/client.c # TODO: Simplify after https://github.com/gramineproject/gramine/issues/878 is fixed (manifest paths # should be relative to the manifest, not to current dir) - drop `cd` and `notdir`. -secret_prov_minimal/client.manifest: secret_prov_minimal/client.manifest.template +secret_prov_minimal/client.manifest: secret_prov_minimal/client.manifest.template \ + secret_prov_minimal/client cd secret_prov_minimal && \ gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ @@ -92,7 +93,7 @@ secret_prov_minimal/client.manifest.sgx secret_prov_minimal/client.sig: sgx_sign @: .INTERMEDIATE: sgx_sign_secret_prov_minimal_client -sgx_sign_secret_prov_minimal_client: secret_prov_minimal/client.manifest secret_prov_minimal/client +sgx_sign_secret_prov_minimal_client: secret_prov_minimal/client.manifest cd secret_prov_minimal && \ gramine-sgx-sign \ --manifest $(notdir $<) \ @@ -100,7 +101,7 @@ sgx_sign_secret_prov_minimal_client: secret_prov_minimal/client.manifest secret_ ############################### CLIENT MANIFEST ############################### -secret_prov/client.manifest: secret_prov/client.manifest.template +secret_prov/client.manifest: secret_prov/client.manifest.template secret_prov/client cd secret_prov && \ gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ @@ -114,7 +115,7 @@ secret_prov/client.manifest.sgx secret_prov/client.sig: sgx_sign_secret_prov_cli @: .INTERMEDIATE: sgx_sign_secret_prov_client -sgx_sign_secret_prov_client: secret_prov/client.manifest secret_prov/client +sgx_sign_secret_prov_client: secret_prov/client.manifest cd secret_prov && \ gramine-sgx-sign \ --manifest $(notdir $<) \ @@ -122,7 +123,7 @@ sgx_sign_secret_prov_client: secret_prov/client.manifest secret_prov/client ############################## PF CLIENT MANIFEST ############################# -secret_prov_pf/client.manifest: secret_prov_pf/client.manifest.template +secret_prov_pf/client.manifest: secret_prov_pf/client.manifest.template secret_prov_pf/client cd secret_prov_pf && \ gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ @@ -136,7 +137,7 @@ secret_prov_pf/client.manifest.sgx secret_prov_pf/client.sig: sgx_sign_secret_pr @: .INTERMEDIATE: sgx_sign_secret_prov_pf_client -sgx_sign_secret_prov_pf_client: secret_prov_pf/client.manifest secret_prov_pf/client +sgx_sign_secret_prov_pf_client: secret_prov_pf/client.manifest cd secret_prov_pf && \ gramine-sgx-sign \ --manifest $(notdir $<) \ diff --git a/CI-Examples/redis/Makefile b/CI-Examples/redis/Makefile index 5e2ca453e2..be174bbe4b 100644 --- a/CI-Examples/redis/Makefile +++ b/CI-Examples/redis/Makefile @@ -70,25 +70,25 @@ endif # information to run Redis under Gramine / Gramine-SGX. We create # redis-server.manifest (to be run under non-SGX Gramine) by replacing variables # in the template file using the "gramine-manifest" script. - -redis-server.manifest: redis-server.manifest.template +# +# "gramine-manifest" also measures all Redis trusted files and adds the +# measurements to the resulting manifest file. +redis-server.manifest: redis-server.manifest.template $(SRCDIR)/src/redis-server gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ $< > $@ -# Manifest for Gramine-SGX requires special "gramine-sgx-sign" procedure. This -# procedure measures all Redis trusted files, adds the measurement to the -# resulting manifest.sgx file (among other, less important SGX options) and -# creates redis-server.sig (SIGSTRUCT object). - # Make on Ubuntu <= 20.04 doesn't support "Rules with Grouped Targets" (`&:`), # see the helloworld example for details on this workaround. redis-server.sig redis-server.manifest.sgx: sgx_outputs @: +# Manifest for Gramine-SGX requires special "gramine-sgx-sign" procedure. This +# procedure creates the final manifest.sgx file and an SGX-specific +# redis-server.sig file (SIGSTRUCT object). .INTERMEDIATE: sgx_outputs -sgx_outputs: redis-server.manifest $(SRCDIR)/src/redis-server +sgx_outputs: redis-server.manifest gramine-sgx-sign \ --manifest redis-server.manifest \ --output redis-server.manifest.sgx diff --git a/CI-Examples/rust/Makefile b/CI-Examples/rust/Makefile index 1996ef0370..14680bc157 100644 --- a/CI-Examples/rust/Makefile +++ b/CI-Examples/rust/Makefile @@ -25,7 +25,7 @@ endif $(SELF_EXE): Cargo.toml cargo build --release -rust-hyper-http-server.manifest: rust-hyper-http-server.manifest.template +rust-hyper-http-server.manifest: rust-hyper-http-server.manifest.template $(SELF_EXE) gramine-manifest \ -Dlog_level=$(GRAMINE_LOG_LEVEL) \ -Darch_libdir=$(ARCH_LIBDIR) \ @@ -38,7 +38,7 @@ rust-hyper-http-server.manifest.sgx rust-hyper-http-server.sig: sgx_sign @: .INTERMEDIATE: sgx_sign -sgx_sign: rust-hyper-http-server.manifest $(SELF_EXE) +sgx_sign: rust-hyper-http-server.manifest gramine-sgx-sign \ --manifest $< \ --output $<.sgx diff --git a/Documentation/manpages/gramine-manifest.rst b/Documentation/manpages/gramine-manifest.rst index ab57e22bfd..cfbf75baa3 100644 --- a/Documentation/manpages/gramine-manifest.rst +++ b/Documentation/manpages/gramine-manifest.rst @@ -40,6 +40,17 @@ Command line arguments Disable schema validation, as described above in :option:`--check`. +.. option:: --chroot + + When calculating cryptographic hashes of trusted files, measure files inside + a |~| chroot instead of paths in root of the file system. Requires that all + paths in manifest are absolute, and those will be interpreted as relative to + the directory specified as the value of the option. + + Note you need to be very careful that the Gramine runtime binaries are + exactly the same inside chroot as the ones used to execute + :program:`gramine-manifest`. + Functions and constants available in templates ============================================== diff --git a/libos/include/libos_fs.h b/libos/include/libos_fs.h index 0590a9c8db..f71664a4dc 100644 --- a/libos/include/libos_fs.h +++ b/libos/include/libos_fs.h @@ -20,6 +20,33 @@ #include "list.h" #include "pal.h" +enum { + FILE_CHECK_POLICY_STRICT = 0, + FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG, +}; +extern int g_file_check_policy; + +struct trusted_file_hash { + uint8_t bytes[32]; +}; +struct trusted_chunk_hash { + uint8_t bytes[16]; +}; +struct trusted_file; +struct allowed_file; + +struct trusted_file* get_trusted_file(const char* path); +struct allowed_file* get_allowed_file(const char* path); +size_t get_chunk_hashes_size(size_t file_size); +int load_trusted_file(struct trusted_file* tf, size_t file_size, + struct trusted_chunk_hash** out_chunk_hashes); +int copy_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t count, uint8_t* buf, + size_t file_size, struct trusted_chunk_hash* chunk_hashes); +int register_allowed_file(const char* path); +int init_trusted_files(void); +int init_allowed_files(void); +int init_file_check_policy(void); + struct libos_handle; /* Describes mount parameters. Passed to `mount_fs`, and to the `mount` callback. */ @@ -532,6 +559,7 @@ extern struct libos_dentry* g_dentry_root; /* initialization for fs and mounts */ int init_fs(void); +int init_trusted_allowed_files(void); int init_mount_root(void); int init_mount(void); int mount_etcfs(void); diff --git a/libos/src/fs/chroot/allowed.c b/libos/src/fs/chroot/allowed.c new file mode 100644 index 0000000000..e04f13a79f --- /dev/null +++ b/libos/src/fs/chroot/allowed.c @@ -0,0 +1,203 @@ +/* Copyright (C) 2024 Intel Corporation + * Dmitrii Kuvaiskii + */ + +/* + * This file contains code for allowed files in 'chroot' filesystem. + * + * Allowed files (AF) have no protections applied. It is the responsibility of the application to + * apply suitable protections for each AF. + * + * Allowed files are useful for debugging, or when files are guaranteed to have no effect on + * security of the execution (e.g. non-critical logs), or when the application itself protects these + * files. + */ + +#include +#include + +#include "api.h" +#include "libos_fs.h" +#include "list.h" +#include "path_utils.h" +#include "toml.h" + +DEFINE_LIST(allowed_file); +struct allowed_file { + LIST_TYPE(allowed_file) list; + size_t path_len; + char path[]; /* must be NULL-terminated */ +}; + +DEFINE_LISTP(allowed_file); +static LISTP_TYPE(allowed_file) g_allowed_file_list = LISTP_INIT; +static spinlock_t g_allowed_file_lock = INIT_SPINLOCK_UNLOCKED; + +static bool path_is_equal_or_subpath(const struct allowed_file* af, const char* path, + size_t path_len) { + if (af->path_len > path_len || memcmp(af->path, path, af->path_len)) { + /* af path is not a prefix of `path` */ + return false; + } + if (af->path_len == path_len) { + /* Both are equal */ + return true; + } + if (af->path[af->path_len - 1] == '/') { + /* af path is a subpath of `path` (with slash), e.g. "foo/" and "foo/bar" */ + return true; + } + if (path[af->path_len] == '/') { + /* af path is a subpath of `path` (without slash), e.g. "foo" and "foo/bar" */ + return true; + } + return false; +} + +struct allowed_file* get_allowed_file(const char* path) { + size_t norm_path_size = strlen(path) + 1; /* overapproximate */ + char* norm_path = malloc(norm_path_size); + if (!norm_path) + return NULL; + + bool normalized = get_norm_path(path, norm_path, &norm_path_size); + if (!normalized) { + free(norm_path); + return NULL; + } + + struct allowed_file* af = NULL; + + spinlock_lock(&g_allowed_file_lock); + struct allowed_file* tmp; + LISTP_FOR_EACH_ENTRY(tmp, &g_allowed_file_list, list) { + /* must be a sub-directory or file */ + if (path_is_equal_or_subpath(tmp, norm_path, norm_path_size - 1)) { + af = tmp; + break; + } + } + spinlock_unlock(&g_allowed_file_lock); + free(norm_path); + return af; +} + +int register_allowed_file(const char* path) { + size_t path_len = strlen(path); + if (path_len >= URI_MAX) { + log_error("Size of file exceeds maximum %dB: %s", URI_MAX, path); + return -EINVAL; + } + + struct allowed_file* new = malloc(sizeof(*new) + path_len + 1); + if (!new) + return -ENOMEM; + + INIT_LIST_HEAD(new, list); + new->path_len = path_len; + memcpy(new->path, path, path_len + 1); + + spinlock_lock(&g_allowed_file_lock); + struct allowed_file* af; + LISTP_FOR_EACH_ENTRY(af, &g_allowed_file_list, list) { + /* below check is required because same file could have been added by another thread */ + if (af->path_len == path_len && !memcmp(af->path, path, path_len)) { + spinlock_unlock(&g_allowed_file_lock); + free(new); + return 0; + } + } + LISTP_ADD_TAIL(new, &g_allowed_file_list, list); + spinlock_unlock(&g_allowed_file_lock); + return 0; +} + +static int init_one_allowed_file(toml_raw_t toml_allowed_uri_raw, size_t idx) { + int ret; + + /* FIXME: toml_allowed_uri_str is a temporary string, allocating it is redundant; however + * tomlc99 lib has only toml_rtos() function that returns a newly allocated string rather + * than a slice into the parsed TOML structure */ + char* toml_allowed_uri_str = NULL; + + /* FIXME: instead of re-allocating in register_allowed_file(), could pass ownership to it */ + char* norm_allowed_path = NULL; + + ret = toml_rtos(toml_allowed_uri_raw, &toml_allowed_uri_str); + if (ret < 0) { + log_error("Invalid allowed file in manifest at index %ld (not a string)", idx); + ret = -EINVAL; + goto out; + } + + if (!strstartswith(toml_allowed_uri_str, URI_PREFIX_FILE) + && !strstartswith(toml_allowed_uri_str, URI_PREFIX_DEV)) { + log_error("Invalid URI [%s]: Allowed files must start with 'file:' or 'dev:'", + toml_allowed_uri_str); + ret = -EINVAL; + goto out; + } + + size_t norm_allowed_path_size = strlen(toml_allowed_uri_str) + 1; /* overapproximate */ + norm_allowed_path = malloc(norm_allowed_path_size); + if (!norm_allowed_path) { + ret = -ENOMEM; + goto out; + } + + size_t uri_prefix_len = strstartswith(toml_allowed_uri_str, URI_PREFIX_FILE) + ? URI_PREFIX_FILE_LEN : URI_PREFIX_DEV_LEN; + + bool normalized = get_norm_path(toml_allowed_uri_str + uri_prefix_len, + norm_allowed_path, &norm_allowed_path_size); + if (!normalized) { + log_error("Allowed file path (%s) normalization failed", toml_allowed_uri_str); + ret = -EINVAL; + goto out; + } + + ret = register_allowed_file(norm_allowed_path); + if (ret < 0) { + log_error("Allowed file registration (%s) failed", toml_allowed_uri_str); + goto out; + } + + ret = 0; +out: + free(norm_allowed_path); + free(toml_allowed_uri_str); + return ret; +} + +int init_allowed_files(void) { + int ret; + + assert(g_manifest_root); + toml_table_t* manifest_sgx = toml_table_in(g_manifest_root, "sgx"); + if (!manifest_sgx) + return 0; + + toml_array_t* toml_allowed_files = toml_array_in(manifest_sgx, "allowed_files"); + if (!toml_allowed_files) + return 0; + + ssize_t toml_allowed_files_cnt = toml_array_nelem(toml_allowed_files); + if (toml_allowed_files_cnt < 0) + return -EPERM; + if (toml_allowed_files_cnt == 0) + return 0; + + for (ssize_t i = 0; i < toml_allowed_files_cnt; i++) { + toml_raw_t toml_allowed_uri_raw = toml_raw_at(toml_allowed_files, i); + if (!toml_allowed_uri_raw) { + log_error("Invalid allowed file in manifest at index %ld", i); + return -EINVAL; + } + + ret = init_one_allowed_file(toml_allowed_uri_raw, i); + if (ret < 0) + return ret; + } + + return 0; +} diff --git a/libos/src/fs/chroot/encrypted.c b/libos/src/fs/chroot/encrypted.c index 27c46551dd..44cc13fb1c 100644 --- a/libos/src/fs/chroot/encrypted.c +++ b/libos/src/fs/chroot/encrypted.c @@ -273,7 +273,7 @@ static int chroot_encrypted_mkdir(struct libos_dentry* dent, mode_t perm) { /* This opens a "dir:..." URI */ PAL_HANDLE palhdl; ret = PalStreamOpen(uri, PAL_ACCESS_RDONLY, HOST_PERM(perm), PAL_CREATE_ALWAYS, - PAL_OPTION_PASSTHROUGH, &palhdl); + /*options=*/0, &palhdl); if (ret < 0) { ret = pal_to_unix_errno(ret); goto out; @@ -303,7 +303,7 @@ static int chroot_encrypted_unlink(struct libos_dentry* dent) { PAL_HANDLE palhdl; ret = PalStreamOpen(uri, PAL_ACCESS_RDONLY, /*share_flags=*/0, PAL_CREATE_NEVER, - PAL_OPTION_PASSTHROUGH, &palhdl); + /*options=*/0, &palhdl); if (ret < 0) { ret = pal_to_unix_errno(ret); goto out; @@ -368,7 +368,7 @@ static int chroot_encrypted_chmod(struct libos_dentry* dent, mode_t perm) { PAL_HANDLE palhdl; ret = PalStreamOpen(uri, PAL_ACCESS_RDONLY, /*share_flags=*/0, PAL_CREATE_NEVER, - PAL_OPTION_PASSTHROUGH, &palhdl); + /*options=*/0, &palhdl); if (ret < 0) { ret = pal_to_unix_errno(ret); goto out; diff --git a/libos/src/fs/chroot/file_check_policy.c b/libos/src/fs/chroot/file_check_policy.c new file mode 100644 index 0000000000..1ce80d9750 --- /dev/null +++ b/libos/src/fs/chroot/file_check_policy.c @@ -0,0 +1,39 @@ +/* Copyright (C) 2024 Intel Corporation + * Dmitrii Kuvaiskii + */ + +#include "api.h" +#include "libos_fs.h" +#include "toml_utils.h" + +int g_file_check_policy = FILE_CHECK_POLICY_STRICT; + +int init_file_check_policy(void) { + int ret; + char* file_check_policy_str = NULL; + + assert(g_manifest_root); + ret = toml_string_in(g_manifest_root, "sgx.file_check_policy", &file_check_policy_str); + if (ret < 0) { + log_error("Cannot parse 'sgx.file_check_policy'"); + return -EINVAL; + } + + if (!file_check_policy_str) + return 0; + + if (!strcmp(file_check_policy_str, "strict")) { + g_file_check_policy = FILE_CHECK_POLICY_STRICT; + } else if (!strcmp(file_check_policy_str, "allow_all_but_log")) { + g_file_check_policy = FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG; + } else { + log_error("Unknown value for 'sgx.file_check_policy' " + "(allowed: `strict`, `allow_all_but_log`)'"); + free(file_check_policy_str); + return -EINVAL; + } + + log_debug("File check policy: %s", file_check_policy_str); + free(file_check_policy_str); + return 0; +} diff --git a/libos/src/fs/chroot/fs.c b/libos/src/fs/chroot/fs.c index 6b981225f5..dcf8d727b8 100644 --- a/libos/src/fs/chroot/fs.c +++ b/libos/src/fs/chroot/fs.c @@ -7,6 +7,9 @@ /* * This file contains code for implementation of 'chroot' filesystem. * + * This filesystem works on `file:` files and `dev:` devices (pseudo-files) on the host. Files can + * be trusted (measured, integrity-protected via hash) or allowed (passthrough, not protected). + * * TODO: reintroduce the file position sync (using libos_fs_sync.h) after the migration to inodes is * finished. */ @@ -36,6 +39,151 @@ */ #define HOST_PERM(perm) ((perm) | PERM_r________) +/* this data is set up only once (at inode creation or restore), so doesn't require locking */ +struct chroot_inode_data { + bool is_trusted; + bool is_allowed; + /* used only if `is_trusted = true`: array of hashes over separate file chunks */ + struct trusted_chunk_hash* chunk_hashes; +}; + +static bool is_allowed_from_inode_data(struct libos_inode* inode) { + if (!inode->data) + die_or_inf_loop(); + assert(inode->data); + return ((struct chroot_inode_data*)inode->data)->is_allowed; +} + +static bool is_trusted_from_inode_data(struct libos_inode* inode) { + assert(inode->data); + return ((struct chroot_inode_data*)inode->data)->is_trusted; +} + +static const char* strip_prefix(const char* uri) { + const char* s = strchr(uri, ':'); + assert(s); + return s + 1; +} + +static int setup_inode_data_created_file(const char* uri, struct libos_inode* inode) { + assert(inode->type == S_IFREG); + + struct chroot_inode_data* data = calloc(1, sizeof(*data)); + if (!data) + return -ENOMEM; + + /* can be only allowed file or unknown file (allowed via file check policy), + * guaranteed to not be a trusted file */ + data->is_allowed = !!get_allowed_file(strip_prefix(uri)); + inode->data = data; + return 0; +} + +static int setup_inode_data_created_dir(struct libos_inode* inode) { + assert(inode->type == S_IFDIR); + + struct chroot_inode_data* data = calloc(1, sizeof(*data)); + if (!data) + return -ENOMEM; + + data->is_allowed = true; /* dirs are always allowed */ + inode->data = data; + return 0; +} + +static int setup_inode_data(mode_t type, const char* uri, size_t file_size, + struct libos_inode* inode) { + struct chroot_inode_data* data = calloc(1, sizeof(*data)); + if (!data) + return -ENOMEM; + + struct allowed_file* af = get_allowed_file(strip_prefix(uri)); + if (af || type == S_IFDIR) { + data->is_allowed = true; + inode->data = data; + return 0; + } + + struct trusted_file* tf = get_trusted_file(strip_prefix(uri)); + if (tf) { + struct trusted_chunk_hash* out_chunk_hashes; + int ret = load_trusted_file(tf, file_size, &out_chunk_hashes); + if (ret < 0) { + free(data); + return ret; + } + data->is_trusted = true; + data->chunk_hashes = out_chunk_hashes; + inode->data = data; + return 0; + } + + /* not an allowed or trusted file, may be still allowed via file check policy */ + inode->data = data; + return 0; +} + +static void chroot_idrop(struct libos_inode* inode) { + assert(locked(&inode->lock)); + if (inode->data) { + struct chroot_inode_data* data = inode->data; + free(data->chunk_hashes); + free(data); + } +} + +struct chroot_checkpoint { + size_t size; + char data[]; +}; + +static int chroot_icheckpoint(struct libos_inode* inode, void** out_data, size_t* out_size) { + assert(locked(&inode->lock)); + assert(inode->data); + + struct chroot_inode_data* idata = inode->data; + size_t chunk_hashes_size = idata->is_trusted ? get_chunk_hashes_size(inode->size) : 0; + + struct chroot_checkpoint* cp; + size_t cp_size = sizeof(*cp) + sizeof(*idata) + chunk_hashes_size; + cp = malloc(cp_size); + if (!cp) + return -ENOMEM; + + cp->size = sizeof(*idata) + chunk_hashes_size; + memcpy(cp->data, idata, sizeof(*idata)); + if (chunk_hashes_size) + memcpy(cp->data + sizeof(*idata), idata->chunk_hashes, chunk_hashes_size); + + *out_data = cp; + *out_size = cp_size; + return 0; +} + +static int chroot_irestore(struct libos_inode* inode, void* data) { + struct chroot_checkpoint* cp = data; + + struct chroot_inode_data* idata = malloc(sizeof(*idata)); + if (!idata) + return -ENOMEM; + + memcpy(idata, cp->data, sizeof(*idata)); + if (idata->is_trusted) { + size_t chunk_hashes_size = cp->size - sizeof(*idata); + idata->chunk_hashes = malloc(chunk_hashes_size); + if (!idata->chunk_hashes) { + free(idata); + return -ENOMEM; + } + memcpy(idata->chunk_hashes, cp->data + sizeof(*idata), chunk_hashes_size); + } else { + idata->chunk_hashes = NULL; + } + + inode->data = idata; + return 0; +} + static int chroot_mount(struct libos_mount_params* params, void** mount_data) { __UNUSED(mount_data); if (!params->uri || (!strstartswith(params->uri, URI_PREFIX_FILE) && @@ -44,31 +192,83 @@ static int chroot_mount(struct libos_mount_params* params, void** mount_data) { return 0; } -static int chroot_setup_dentry(struct libos_dentry* dent, mode_t type, mode_t perm, - file_off_t size) { - assert(locked(&g_dcache_lock)); - assert(!dent->inode); +static int chroot_dentry_uri(struct libos_dentry* dent, mode_t type, char** out_uri) { + assert(dent->mount); + assert(dent->mount->uri); - struct libos_inode* inode = get_new_inode(dent->mount, type, perm); - if (!inode) - return -ENOMEM; - inode->size = size; - dent->inode = inode; - return 0; + int ret; + + const char* root = strip_prefix(dent->mount->uri); + + const char* prefix; + size_t prefix_len; + switch (type) { + case S_IFREG: + prefix = URI_PREFIX_FILE; + prefix_len = static_strlen(URI_PREFIX_FILE); + break; + case S_IFDIR: + prefix = URI_PREFIX_DIR; + prefix_len = static_strlen(URI_PREFIX_DIR); + break; + case S_IFCHR: + prefix = URI_PREFIX_DEV; + prefix_len = static_strlen(URI_PREFIX_DEV); + break; + default: + BUG(); + } + + char* rel_path; + size_t rel_path_size; + ret = dentry_rel_path(dent, &rel_path, &rel_path_size); + if (ret < 0) + return ret; + + /* Treat empty path as "." */ + if (*root == '\0') + root = "."; + + size_t root_len = strlen(root); + + /* Allocate buffer for "/" (if `rel_path` is empty, we don't need the + * space for `/`, but overallocating 1 byte doesn't hurt us, and keeps the code simple) */ + char* uri = malloc(prefix_len + root_len + 1 + rel_path_size); + if (!uri) { + ret = -ENOMEM; + goto out; + } + memcpy(uri, prefix, prefix_len); + memcpy(uri + prefix_len, root, root_len); + if (rel_path_size == 1) { + /* this is the mount root, the URI is ""*/ + uri[prefix_len + root_len] = '\0'; + } else { + /* this is not the mount root, the URI is "/" */ + uri[prefix_len + root_len] = '/'; + memcpy(uri + prefix_len + root_len + 1, rel_path, rel_path_size); + } + *out_uri = uri; + ret = 0; + +out: + free(rel_path); + return ret; } static int chroot_lookup(struct libos_dentry* dent) { assert(locked(&g_dcache_lock)); int ret; + struct libos_inode* inode = NULL; + char* uri = NULL; /* * We don't know the file type yet, so we can't construct a PAL URI with the right prefix. In * most cases, a "file:" prefix is good enough: `PalStreamAttributesQuery` will access the file * and report the right file type. */ - char* uri = NULL; - ret = dentry_uri(dent, S_IFREG, &uri); + ret = chroot_dentry_uri(dent, S_IFREG, &uri); if (ret < 0) goto out; @@ -102,15 +302,55 @@ static int chroot_lookup(struct libos_dentry* dent) { } mode_t perm = pal_attr.share_flags; + size_t file_size = (type == S_IFREG ? pal_attr.pending_size : 0); - file_off_t size = (type == S_IFREG ? pal_attr.pending_size : 0); + inode = get_new_inode(dent->mount, type, perm); + if (!inode) { + ret = -ENOMEM; + goto out; + } - ret = chroot_setup_dentry(dent, type, perm, size); + ret = setup_inode_data(type, uri, file_size, inode); + if (ret < 0) + goto out; + + inode->size = file_size; + dent->inode = inode; + ret = 0; out: + if (ret < 0) + free(inode); free(uri); return ret; } +static bool is_open_allowed(struct libos_dentry* dent, enum pal_access access) { + if (dent->inode->type == S_IFDIR) { + /* directories have no protections, always allowed to be opened */ + return true; + } + assert(dent->inode->type == S_IFREG || dent->inode->type == S_IFCHR); + + if (is_allowed_from_inode_data(dent->inode)) + return true; + + if (is_trusted_from_inode_data(dent->inode)) { + if (access == PAL_ACCESS_RDWR || access == PAL_ACCESS_WRONLY) { + log_error("Disallowing write/append to a trusted file '%s'", dent->name); + return false; + } + return true; + } + + if (g_file_check_policy != FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG) { + log_warning("Disallowing access to file '%s'; file is not allowed.", dent->name); + return false; + } + + log_warning("Allowing access to unknown file '%s' due to file_check_policy.", dent->name); + return true; +} + /* Open a temporary read-only PAL handle for a file (used by `unlink` etc.) */ static int chroot_temp_open(struct libos_dentry* dent, PAL_HANDLE* out_palhdl) { char* uri; @@ -118,10 +358,22 @@ static int chroot_temp_open(struct libos_dentry* dent, PAL_HANDLE* out_palhdl) { if (ret < 0) return ret; + if (!is_open_allowed(dent, PAL_ACCESS_RDONLY)) { + ret = -EACCES; + goto out; + } + ret = PalStreamOpen(uri, PAL_ACCESS_RDONLY, /*share_flags=*/0, PAL_CREATE_NEVER, /*options=*/0, out_palhdl); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + ret = 0; +out: free(uri); - return pal_to_unix_errno(ret); + return ret; } /* Open a PAL handle, and associate it with a LibOS handle (if provided). */ @@ -169,22 +421,76 @@ static int chroot_open(struct libos_handle* hdl, struct libos_dentry* dent, int assert(locked(&g_dcache_lock)); assert(dent->inode); + if (!is_open_allowed(dent, LINUX_OPEN_FLAGS_TO_PAL_ACCESS(flags))) + return -EACCES; + return chroot_do_open(hdl, dent, dent->inode->type, flags, /*perm=*/0); } -static int chroot_creat(struct libos_handle* hdl, struct libos_dentry* dent, int flags, mode_t perm) { +static bool is_create_allowed(const char* uri) { + assert(strstartswith(uri, URI_PREFIX_FILE)); + const char* path = strip_prefix(uri); + + if (get_allowed_file(path)) + return true; + + if (get_trusted_file(path)) { + log_error("Disallowing creating a trusted file '%s'", path); + return false; + } + + if (g_file_check_policy != FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG) { + log_warning("Disallowing creating file '%s'; file is not trusted or allowed.", path); + return false; + } + + log_warning("Allowing creating unknown file '%s' due to file_check_policy.", path); + return true; +} + +static int chroot_creat(struct libos_handle* hdl, struct libos_dentry* dent, int flags, + mode_t perm) { assert(locked(&g_dcache_lock)); assert(!dent->inode); int ret; + struct libos_inode* inode = NULL; + char* uri = NULL; - mode_t type = S_IFREG; + ret = chroot_dentry_uri(dent, S_IFREG, &uri); + if (ret < 0) + goto out; + + if (!is_create_allowed(uri)) { + ret = -EACCES; + goto out; + } - ret = chroot_do_open(hdl, dent, type, flags | O_CREAT | O_EXCL, perm); + ret = chroot_do_open(hdl, dent, S_IFREG, flags | O_CREAT | O_EXCL, perm); if (ret < 0) - return ret; + goto out; - return chroot_setup_dentry(dent, type, perm, /*size=*/0); + ret = register_allowed_file(strip_prefix(uri)); + if (ret < 0) + goto out; + + inode = get_new_inode(dent->mount, S_IFREG, perm); + if (!inode) { + ret = -ENOMEM; + goto out; + } + + ret = setup_inode_data_created_file(uri, inode); + if (ret < 0) + goto out; + + dent->inode = inode; + ret = 0; +out: + if (ret < 0) + free(inode); + free(uri); + return ret; } static int chroot_mkdir(struct libos_dentry* dent, mode_t perm) { @@ -192,14 +498,28 @@ static int chroot_mkdir(struct libos_dentry* dent, mode_t perm) { assert(!dent->inode); int ret; + struct libos_inode* inode = NULL; - mode_t type = S_IFDIR; + ret = chroot_do_open(/*hdl=*/NULL, dent, S_IFDIR, O_CREAT | O_EXCL, perm); + if (ret < 0) + goto out; - ret = chroot_do_open(/*hdl=*/NULL, dent, type, O_CREAT | O_EXCL, perm); + inode = get_new_inode(dent->mount, S_IFDIR, perm); + if (!inode) { + ret = -ENOMEM; + goto out; + } + + ret = setup_inode_data_created_dir(inode); if (ret < 0) - return ret; + goto out; - return chroot_setup_dentry(dent, type, perm, /*size=*/0); + dent->inode = inode; + ret = 0; +out: + if (ret < 0) + free(inode); + return ret; } static int chroot_flush(struct libos_handle* hdl) { @@ -212,11 +532,23 @@ static int chroot_flush(struct libos_handle* hdl) { static ssize_t chroot_read(struct libos_handle* hdl, void* buf, size_t count, file_off_t* pos) { assert(hdl->type == TYPE_CHROOT); + int ret; size_t actual_count = count; - int ret = PalStreamRead(hdl->pal_handle, *pos, &actual_count, buf); - if (ret < 0) { - return pal_to_unix_errno(ret); + uint64_t offset = *pos; + + if (is_trusted_from_inode_data(hdl->inode)) { + struct chroot_inode_data* data = hdl->inode->data; + ret = copy_and_verify_trusted_file(hdl->pal_handle, offset, actual_count, buf, + hdl->inode->size, data->chunk_hashes); + if (ret < 0) + return ret; + actual_count = MIN(offset + count, (size_t)hdl->inode->size) - offset; + } else { + ret = PalStreamRead(hdl->pal_handle, offset, &actual_count, buf); + if (ret < 0) + return pal_to_unix_errno(ret); } + assert(actual_count <= count); if (hdl->inode->type == S_IFREG) { *pos += actual_count; @@ -228,6 +560,11 @@ static ssize_t chroot_write(struct libos_handle* hdl, const void* buf, size_t co file_off_t* pos) { assert(hdl->type == TYPE_CHROOT); + if (is_trusted_from_inode_data(hdl->inode)) { + log_warning("Writing to a trusted file is disallowed!"); + return -EACCES; + } + size_t actual_count = count; int ret = PalStreamWrite(hdl->pal_handle, *pos, &actual_count, (void*)buf); if (ret < 0) { @@ -410,15 +747,18 @@ struct libos_fs_ops chroot_fs_ops = { }; struct libos_d_ops chroot_d_ops = { - .open = &chroot_open, - .lookup = &chroot_lookup, - .creat = &chroot_creat, - .mkdir = &chroot_mkdir, - .stat = &generic_inode_stat, - .readdir = &chroot_readdir, - .unlink = &chroot_unlink, - .rename = &chroot_rename, - .chmod = &chroot_chmod, + .open = &chroot_open, + .lookup = &chroot_lookup, + .creat = &chroot_creat, + .mkdir = &chroot_mkdir, + .stat = &generic_inode_stat, + .readdir = &chroot_readdir, + .unlink = &chroot_unlink, + .rename = &chroot_rename, + .chmod = &chroot_chmod, + .idrop = &chroot_idrop, + .icheckpoint = &chroot_icheckpoint, + .irestore = &chroot_irestore, }; struct libos_fs chroot_builtin_fs = { diff --git a/libos/src/fs/chroot/trusted.c b/libos/src/fs/chroot/trusted.c new file mode 100644 index 0000000000..2f5f376b78 --- /dev/null +++ b/libos/src/fs/chroot/trusted.c @@ -0,0 +1,437 @@ +/* Copyright (C) 2024 Intel Corporation + * Dmitrii Kuvaiskii + */ + +/* + * This file contains code for trusted files in 'chroot' filesystem. + * + * Trusted files (TF) are integrity protected and transparently verified when accessed by Gramine + * or by app running inside Gramine. 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, Gramine + * loads the whole file, calculates its SHA256 hash, and checks against the corresponding hash in + * the manifest. If the hashes do not match, the file access will be rejected. + * + * During the generation of the SHA256 hash, a 128-bit hash (truncated SHA256) is also generated for + * each chunk (of size TRUSTED_CHUNK_SIZE) in the file. The per-chunk hashes are used for partial + * verification in future reads, to avoid re-verifying the whole file again or the need of caching + * file contents. + */ + +#include +#include + +#include "api.h" +#include "crypto.h" +#include "hex.h" +#include "libos_fs.h" +#include "list.h" +#include "path_utils.h" +#include "toml.h" + +/* FIXME: current size is 16KB, but maybe there's a better size for perf/mem trade-off? */ +#define TRUSTED_CHUNK_SIZE (PAGE_SIZE * 4UL) + +/* FIXME: use hash table instead of list */ +DEFINE_LIST(trusted_file); +struct trusted_file { + LIST_TYPE(trusted_file) list; + struct trusted_file_hash file_hash; /* hash over file, retrieved from the manifest */ + size_t path_len; + char path[]; /* must be NULL-terminated */ +}; + +/* initialized once at startup, so doesn't require locking */ +DEFINE_LISTP(trusted_file); +static LISTP_TYPE(trusted_file) g_trusted_file_list = LISTP_INIT; + +static int read_trusted_file(PAL_HANDLE handle, void* buffer, uint64_t offset, size_t size) { + size_t buffer_offset = 0; + size_t remaining = size; + + while (remaining > 0) { + size_t count = remaining; + int ret = PalStreamRead(handle, offset + buffer_offset, &count, buffer + buffer_offset); + if (ret < 0) { + if (ret == -PAL_ERROR_INTERRUPTED || ret == -PAL_ERROR_TRYAGAIN) { + continue; + } + return pal_to_unix_errno(ret); + } else if (count == 0) { + return -ENODATA; + } + + assert(count <= remaining); + remaining -= count; + buffer_offset += count; + } + return 0; +} + +struct trusted_file* get_trusted_file(const char* path) { + size_t norm_path_size = strlen(path) + 1; /* overapproximate */ + char* norm_path = malloc(norm_path_size); + if (!norm_path) + return NULL; + + bool normalized = get_norm_path(path, norm_path, &norm_path_size); + if (!normalized) { + free(norm_path); + return NULL; + } + + struct trusted_file* tf = NULL; + struct trusted_file* tmp; + LISTP_FOR_EACH_ENTRY(tmp, &g_trusted_file_list, list) { + if ((tmp->path_len == norm_path_size - 1) + && !memcmp(tmp->path, norm_path, norm_path_size)) { + tf = tmp; + break; + } + } + free(norm_path); + return tf; +} + +size_t get_chunk_hashes_size(size_t file_size) { + return (sizeof(struct trusted_chunk_hash) * UDIV_ROUND_UP(file_size, TRUSTED_CHUNK_SIZE)); +} + +/* calculate chunk hashes and compare with hash in manifest */ +int load_trusted_file(struct trusted_file* tf, size_t file_size, + struct trusted_chunk_hash** out_chunk_hashes) { + int ret; + uint8_t* tmp_chunk = NULL; + struct trusted_chunk_hash* chunk_hashes = NULL; + PAL_HANDLE handle = NULL; + + char* uri = malloc(URI_PREFIX_FILE_LEN + tf->path_len + 1); + if (!uri) { + ret = -ENOMEM; + goto out; + } + memcpy(uri, URI_PREFIX_FILE, URI_PREFIX_FILE_LEN); + memcpy(uri + URI_PREFIX_FILE_LEN, tf->path, tf->path_len); + uri[URI_PREFIX_FILE_LEN + tf->path_len] = '\0'; + + chunk_hashes = malloc(get_chunk_hashes_size(file_size)); + if (!chunk_hashes) { + ret = -ENOMEM; + goto out; + } + + /* FIXME: use pre-allocated object in common case (e.g. for the first thread) */ + tmp_chunk = malloc(TRUSTED_CHUNK_SIZE); + if (!tmp_chunk) { + ret = -ENOMEM; + goto out; + } + + ret = PalStreamOpen(uri, PAL_ACCESS_RDONLY, /*share_flags=*/0, PAL_CREATE_NEVER, + /*options=*/0, &handle); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + LIB_SHA256_CONTEXT file_sha; + ret = lib_SHA256Init(&file_sha); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + struct trusted_chunk_hash* chunk_hashes_item = chunk_hashes; + for (uint64_t offset = 0; offset < file_size; offset += TRUSTED_CHUNK_SIZE) { + /* For each file chunk of size TRUSTED_CHUNK_SIZE, generate 128-bit hash from SHA-256 hash + * over contents of this file chunk (we simply truncate SHA-256 hash to first 128 bits; this + * is fine for integrity purposes). Also, generate a SHA-256 hash for the whole file + * contents to compare with the manifest "reference" hash value. */ + uint64_t chunk_size = MIN(file_size - offset, TRUSTED_CHUNK_SIZE); + + LIB_SHA256_CONTEXT chunk_sha; + ret = lib_SHA256Init(&chunk_sha); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + ret = read_trusted_file(handle, tmp_chunk, offset, chunk_size); + if (ret < 0) + goto out; + ret = lib_SHA256Update(&file_sha, tmp_chunk, chunk_size); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + ret = lib_SHA256Update(&chunk_sha, tmp_chunk, chunk_size); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + struct trusted_chunk_hash chunk_hash[2]; + static_assert(sizeof(chunk_hash) * 8 == 256, ""); + ret = lib_SHA256Final(&chunk_sha, (uint8_t*)&chunk_hash[0]); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + /* note that we truncate SHA256 to 128 bits */ + memcpy(chunk_hashes_item, &chunk_hash[0], sizeof(*chunk_hashes_item)); + chunk_hashes_item++; + } + + struct trusted_file_hash file_hash; + ret = lib_SHA256Final(&file_sha, file_hash.bytes); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + /* check the generated hash-over-whole-file against the reference hash in the manifest */ + if (memcmp(&file_hash, &tf->file_hash, sizeof(file_hash))) { + log_warning("Hash of trusted file '%s' does not match with the reference hash in manifest", + tf->path); + ret = -EPERM; + goto out; + } + + *out_chunk_hashes = chunk_hashes; + ret = 0; +out: + if (ret < 0) + free(chunk_hashes); + if (handle) + PalObjectDestroy(handle); + free(tmp_chunk); + free(uri); + return ret; +} + +int copy_and_verify_trusted_file(PAL_HANDLE handle, uint64_t offset, size_t count, uint8_t* buf, + size_t file_size, struct trusted_chunk_hash* chunk_hashes) { + int ret; + + if (offset >= file_size) + return 0; + + uint64_t end = MIN(offset + count, file_size); + uint64_t aligned_offset = ALIGN_DOWN(offset, TRUSTED_CHUNK_SIZE); + uint64_t aligned_end = ALIGN_UP(end, TRUSTED_CHUNK_SIZE); + + /* FIXME: use pre-allocated object in common case (e.g. for the first thread) */ + uint8_t* tmp_chunk = malloc(TRUSTED_CHUNK_SIZE); + if (!tmp_chunk) + return -ENOMEM; + + uint8_t* buf_pos = buf; + uint64_t chunk_offset = aligned_offset; + struct trusted_chunk_hash* chunk_hashes_item = chunk_hashes + + aligned_offset / TRUSTED_CHUNK_SIZE; + for (; chunk_offset < aligned_end; chunk_offset += TRUSTED_CHUNK_SIZE) { + size_t chunk_size = MIN(file_size - chunk_offset, TRUSTED_CHUNK_SIZE); + uint64_t chunk_end = chunk_offset + chunk_size; + + LIB_SHA256_CONTEXT chunk_sha; + ret = lib_SHA256Init(&chunk_sha); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + if (chunk_offset >= offset && chunk_end <= end) { + /* if current chunk-to-copy completely resides in the requested region-to-copy, + * directly copy into buf (without a scratch buffer) and hash in-place */ + ret = read_trusted_file(handle, buf_pos, chunk_offset, chunk_size); + if (ret < 0) + goto out; + ret = lib_SHA256Update(&chunk_sha, buf_pos, chunk_size); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + buf_pos += chunk_size; + } else { + /* if current chunk-to-copy only partially overlaps with the requested region-to-copy, + * read the file contents into a scratch buffer, verify hash and then copy only the part + * needed by the caller */ + ret = read_trusted_file(handle, tmp_chunk, chunk_offset, chunk_size); + if (ret < 0) + goto out; + ret = lib_SHA256Update(&chunk_sha, tmp_chunk, chunk_size); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + /* determine which part of the chunk is needed by the caller */ + uint64_t copy_start = MAX(chunk_offset, offset); + uint64_t copy_end = MIN(chunk_offset + chunk_size, end); + assert(copy_end > copy_start); + + memcpy(buf_pos, tmp_chunk + copy_start - chunk_offset, copy_end - copy_start); + buf_pos += copy_end - copy_start; + } + + struct trusted_chunk_hash chunk_hash[2]; /* each chunk_hash is 128 bits in size */ + static_assert(sizeof(chunk_hash) * 8 == 256, ""); + ret = lib_SHA256Final(&chunk_sha, (uint8_t*)&chunk_hash[0]); + if (ret < 0) { + ret = pal_to_unix_errno(ret); + goto out; + } + + if (memcmp(chunk_hashes_item, &chunk_hash[0], sizeof(*chunk_hashes_item))) { + ret = -EPERM; + goto out; + } + + chunk_hashes_item++; + } + + ret = 0; +out: + free(tmp_chunk); + return ret; +} + +static int register_trusted_file(const char* path, const char* hash_str) { + if (strlen(hash_str) != sizeof(struct trusted_file_hash) * 2) { + log_error("Hash (%s) of a trusted file %s is not a SHA256 hash", hash_str, path); + return -EINVAL; + } + + size_t path_len = strlen(path); + if (path_len >= URI_MAX) { + log_error("Size of file exceeds maximum %dB: %s", URI_MAX, path); + return -EINVAL; + } + + struct trusted_file_hash file_hash; + char* bytes = hex2bytes(hash_str, strlen(hash_str), file_hash.bytes, sizeof(file_hash.bytes)); + if (!bytes) { + log_error("Could not parse hash of trusted file: %s", path); + return -EINVAL; + } + + struct trusted_file* new = malloc(sizeof(*new) + path_len + 1); + if (!new) + return -ENOMEM; + + INIT_LIST_HEAD(new, list); + new->path_len = path_len; + memcpy(new->path, path, path_len + 1); + memcpy(&new->file_hash, &file_hash, sizeof(file_hash)); + + LISTP_ADD_TAIL(new, &g_trusted_file_list, list); + return 0; +} + +static int init_one_trusted_file(toml_raw_t toml_trusted_uri_raw, + toml_raw_t toml_trusted_sha256_raw, size_t idx) { + int ret; + + /* FIXME: toml_trusted_uri_str and toml_trusted_sha256_str are temporary strings, allocating + * them is redundant; however tomlc99 lib has only toml_rtos() function that returns a + * newly allocated string rather than a slice into the parsed TOML structure */ + char* toml_trusted_uri_str = NULL; + char* toml_trusted_sha256_str = NULL; + + /* FIXME: instead of re-allocating in register_trusted_file(), could pass ownership to it */ + char* norm_trusted_path = NULL; + + ret = toml_rtos(toml_trusted_uri_raw, &toml_trusted_uri_str); + if (ret < 0) { + log_error("Invalid trusted file in manifest at index %ld ('uri' is not a string)", idx); + ret = -EINVAL; + goto out; + } + + ret = toml_rtos(toml_trusted_sha256_raw, &toml_trusted_sha256_str); + if (ret < 0 || !toml_trusted_sha256_str) { + log_error("Invalid trusted file in manifest at index %ld ('sha256' is not a string)", idx); + ret = -EINVAL; + goto out; + } + + if (!strstartswith(toml_trusted_uri_str, URI_PREFIX_FILE)) { + log_error("Invalid URI [%s]: Trusted files must start with 'file:'", toml_trusted_uri_str); + ret = -EINVAL; + goto out; + } + + size_t norm_trusted_path_size = strlen(toml_trusted_uri_str) - URI_PREFIX_FILE_LEN + 1; + norm_trusted_path = malloc(norm_trusted_path_size); + if (!norm_trusted_path) { + ret = -ENOMEM; + goto out; + } + + bool normalized = get_norm_path(toml_trusted_uri_str + URI_PREFIX_FILE_LEN, + norm_trusted_path, &norm_trusted_path_size); + if (!normalized) { + log_error("Trusted file path (%s) normalization failed", toml_trusted_uri_str); + ret = -EINVAL; + goto out; + } + + ret = register_trusted_file(norm_trusted_path, toml_trusted_sha256_str); + if (ret < 0) { + log_error("Trusted file registration (%s) failed", toml_trusted_uri_str); + goto out; + } + + ret = 0; +out: + free(norm_trusted_path); + free(toml_trusted_uri_str); + free(toml_trusted_sha256_str); + return ret; +} + +int init_trusted_files(void) { + int ret; + + assert(g_manifest_root); + toml_table_t* manifest_sgx = toml_table_in(g_manifest_root, "sgx"); + if (!manifest_sgx) + return 0; + + toml_array_t* toml_trusted_files = toml_array_in(manifest_sgx, "trusted_files"); + if (!toml_trusted_files) + return 0; + + ssize_t toml_trusted_files_cnt = toml_array_nelem(toml_trusted_files); + if (toml_trusted_files_cnt < 0) + return -EPERM; + if (toml_trusted_files_cnt == 0) + return 0; + + for (size_t i = 0; i < (size_t)toml_trusted_files_cnt; i++) { + /* read `sgx.trusted_file = {uri = "file:foo", sha256 = "deadbeef"}` entry from manifest */ + toml_table_t* toml_trusted_file = toml_table_at(toml_trusted_files, i); + if (!toml_trusted_file) { + log_error("Invalid trusted file in manifest at index %ld (not a TOML table)", i); + return -EINVAL; + } + + toml_raw_t toml_trusted_uri_raw = toml_raw_in(toml_trusted_file, "uri"); + if (!toml_trusted_uri_raw) { + log_error("Invalid trusted file in manifest at index %ld (no 'uri' key)", i); + return -EINVAL; + } + + toml_raw_t toml_trusted_sha256_raw = toml_raw_in(toml_trusted_file, "sha256"); + if (!toml_trusted_sha256_raw) { + log_error("Invalid trusted file in manifest at index %ld (no 'sha256' key)", i); + return -EINVAL; + } + + ret = init_one_trusted_file(toml_trusted_uri_raw, toml_trusted_sha256_raw, i); + if (ret < 0) + return ret; + } + + return 0; +} diff --git a/libos/src/fs/libos_fs.c b/libos/src/fs/libos_fs.c index 5a29a36d6d..22022391df 100644 --- a/libos/src/fs/libos_fs.c +++ b/libos/src/fs/libos_fs.c @@ -93,6 +93,19 @@ int init_fs(void) { return ret; } +int init_trusted_allowed_files(void) { + int ret; + + if ((ret = init_file_check_policy()) < 0) + return ret; + if ((ret = init_allowed_files()) < 0) + return ret; + if ((ret = init_trusted_files()) < 0) + return ret; + + return 0; +} + static struct libos_mount* alloc_mount(void) { return get_mem_obj_from_mgr_enlarge(g_mount_mgr, size_align_up(MOUNT_MGR_ALLOC)); } diff --git a/libos/src/fs/libos_fs_encrypted.c b/libos/src/fs/libos_fs_encrypted.c index 15f73725da..91e8220bec 100644 --- a/libos/src/fs/libos_fs_encrypted.c +++ b/libos/src/fs/libos_fs_encrypted.c @@ -169,7 +169,7 @@ static int encrypted_file_internal_open(struct libos_encrypted_file* enc, PAL_HA if (!pal_handle) { enum pal_create_mode create_mode = create ? PAL_CREATE_ALWAYS : PAL_CREATE_NEVER; ret = PalStreamOpen(enc->uri, PAL_ACCESS_RDWR, share_flags, create_mode, - PAL_OPTION_PASSTHROUGH, &pal_handle); + /*options=*/0, &pal_handle); if (ret < 0) { log_warning("PalStreamOpen failed: %s", pal_strerror(ret)); return pal_to_unix_errno(ret); diff --git a/libos/src/libos_init.c b/libos/src/libos_init.c index 19e47635c2..cfb6fb602b 100644 --- a/libos/src/libos_init.c +++ b/libos/src/libos_init.c @@ -428,6 +428,16 @@ noreturn void libos_init(const char* const* argv, const char* const* envp) { g_process_ipc_ids.self_vmid = STARTING_VMID; } + /* + * Must be after receiving the checkpoint (if in child process) and before initializing the + * mount points. The former is because trusted/allowed files' lists are allocated from heap as + * "internal VMAs" in potentially large sizes, but early LibOS init code (before receiving the + * checkpoint) is limited in its size. See libos_vma.c:bkeep_mmap_any_in_range(). The latter is + * because mount points can be separate files (e.g., the main executable), and their + * meta-information (including trusted/allowed info) is initialized during mounting. + */ + RUN_INIT(init_trusted_allowed_files); + RUN_INIT(init_ipc); RUN_INIT(init_process); RUN_INIT(init_threading); diff --git a/libos/src/meson.build b/libos/src/meson.build index b9946bc2af..dec07cc21c 100644 --- a/libos/src/meson.build +++ b/libos/src/meson.build @@ -15,8 +15,11 @@ libos_sources = files( 'bookkeep/libos_signal.c', 'bookkeep/libos_thread.c', 'bookkeep/libos_vma.c', + 'fs/chroot/allowed.c', 'fs/chroot/encrypted.c', + 'fs/chroot/file_check_policy.c', 'fs/chroot/fs.c', + 'fs/chroot/trusted.c', 'fs/dev/attestation.c', 'fs/dev/fs.c', 'fs/etc/fs.c', diff --git a/libos/test/ltp/ltp.cfg b/libos/test/ltp/ltp.cfg index ebc04378ec..26e488f786 100644 --- a/libos/test/ltp/ltp.cfg +++ b/libos/test/ltp/ltp.cfg @@ -249,7 +249,7 @@ skip = yes # very long test, does thousands of forks [epoll01] -timeout = 300 +timeout = 600 # tries to open /proc/1/stat, which is not implemented in Gramine [epoll_pwait01] diff --git a/libos/test/ltp/manifest.template b/libos/test/ltp/manifest.template index ea76c87326..40d17ea3b4 100644 --- a/libos/test/ltp/manifest.template +++ b/libos/test/ltp/manifest.template @@ -31,18 +31,21 @@ sgx.edmm_enable = {{ 'true' if env.get('EDMM', '0') == '1' else 'false' }} sgx.use_exinfo = true sgx.allowed_files = [ + "file:/etc", "file:/tmp", + "file:/usr", + "dev:/dev/shm/", # for tests that rely on shared-memory IPC, see note above + + "file:install/testcases/bin/execl01_child", # for execl01 test + "file:install/testcases/bin/execlp01_child", # for execlp01 test + "file:install/testcases/bin/execv01_child", # for execv01 test + "file:install/testcases/bin/execvp01_child", # for execvp01 test ] sgx.trusted_files = [ "file:{{ binary_dir }}/{{ entrypoint }}", - "file:{{ gramine.runtimedir() }}/ld-linux-x86-64.so.2", - "file:{{ gramine.runtimedir() }}/libc.so.6", - "file:{{ gramine.runtimedir() }}/libdl.so.2", - "file:{{ gramine.runtimedir() }}/libm.so.6", - "file:{{ gramine.runtimedir() }}/libpthread.so.0", - "file:{{ gramine.runtimedir() }}/librt.so.1", + "file:{{ gramine.runtimedir() }}/", "file:{{ coreutils_libdir }}/libstdbuf.so", ] diff --git a/libos/test/regression/fork_and_access_file.c b/libos/test/regression/fork_and_access_file.c index 8b711e2a32..665b56cf39 100644 --- a/libos/test/regression/fork_and_access_file.c +++ b/libos/test/regression/fork_and_access_file.c @@ -19,6 +19,10 @@ char g_parent_buf[MAX_BUF_SIZE]; char g_child_buf[MAX_BUF_SIZE]; +static __attribute__ ((noinline)) void die_on_wrong_file_contents(void) { + errx(1, "child detected incorrect contents in test file"); +} + int main(void) { int fd = CHECK(open(FILENAME, O_RDONLY)); @@ -27,7 +31,12 @@ int main(void) { pid_t p = CHECK(fork()); if (p == 0) { - ssize_t child_read_ret = CHECK(posix_fd_read(fd, g_child_buf, sizeof(g_child_buf))); + ssize_t child_read_ret = posix_fd_read(fd, g_child_buf, sizeof(g_child_buf)); + if (child_read_ret < 0) { + if (errno == EPERM) + die_on_wrong_file_contents(); + errx(1, "child read failed with an unexpected error %d", errno); + } if (child_read_ret != parent_read_ret || memcmp(g_child_buf, g_parent_buf, child_read_ret)) { errx(1, "child read data different from what parent read"); diff --git a/libos/test/regression/fork_and_access_file.gdb b/libos/test/regression/fork_and_access_file.gdb index 93e58bdff0..e997972171 100644 --- a/libos/test/regression/fork_and_access_file.gdb +++ b/libos/test/regression/fork_and_access_file.gdb @@ -14,15 +14,15 @@ commands shell echo "WRITING NEW CONTENT IN FORK_AND_ACCESS_FILE_TESTFILE" > fork_and_access_file_testfile - tbreak die_or_inf_loop + tbreak die_on_wrong_file_contents commands - echo EXITING GDB WITH A GRAMINE ERROR\n + echo EXITING GDB WITH AN ERROR\n quit end tbreak exit commands - echo EXITING GDB WITHOUT A GRAMINE ERROR\n + echo EXITING GDB WITHOUT AN ERROR\n quit end diff --git a/libos/test/regression/test_libos.py b/libos/test/regression/test_libos.py index 55ed2c56be..c09deb47b1 100644 --- a/libos/test/regression/test_libos.py +++ b/libos/test/regression/test_libos.py @@ -483,9 +483,6 @@ def test_000_simple_for_loop(self): # OpenMP simple for loop self.assertIn('first: 0, last: 9', stdout) -@unittest.skipUnless(HAS_SGX, - 'This test is only meaningful on SGX PAL because file-check-policy is ' - 'only relevant to SGX.') class TC_03_FileCheckPolicy(RegressionTestCase): @classmethod def setUpClass(cls): @@ -519,7 +516,7 @@ def test_002_strict_fail_create(self): except subprocess.CalledProcessError as e: self.assertEqual(e.returncode, 2) stderr = e.stderr.decode() - self.assertIn('Disallowing access to file \'nonexisting_testfile\'', stderr) + self.assertIn('Disallowing creating file \'./nonexisting_testfile\'', stderr) if os.path.exists('nonexisting_testfile'): self.fail('test created a file unexpectedly') @@ -531,14 +528,13 @@ def test_003_strict_fail_write(self): except subprocess.CalledProcessError as e: self.assertEqual(e.returncode, 2) stderr = e.stderr.decode() - self.assertIn('Disallowing create/write/append to a trusted file \'trusted_testfile\'', - stderr) + self.assertIn('Disallowing write/append to a trusted file \'trusted_testfile\'', stderr) def test_004_allow_all_but_log_unknown(self): stdout, stderr = self.run_binary(['file_check_policy_allow_all_but_log', 'read', 'unknown_testfile']) self.assertIn('Allowing access to unknown file \'unknown_testfile\' due to ' - 'file_check_policy settings.', stderr) + 'file_check_policy', stderr) self.assertIn('file_check_policy succeeded', stdout) def test_005_allow_all_but_log_trusted(self): @@ -556,8 +552,7 @@ def test_006_allow_all_but_log_trusted_create_fail(self): except subprocess.CalledProcessError as e: self.assertEqual(e.returncode, 2) stderr = e.stderr.decode() - self.assertIn('Disallowing create/write/append to a trusted file \'trusted_testfile\'', - stderr) + self.assertIn('Disallowing write/append to a trusted file \'trusted_testfile\'', stderr) def test_007_allow_all_but_log_unknown_create(self): if os.path.exists('nonexisting_testfile'): @@ -565,8 +560,8 @@ def test_007_allow_all_but_log_unknown_create(self): try: stdout, stderr = self.run_binary(['file_check_policy_allow_all_but_log', 'append', 'nonexisting_testfile']) - self.assertIn('Allowing access to unknown file \'nonexisting_testfile\' due to ' - 'file_check_policy settings.', stderr) + self.assertIn('Allowing creating unknown file \'./nonexisting_testfile\' due to ' + 'file_check_policy', stderr) self.assertIn('file_check_policy succeeded', stdout) if not os.path.exists('nonexisting_testfile'): self.fail('test did not create a file') @@ -844,8 +839,6 @@ def test_051_mmap_file_sigbus_child(self): self.assertIn('CHILD OK', stdout) self.assertIn('TEST OK', stdout) - @unittest.skipUnless(HAS_SGX, - 'Trusted files are only available with SGX') def test_052_mmap_file_backed_trusted(self): stdout, _ = self.run_binary(['mmap_file_backed', 'mmap_file_backed'], timeout=60) self.assertIn('Child process done', stdout) @@ -1420,7 +1413,6 @@ def test_010_regs_x86_64(self): xmm0_result = self.find('XMM0 result', stdout) self.assertEqual(xmm0_result, '$4 = 0x4000400040004000') - @unittest.skipUnless(HAS_SGX, 'Trusted files bug was SGX-specific') def test_020_gdb_fork_and_access_file_bug(self): # To run this test manually, use: # GDB=1 GDB_SCRIPT=fork_and_access_file.gdb gramine-sgx fork_and_access_file @@ -1433,9 +1425,9 @@ def test_020_gdb_fork_and_access_file_bug(self): try: stdout, _ = self.run_gdb(['fork_and_access_file'], 'fork_and_access_file.gdb') self.assertIn('BREAK ON FORK', stdout) - self.assertIn('EXITING GDB WITH A GRAMINE ERROR', stdout) + self.assertIn('EXITING GDB WITH AN ERROR', stdout) # below message must NOT be printed; it means Gramine didn't fail but the program itself - self.assertNotIn('EXITING GDB WITHOUT A GRAMINE ERROR', stdout) + self.assertNotIn('EXITING GDB WITHOUT AN ERROR', stdout) # below message from program must NOT be printed; Gramine must fail before it self.assertNotIn('child read data different from what parent read', stdout) finally: diff --git a/pal/include/host/linux-common/pal_flags_conv.h b/pal/include/host/linux-common/pal_flags_conv.h index 6cf6d0a3a1..bbed47cbe2 100644 --- a/pal/include/host/linux-common/pal_flags_conv.h +++ b/pal/include/host/linux-common/pal_flags_conv.h @@ -60,6 +60,6 @@ static inline int PAL_CREATE_TO_LINUX_OPEN(enum pal_create_mode create) { } static inline int PAL_OPTION_TO_LINUX_OPEN(pal_stream_options_t options) { - assert(WITHIN_MASK(options, PAL_OPTION_NONBLOCK | PAL_OPTION_PASSTHROUGH)); + assert(WITHIN_MASK(options, PAL_OPTION_NONBLOCK)); return options & PAL_OPTION_NONBLOCK ? O_NONBLOCK : 0; } diff --git a/pal/include/pal/pal.h b/pal/include/pal/pal.h index 9aef051ab1..cd8e213ae0 100644 --- a/pal/include/pal/pal.h +++ b/pal/include/pal/pal.h @@ -325,8 +325,7 @@ enum pal_create_mode { typedef uint32_t pal_stream_options_t; /* bitfield */ #define PAL_OPTION_EFD_SEMAPHORE 0x1 /*!< specific to `eventfd` syscall */ #define PAL_OPTION_NONBLOCK 0x2 -#define PAL_OPTION_PASSTHROUGH 0x4 /*!< Disregard `sgx.{allowed,trusted}_files` */ -#define PAL_OPTION_MASK 0x7 +#define PAL_OPTION_MASK 0x3 /*! * \brief Open/create a stream resource specified by `uri`. diff --git a/pal/src/host/linux-sgx/enclave_framework.c b/pal/src/host/linux-sgx/enclave_framework.c index 35385358c7..72adb4f32d 100644 --- a/pal/src/host/linux-sgx/enclave_framework.c +++ b/pal/src/host/linux-sgx/enclave_framework.c @@ -2,7 +2,6 @@ #include "api.h" #include "crypto.h" -#include "enclave_tf.h" #include "hex.h" #include "list.h" #include "pal_error.h" @@ -18,11 +17,8 @@ #define LOCAL_ATTESTATION_TAG_PARENT_STR "GRAMINE_LOCAL_ATTESTATION_TAG_PARENT" #define LOCAL_ATTESTATION_TAG_CHILD_STR "GRAMINE_LOCAL_ATTESTATION_TAG_CHILD" -static int register_file(const char* uri, const char* hash_str, bool check_duplicates); - uintptr_t g_enclave_base; uintptr_t g_enclave_top; -bool g_allowed_files_warn = false; /* * SGX's EGETKEY(SEAL_KEY) uses three masks as key-derivation material: @@ -400,599 +396,6 @@ int sgx_get_seal_key(uint16_t key_policy, sgx_key_128bit_t* out_seal_key) { return 0; } -DEFINE_LISTP(trusted_file); -static LISTP_TYPE(trusted_file) g_trusted_file_list = LISTP_INIT; -static spinlock_t g_trusted_file_lock = INIT_SPINLOCK_UNLOCKED; -static int g_file_check_policy = FILE_CHECK_POLICY_STRICT; - -static void find_path_in_uri(const char* uri, size_t uri_len, const char** out_path, - size_t* out_path_len) { - if (strstartswith(uri, URI_PREFIX_FILE)) { - *out_path = uri + URI_PREFIX_FILE_LEN; - *out_path_len = uri_len - URI_PREFIX_FILE_LEN; - return; - } - - assert(strstartswith(uri, URI_PREFIX_DEV)); - *out_path = uri + URI_PREFIX_DEV_LEN; - *out_path_len = uri_len - URI_PREFIX_DEV_LEN; -} - -/* assumes `path` is normalized */ -static bool path_is_equal_or_subpath(const struct trusted_file* tf, const char* path, - size_t path_len) { - const char* tf_path; - size_t tf_path_len; - find_path_in_uri(tf->uri, tf->uri_len, &tf_path, &tf_path_len); - - if (tf_path_len > path_len || memcmp(tf_path, path, tf_path_len)) { - /* tf path is not a prefix of `path` */ - return false; - } - if (tf_path_len == path_len) { - /* Both are equal */ - return true; - } - if (tf_path[tf_path_len - 1] == '/') { - /* tf path is a subpath of `path` (with slash), e.g. "foo/" and "foo/bar" */ - return true; - } - if (path[tf_path_len] == '/') { - /* tf path is a subpath of `path` (without slash), e.g. "foo" and "foo/bar" */ - return true; - } - return false; -} - -struct trusted_file* get_trusted_or_allowed_file(const char* path) { - struct trusted_file* tf = NULL; - - size_t path_len = strlen(path); - - spinlock_lock(&g_trusted_file_lock); - - struct trusted_file* tmp; - LISTP_FOR_EACH_ENTRY(tmp, &g_trusted_file_list, list) { - if (tmp->allowed) { - /* allowed files: must be a subfolder or file */ - if (path_is_equal_or_subpath(tmp, path, path_len)) { - tf = tmp; - break; - } - } else { - /* trusted files: must be exactly the same URI */ - const char* tf_path; - size_t tf_path_len; - find_path_in_uri(tmp->uri, tmp->uri_len, &tf_path, &tf_path_len); - if (tf_path_len == path_len && !memcmp(tf_path, path, path_len + 1)) { - tf = tmp; - break; - } - } - } - - spinlock_unlock(&g_trusted_file_lock); - - return tf; -} - -int load_trusted_or_allowed_file(struct trusted_file* tf, PAL_HANDLE file, bool create, - sgx_chunk_hash_t** out_chunk_hashes, uint64_t* out_size, - void** out_umem) { - int ret; - - *out_chunk_hashes = NULL; - *out_size = 0; - *out_umem = NULL; - - if (create) { - assert(tf->allowed); - return register_file(tf->uri, /*hash_str=*/NULL, /*check_duplicates=*/true); - } - - if (tf->allowed) { - /* allowed files: do not need any integrity, so no need for chunk hashes */ - return 0; - } - - /* trusted files: need integrity, so calculate chunk hashes and compare with hash in manifest */ - if (!file->file.seekable) { - log_warning("Trusted file '%s' is not seekable, cannot load it", file->file.realpath); - return PAL_ERROR_DENIED; - } - - sgx_chunk_hash_t* chunk_hashes = NULL; - uint8_t* tmp_chunk = NULL; /* scratch buf to calculate whole-file and chunk-of-file hashes */ - - /* mmap the whole trusted file in untrusted memory for future reads/writes; it is - * caller's responsibility to unmap those areas after use */ - *out_size = tf->size; - if (*out_size) { - ret = ocall_mmap_untrusted(out_umem, tf->size, PROT_READ, MAP_SHARED, file->file.fd, - /*offset=*/0); - if (ret < 0) { - *out_umem = NULL; - ret = unix_to_pal_error(ret); - goto fail; - } - } - - spinlock_lock(&g_trusted_file_lock); - if (tf->chunk_hashes) { - *out_chunk_hashes = tf->chunk_hashes; - spinlock_unlock(&g_trusted_file_lock); - return 0; - } - spinlock_unlock(&g_trusted_file_lock); - - chunk_hashes = malloc(sizeof(sgx_chunk_hash_t) * UDIV_ROUND_UP(tf->size, TRUSTED_CHUNK_SIZE)); - if (!chunk_hashes) { - ret = PAL_ERROR_NOMEM; - goto fail; - } - - tmp_chunk = malloc(TRUSTED_CHUNK_SIZE); - if (!tmp_chunk) { - ret = PAL_ERROR_NOMEM; - goto fail; - } - - sgx_chunk_hash_t* chunk_hashes_item = chunk_hashes; - uint64_t offset = 0; - LIB_SHA256_CONTEXT file_sha; - - ret = lib_SHA256Init(&file_sha); - if (ret < 0) - goto fail; - - for (; offset < tf->size; offset += TRUSTED_CHUNK_SIZE, chunk_hashes_item++) { - /* For each file chunk of size TRUSTED_CHUNK_SIZE, generate 128-bit hash from SHA-256 hash - * over contents of this file chunk (we simply truncate SHA-256 hash to first 128 bits; this - * is fine for integrity purposes). Also, generate a SHA-256 hash for the whole file - * contents to compare with the manifest "reference" hash value. */ - uint64_t chunk_size = MIN(tf->size - offset, TRUSTED_CHUNK_SIZE); - LIB_SHA256_CONTEXT chunk_sha; - ret = lib_SHA256Init(&chunk_sha); - if (ret < 0) - goto fail; - - /* to prevent TOCTOU attacks, copy file contents into the enclave before hashing */ - if (!sgx_copy_to_enclave(tmp_chunk, TRUSTED_CHUNK_SIZE, *out_umem + offset, chunk_size)) - goto fail; - - ret = lib_SHA256Update(&file_sha, tmp_chunk, chunk_size); - if (ret < 0) - goto fail; - - ret = lib_SHA256Update(&chunk_sha, tmp_chunk, chunk_size); - if (ret < 0) - goto fail; - - sgx_chunk_hash_t chunk_hash[2]; /* each chunk_hash is 128 bits in size */ - static_assert(sizeof(chunk_hash) * 8 == 256, ""); - ret = lib_SHA256Final(&chunk_sha, (uint8_t*)&chunk_hash[0]); - if (ret < 0) - goto fail; - - /* note that we truncate SHA256 to 128 bits */ - memcpy(chunk_hashes_item, &chunk_hash[0], sizeof(*chunk_hashes_item)); - } - - sgx_file_hash_t file_hash; - ret = lib_SHA256Final(&file_sha, file_hash.bytes); - if (ret < 0) - goto fail; - - /* check the generated hash-over-whole-file against the reference hash in the manifest */ - if (memcmp(&file_hash, &tf->file_hash, sizeof(file_hash))) { - log_warning("Hash of trusted file '%s' does not match with the reference hash in manifest", - file->file.realpath); - ret = PAL_ERROR_DENIED; - goto fail; - } - - spinlock_lock(&g_trusted_file_lock); - if (tf->chunk_hashes) { - *out_chunk_hashes = tf->chunk_hashes; - spinlock_unlock(&g_trusted_file_lock); - free(chunk_hashes); - free(tmp_chunk); - return 0; - } - tf->chunk_hashes = chunk_hashes; - *out_chunk_hashes = chunk_hashes; - spinlock_unlock(&g_trusted_file_lock); - - free(tmp_chunk); - return 0; - -fail: - if (*out_umem) { - assert(*out_size > 0); - ocall_munmap_untrusted(*out_umem, *out_size); - } - free(chunk_hashes); - free(tmp_chunk); - return ret; -} - -int get_file_check_policy(void) { - return g_file_check_policy; -} - -static void set_file_check_policy(int policy) { - g_file_check_policy = policy; -} - -int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* umem, - off_t aligned_offset, off_t aligned_end, off_t offset, off_t end, - sgx_chunk_hash_t* chunk_hashes, size_t file_size) { - int ret = 0; - - assert(IS_ALIGNED(aligned_offset, TRUSTED_CHUNK_SIZE)); - assert(offset >= aligned_offset && end <= aligned_end); - - uint8_t* tmp_chunk = malloc(TRUSTED_CHUNK_SIZE); - if (!tmp_chunk) { - ret = PAL_ERROR_NOMEM; - goto failed; - } - - sgx_chunk_hash_t* chunk_hashes_item = chunk_hashes + aligned_offset / TRUSTED_CHUNK_SIZE; - - uint8_t* buf_pos = buf; - off_t chunk_offset = aligned_offset; - for (; chunk_offset < aligned_end; chunk_offset += TRUSTED_CHUNK_SIZE, chunk_hashes_item++) { - size_t chunk_size = MIN(file_size - chunk_offset, TRUSTED_CHUNK_SIZE); - off_t chunk_end = chunk_offset + chunk_size; - - sgx_chunk_hash_t chunk_hash[2]; /* each chunk_hash is 128 bits in size but we need 256 */ - - LIB_SHA256_CONTEXT chunk_sha; - ret = lib_SHA256Init(&chunk_sha); - if (ret < 0) - goto failed; - - if (chunk_offset >= offset && chunk_end <= end) { - /* if current chunk-to-copy completely resides in the requested region-to-copy, - * directly copy into buf (without a scratch buffer) and hash in-place */ - if (!sgx_copy_to_enclave(buf_pos, chunk_size, umem + chunk_offset, chunk_size)) { - goto failed; - } - - ret = lib_SHA256Update(&chunk_sha, buf_pos, chunk_size); - if (ret < 0) - goto failed; - - buf_pos += chunk_size; - } else { - /* if current chunk-to-copy only partially overlaps with the requested region-to-copy, - * read the file contents into a scratch buffer, verify hash and then copy only the part - * needed by the caller */ - if (!sgx_copy_to_enclave(tmp_chunk, chunk_size, umem + chunk_offset, chunk_size)) { - goto failed; - } - - ret = lib_SHA256Update(&chunk_sha, tmp_chunk, chunk_size); - if (ret < 0) - goto failed; - - /* determine which part of the chunk is needed by the caller */ - off_t copy_start = MAX(chunk_offset, offset); - off_t copy_end = MIN(chunk_offset + (off_t)chunk_size, end); - assert(copy_end > copy_start); - - memcpy(buf_pos, tmp_chunk + copy_start - chunk_offset, copy_end - copy_start); - buf_pos += copy_end - copy_start; - } - - ret = lib_SHA256Final(&chunk_sha, (uint8_t*)&chunk_hash[0]); - if (ret < 0) - goto failed; - - if (memcmp(chunk_hashes_item, &chunk_hash[0], sizeof(*chunk_hashes_item))) { - log_error("Accessing file '%s' is denied: incorrect hash of file chunk at %lu-%lu.", - path, chunk_offset, chunk_end); - ret = PAL_ERROR_DENIED; - goto failed; - } - } - - free(tmp_chunk); - return 0; - -failed: - free(tmp_chunk); - memset(buf, 0, end - offset); - return ret; -} - -static int register_file(const char* uri, const char* hash_str, bool check_duplicates) { - if (hash_str && strlen(hash_str) != sizeof(sgx_file_hash_t) * 2) { - log_error("Hash (%s) of a trusted file %s is not a SHA256 hash", hash_str, uri); - return PAL_ERROR_INVAL; - } - - size_t uri_len = strlen(uri); - if (uri_len >= URI_MAX) { - log_error("Size of file exceeds maximum %dB: %s", URI_MAX, uri); - return PAL_ERROR_INVAL; - } - - if (check_duplicates) { - /* this check is only done during runtime (when creating a new file) and not needed during - * initialization (because manifest is assumed to have no duplicates); skipping this check - * significantly improves startup time */ - spinlock_lock(&g_trusted_file_lock); - struct trusted_file* tf; - LISTP_FOR_EACH_ENTRY(tf, &g_trusted_file_list, list) { - if (tf->uri_len == uri_len && !memcmp(tf->uri, uri, uri_len)) { - spinlock_unlock(&g_trusted_file_lock); - return 0; - } - } - spinlock_unlock(&g_trusted_file_lock); - } - - struct trusted_file* new = malloc(sizeof(*new) + uri_len + 1); - if (!new) - return PAL_ERROR_NOMEM; - - INIT_LIST_HEAD(new, list); - new->size = 0; - new->chunk_hashes = NULL; - new->allowed = false; - new->uri_len = uri_len; - memcpy(new->uri, uri, uri_len + 1); - - if (hash_str) { - assert(strlen(hash_str) == sizeof(sgx_file_hash_t) * 2); - - char* bytes = hex2bytes(hash_str, strlen(hash_str), new->file_hash.bytes, - sizeof(new->file_hash.bytes)); - if (!bytes) { - log_error("Could not parse hash of file: %s", uri); - free(new); - return PAL_ERROR_INVAL; - } - } else { - memset(&new->file_hash, 0, sizeof(new->file_hash)); - new->allowed = true; - } - - spinlock_lock(&g_trusted_file_lock); - - if (check_duplicates) { - /* this check is only done during runtime and not needed during initialization (see above); - * we check again because same file could have been added by another thread in meantime */ - struct trusted_file* tf; - LISTP_FOR_EACH_ENTRY(tf, &g_trusted_file_list, list) { - if (tf->uri_len == uri_len && !memcmp(tf->uri, uri, uri_len)) { - spinlock_unlock(&g_trusted_file_lock); - free(new); - return 0; - } - } - } - - LISTP_ADD_TAIL(new, &g_trusted_file_list, list); - spinlock_unlock(&g_trusted_file_lock); - - return 0; -} - -static int normalize_and_register_file(const char* uri, const char* hash_str) { - int ret; - - if (hash_str) { - if (!strstartswith(uri, URI_PREFIX_FILE)) { - log_error("Invalid URI [%s]: Trusted files must start with 'file:'", uri); - return PAL_ERROR_INVAL; - } - } else { - if (!strstartswith(uri, URI_PREFIX_FILE) && !strstartswith(uri, URI_PREFIX_DEV)) { - log_error("Invalid URI [%s]: Allowed files must start with 'file:' or 'dev:'", uri); - return PAL_ERROR_INVAL; - } - } - - const size_t norm_uri_size = strlen(uri) + 1; - char* norm_uri = malloc(norm_uri_size); - if (!norm_uri) { - return PAL_ERROR_NOMEM; - } - - const char* uri_prefix; - size_t uri_prefix_len; - if (strstartswith(uri, URI_PREFIX_FILE)) { - uri_prefix = URI_PREFIX_FILE; - uri_prefix_len = URI_PREFIX_FILE_LEN; - } else { - assert(strstartswith(uri, URI_PREFIX_DEV)); - uri_prefix = URI_PREFIX_DEV; - uri_prefix_len = URI_PREFIX_DEV_LEN; - } - - memcpy(norm_uri, uri_prefix, uri_prefix_len); - size_t norm_path_size = norm_uri_size - uri_prefix_len; - if (!get_norm_path(uri + uri_prefix_len, norm_uri + uri_prefix_len, &norm_path_size)) { - log_error("Path (%s) normalization failed", uri); - ret = PAL_ERROR_INVAL; - goto out; - } - - ret = register_file(norm_uri, hash_str, /*check_duplicates=*/false); -out: - free(norm_uri); - return ret; -} - -int init_trusted_files(void) { - int ret; - - toml_table_t* manifest_sgx = toml_table_in(g_pal_public_state.manifest_root, "sgx"); - if (!manifest_sgx) - return 0; - - toml_array_t* toml_trusted_files = toml_array_in(manifest_sgx, "trusted_files"); - if (!toml_trusted_files) - return 0; - - ssize_t toml_trusted_files_cnt = toml_array_nelem(toml_trusted_files); - if (toml_trusted_files_cnt < 0) - return PAL_ERROR_DENIED; - if (toml_trusted_files_cnt == 0) - return 0; - - char* toml_trusted_uri_str = NULL; - char* toml_trusted_sha256_str = NULL; - - for (ssize_t i = 0; i < toml_trusted_files_cnt; i++) { - /* read `sgx.trusted_file = {uri = "file:foo", sha256 = "deadbeef"}` entry from manifest */ - toml_table_t* toml_trusted_file = toml_table_at(toml_trusted_files, i); - if (!toml_trusted_file) { - log_error("Invalid trusted file in manifest at index %ld (not a TOML table)", i); - ret = PAL_ERROR_INVAL; - goto out; - } - - toml_raw_t toml_trusted_uri_raw = toml_raw_in(toml_trusted_file, "uri"); - if (!toml_trusted_uri_raw) { - log_error("Invalid trusted file in manifest at index %ld (no 'uri' key)", i); - ret = PAL_ERROR_INVAL; - goto out; - } - - ret = toml_rtos(toml_trusted_uri_raw, &toml_trusted_uri_str); - if (ret < 0) { - log_error("Invalid trusted file in manifest at index %ld ('uri' is not a string)", i); - ret = PAL_ERROR_INVAL; - goto out; - } - - toml_raw_t toml_trusted_sha256_raw = toml_raw_in(toml_trusted_file, "sha256"); - if (!toml_trusted_sha256_raw) { - log_error("Invalid trusted file in manifest at index %ld (no 'sha256' key)", i); - ret = PAL_ERROR_INVAL; - goto out; - } - - ret = toml_rtos(toml_trusted_sha256_raw, &toml_trusted_sha256_str); - if (ret < 0 || !toml_trusted_sha256_str) { - log_error("Invalid trusted file in manifest at index %ld ('sha256' is not a string)", - i); - ret = PAL_ERROR_INVAL; - goto out; - } - - ret = normalize_and_register_file(toml_trusted_uri_str, toml_trusted_sha256_str); - if (ret < 0) { - log_error("normalize_and_register_file(\"%s\", \"%s\") failed with error code: %s", - toml_trusted_uri_str, toml_trusted_sha256_str, pal_strerror(ret)); - goto out; - } - - free(toml_trusted_uri_str); - free(toml_trusted_sha256_str); - toml_trusted_uri_str = NULL; - toml_trusted_sha256_str = NULL; - } - - ret = 0; -out: - free(toml_trusted_uri_str); - free(toml_trusted_sha256_str); - return ret; -} - -static void maybe_warn_about_allowed_files_usage(void) { - if (!g_pal_common_state.parent_process) - g_allowed_files_warn = true; -} - -int init_allowed_files(void) { - int ret; - - toml_table_t* manifest_sgx = toml_table_in(g_pal_public_state.manifest_root, "sgx"); - if (!manifest_sgx) - return 0; - - toml_array_t* toml_allowed_files = toml_array_in(manifest_sgx, "allowed_files"); - if (!toml_allowed_files) - return 0; - - maybe_warn_about_allowed_files_usage(); - - ssize_t toml_allowed_files_cnt = toml_array_nelem(toml_allowed_files); - if (toml_allowed_files_cnt < 0) - return PAL_ERROR_DENIED; - if (toml_allowed_files_cnt == 0) - return 0; - - char* toml_allowed_file_str = NULL; - - for (ssize_t i = 0; i < toml_allowed_files_cnt; i++) { - toml_raw_t toml_allowed_file_raw = toml_raw_at(toml_allowed_files, i); - if (!toml_allowed_file_raw) { - log_error("Invalid allowed file in manifest at index %ld", i); - ret = PAL_ERROR_INVAL; - goto out; - } - - ret = toml_rtos(toml_allowed_file_raw, &toml_allowed_file_str); - if (ret < 0) { - log_error("Invalid allowed file in manifest at index %ld (not a string)", i); - ret = PAL_ERROR_INVAL; - goto out; - } - - ret = normalize_and_register_file(toml_allowed_file_str, /*hash_str=*/NULL); - if (ret < 0) { - log_error("normalize_and_register_file(\"%s\", NULL) failed with error: %s", - toml_allowed_file_str, pal_strerror(ret)); - goto out; - } - - free(toml_allowed_file_str); - toml_allowed_file_str = NULL; - } - - ret = 0; -out: - free(toml_allowed_file_str); - return ret; -} - -int init_file_check_policy(void) { - int ret; - - char* file_check_policy_str = NULL; - ret = toml_string_in(g_pal_public_state.manifest_root, "sgx.file_check_policy", - &file_check_policy_str); - if (ret < 0) { - log_error("Cannot parse 'sgx.file_check_policy'"); - return PAL_ERROR_INVAL; - } - - if (!file_check_policy_str) - return 0; - - if (!strcmp(file_check_policy_str, "strict")) { - set_file_check_policy(FILE_CHECK_POLICY_STRICT); - } else if (!strcmp(file_check_policy_str, "allow_all_but_log")) { - set_file_check_policy(FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG); - } else { - log_error("Unknown value for 'sgx.file_check_policy' " - "(allowed: `strict`, `allow_all_but_log`)'"); - free(file_check_policy_str); - return PAL_ERROR_INVAL; - } - - log_debug("File check policy: %s", file_check_policy_str); - free(file_check_policy_str); - return 0; -} - static int update_seal_key_mask(const char* mask_name, uint8_t* mask_ptr, size_t mask_size) { int ret; diff --git a/pal/src/host/linux-sgx/enclave_ocalls.c b/pal/src/host/linux-sgx/enclave_ocalls.c index 5e898bebc6..91739defc9 100644 --- a/pal/src/host/linux-sgx/enclave_ocalls.c +++ b/pal/src/host/linux-sgx/enclave_ocalls.c @@ -523,7 +523,7 @@ ssize_t ocall_write(int fd, const void* buf, size_t count) { void* old_ustack = sgx_prepare_ustack(); if (sgx_is_valid_untrusted_ptr(buf, count, /*alignment=*/1)) { - /* buf is in untrusted memory (e.g., allowed file mmaped in untrusted memory) */ + /* buf is in untrusted memory */ untrusted_buf = buf; } else if (sgx_is_completely_within_enclave(buf, count)) { /* typical case of buf inside of enclave memory */ @@ -649,7 +649,7 @@ ssize_t ocall_pwrite(int fd, const void* buf, size_t count, off_t offset) { void* old_ustack = sgx_prepare_ustack(); if (sgx_is_valid_untrusted_ptr(buf, count, /*alignment=*/1)) { - /* buf is in untrusted memory (e.g., allowed file mmaped in untrusted memory) */ + /* buf is in untrusted memory */ untrusted_buf = buf; } else if (sgx_is_completely_within_enclave(buf, count)) { /* typical case of buf inside of enclave memory */ diff --git a/pal/src/host/linux-sgx/enclave_tf.h b/pal/src/host/linux-sgx/enclave_tf.h deleted file mode 100644 index c930a37aa9..0000000000 --- a/pal/src/host/linux-sgx/enclave_tf.h +++ /dev/null @@ -1,80 +0,0 @@ -/* SPDX-License-Identifier: LGPL-3.0-or-later */ -/* Copyright (C) 2021 Intel Corporation */ - -/* Trusted files (TF) are integrity protected and transparently verified when accessed by Gramine - * or by app running inside Gramine. 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, Gramine - * loads the whole file, calculates its SHA256 hash, and checks against the corresponding hash in - * the manifest. If the hashes do not match, the file access will be rejected. - * - * During the generation of the SHA256 hash, a 128-bit hash (truncated SHA256) is also generated for - * each chunk (of size TRUSTED_CHUNK_SIZE) in the file. The per-chunk hashes are used for partial - * verification in future reads, to avoid re-verifying the whole file again or the need of caching - * file contents. - */ - -/* TODO: Move trusted/allowed files implementation into a separate file (`enclave_tf.c`?) */ - -#pragma once - -#include -#include -#include - -#include "api.h" -#include "enclave_tf_structs.h" -#include "pal.h" -#include "pal_linux_types.h" - -int init_seal_key_material(void); - -int init_file_check_policy(void); -int get_file_check_policy(void); - -/*! - * \brief Get trusted/allowed file struct, if corresponding path entry exists in the manifest. - * - * \param path Normalized path to search for trusted/allowed files. - * - * \returns trusted/allowed file struct if found, NULL otherwise. - */ -struct trusted_file* get_trusted_or_allowed_file(const char* path); - -/*! - * \brief Open the file as trusted or allowed, according to the manifest. - * - * \param tf Trusted file struct corresponding to this file. - * \param file File handle to be opened. - * \param create Whether this file is newly created. - * \param out_chunk_hashes Array of hashes over file chunks. - * \param out_size Returns size of opened file. - * \param out_umem Untrusted memory address at which the file was loaded. - * - * \returns 0 on success, negative error code on failure - */ -int load_trusted_or_allowed_file(struct trusted_file* tf, PAL_HANDLE file, bool create, - sgx_chunk_hash_t** out_chunk_hashes, uint64_t* out_size, - void** out_umem); - -/*! - * \brief Copy and check file contents from untrusted outside buffer to in-enclave buffer - * - * \param path File path (currently only for a log message). - * \param buf In-enclave buffer where contents of the file are copied. - * \param umem Start of untrusted file memory mapped outside the enclave. - * \param aligned_offset Offset into file contents to copy, aligned to TRUSTED_CHUNK_SIZE. - * \param aligned_end End of file contents to copy, aligned to TRUSTED_CHUNK_SIZE. - * \param offset Unaligned offset into file contents to copy. - * \param end Unaligned end of file contents to copy. - * \param chunk_hashes Array of hashes of all file chunks. - * \param file_size Total size of the file. - * - * \returns 0 on success, negative error code on failure - */ -int copy_and_verify_trusted_file(const char* path, uint8_t* buf, const void* umem, - off_t aligned_offset, off_t aligned_end, off_t offset, off_t end, - sgx_chunk_hash_t* chunk_hashes, size_t file_size); - -int init_trusted_files(void); -int init_allowed_files(void); diff --git a/pal/src/host/linux-sgx/enclave_tf_structs.h b/pal/src/host/linux-sgx/enclave_tf_structs.h deleted file mode 100644 index f591b38bf7..0000000000 --- a/pal/src/host/linux-sgx/enclave_tf_structs.h +++ /dev/null @@ -1,39 +0,0 @@ -/* SPDX-License-Identifier: LGPL-3.0-or-later */ -/* Copyright (C) 2021 Intel Corporation */ - -#pragma once - -#include -#include -#include - -#include "list.h" - -enum { - FILE_CHECK_POLICY_STRICT = 0, - FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG, -}; - -typedef struct { - uint8_t bytes[32]; -} sgx_file_hash_t; - -typedef struct { - uint8_t bytes[16]; -} sgx_chunk_hash_t; - -/* - * Perhaps confusingly, `struct trusted_file` describes not only "sgx.trusted_files" but also - * "sgx.allowed_files". For allowed files, `allowed = true`, `chunk_hashes = NULL`, and `uri` can be - * not only a file but also a directory. TODO: Perhaps split "allowed_files" into a separate struct? - */ -DEFINE_LIST(trusted_file); -struct trusted_file { - LIST_TYPE(trusted_file) list; - uint64_t size; - bool allowed; - sgx_file_hash_t file_hash; /* hash over the whole file, retrieved from the manifest */ - sgx_chunk_hash_t* chunk_hashes; /* array of hashes over separate file chunks */ - size_t uri_len; - char uri[]; /* must be NULL-terminated */ -}; diff --git a/pal/src/host/linux-sgx/pal_devices.c b/pal/src/host/linux-sgx/pal_devices.c index 4c88f5e5b2..51e962303f 100644 --- a/pal/src/host/linux-sgx/pal_devices.c +++ b/pal/src/host/linux-sgx/pal_devices.c @@ -11,7 +11,6 @@ */ #include "api.h" -#include "enclave_tf.h" #include "ioctls.h" #include "pal.h" #include "pal_error.h" @@ -71,18 +70,6 @@ static int dev_open(PAL_HANDLE* handle, const char* type, const char* uri, enum } hdl->dev.realpath = normpath; - struct trusted_file* tf = get_trusted_or_allowed_file(hdl->dev.realpath); - if (!tf || !tf->allowed) { - if (get_file_check_policy() != FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG) { - log_warning("Disallowing access to device '%s'; device is not allowed.", - hdl->dev.realpath); - ret = PAL_ERROR_DENIED; - goto fail; - } - log_warning("Allowing access to unknown device '%s' due to file_check_policy settings.", - hdl->dev.realpath); - } - if (access == PAL_ACCESS_RDONLY) { hdl->flags |= PAL_HANDLE_FD_READABLE; } else if (access == PAL_ACCESS_WRONLY) { diff --git a/pal/src/host/linux-sgx/pal_files.c b/pal/src/host/linux-sgx/pal_files.c index 84e6d3b847..14df63e4c6 100644 --- a/pal/src/host/linux-sgx/pal_files.c +++ b/pal/src/host/linux-sgx/pal_files.c @@ -6,60 +6,16 @@ */ #include "api.h" -#include "asan.h" -#include "enclave_tf.h" #include "linux_utils.h" #include "pal.h" #include "pal_error.h" #include "pal_flags_conv.h" #include "pal_internal.h" #include "pal_linux.h" -#include "pal_linux_defs.h" #include "pal_linux_error.h" -#include "pal_sgx.h" #include "path_utils.h" #include "stat.h" -/* this macro is used to emulate mmap() via pread() in chunks of 128MB (mmapped files may be many - * GBs in size, and a pread OCALL could fail with -ENOMEM, so we cap to reasonably small size) */ -#define MAX_READ_SIZE (PRESET_PAGESIZE * 1024 * 32) - -void fixup_file_handle_after_deserialization(PAL_HANDLE handle) { - int ret; - - assert(handle->hdr.type == PAL_TYPE_FILE); - assert(!handle->file.chunk_hashes); - assert(!handle->file.umem); - assert(handle->file.realpath); - - if (!handle->file.trusted) { - /* unknown (if file check policy allows) or encrypted or allowed file, no need to fix */ - return; - } - - struct trusted_file* tf = get_trusted_or_allowed_file(handle->file.realpath); - if (!tf || tf->allowed) { - log_error("cannot find checkpointed trusted file '%s' in manifest", handle->file.realpath); - die_or_inf_loop(); - } - - tf->size = handle->file.size; /* tf size is required for load_trusted_or_allowed_file() below */ - - sgx_chunk_hash_t* chunk_hashes; - uint64_t file_size; - void* umem; - ret = load_trusted_or_allowed_file(tf, handle, /*create=*/false, &chunk_hashes, &file_size, - &umem); - if (ret < 0) { - log_error("cannot load checkpointed trusted file '%s'", handle->file.realpath); - die_or_inf_loop(); - } - - assert(file_size == handle->file.size); - handle->file.chunk_hashes = chunk_hashes; - handle->file.umem = umem; -} - static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, enum pal_access pal_access, pal_share_flags_t pal_share, enum pal_create_mode pal_create, pal_stream_options_t pal_options) { @@ -67,7 +23,6 @@ static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, int ret; int fd = -1; PAL_HANDLE hdl = NULL; - bool do_create = (pal_create == PAL_CREATE_ALWAYS) || (pal_create == PAL_CREATE_TRY); int flags = PAL_ACCESS_TO_LINUX_OPEN(pal_access) | PAL_CREATE_TO_LINUX_OPEN(pal_create) | PAL_OPTION_TO_LINUX_OPEN(pal_options) | O_CLOEXEC; @@ -96,33 +51,8 @@ static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, init_handle_hdr(hdl, PAL_TYPE_FILE); hdl->flags |= PAL_HANDLE_FD_READABLE | PAL_HANDLE_FD_WRITABLE; - hdl->file.realpath = normpath; - struct trusted_file* tf = NULL; - - if (!(pal_options & PAL_OPTION_PASSTHROUGH)) { - tf = get_trusted_or_allowed_file(hdl->file.realpath); - if (!tf) { - if (get_file_check_policy() != FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG) { - log_warning("Disallowing access to file '%s'; file is not trusted or allowed.", - hdl->file.realpath); - ret = PAL_ERROR_DENIED; - goto fail; - } - log_warning("Allowing access to unknown file '%s' due to file_check_policy settings.", - hdl->file.realpath); - } - } - - if (tf && !tf->allowed && (do_create - || (pal_access == PAL_ACCESS_RDWR) - || (pal_access == PAL_ACCESS_WRONLY))) { - log_error("Disallowing create/write/append to a trusted file '%s'", hdl->file.realpath); - ret = PAL_ERROR_DENIED; - goto fail; - } - fd = ocall_open(uri, flags, pal_share); if (fd < 0) { ret = unix_to_pal_error(fd); @@ -138,27 +68,6 @@ static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, hdl->file.fd = fd; hdl->file.seekable = !S_ISFIFO(st.st_mode); - hdl->file.size = st.st_size; - - if (!tf) { - *handle = hdl; - return 0; - } - - /* at this point, we work with a trusted or allowed file */ - tf->size = st.st_size; - - sgx_chunk_hash_t* chunk_hashes; - uint64_t file_size; - void* umem; - ret = load_trusted_or_allowed_file(tf, hdl, do_create, &chunk_hashes, &file_size, &umem); - if (ret < 0) - goto fail; - - hdl->file.chunk_hashes = chunk_hashes; - hdl->file.size = file_size; - hdl->file.umem = umem; - hdl->file.trusted = !tf->allowed; *handle = hdl; return 0; @@ -172,66 +81,27 @@ static int file_open(PAL_HANDLE* handle, const char* type, const char* uri, static int64_t file_read(PAL_HANDLE handle, uint64_t offset, uint64_t count, void* buffer) { int64_t ret; - - if (!handle->file.trusted) { - assert(!handle->file.chunk_hashes); - if (handle->file.seekable) { - ret = ocall_pread(handle->file.fd, buffer, count, offset); - } else { - ret = ocall_read(handle->file.fd, buffer, count); - } - return ret < 0 ? unix_to_pal_error(ret) : ret; + if (handle->file.seekable) { + ret = ocall_pread(handle->file.fd, buffer, count, offset); + } else { + ret = ocall_read(handle->file.fd, buffer, count); } - - /* case of trusted file: already mmaped in umem, copy from there and verify hash */ - assert(handle->file.chunk_hashes); - - if (offset >= handle->file.size) - return 0; - - off_t end = MIN(offset + count, handle->file.size); - off_t aligned_offset = ALIGN_DOWN(offset, TRUSTED_CHUNK_SIZE); - off_t aligned_end = ALIGN_UP(end, TRUSTED_CHUNK_SIZE); - - assert(handle->file.size && handle->file.umem); - ret = copy_and_verify_trusted_file(handle->file.realpath, buffer, handle->file.umem, - aligned_offset, aligned_end, offset, end, - handle->file.chunk_hashes, handle->file.size); - if (ret < 0) - return ret; - - return end - offset; + return ret < 0 ? unix_to_pal_error(ret) : ret; } static int64_t file_write(PAL_HANDLE handle, uint64_t offset, uint64_t count, const void* buffer) { int64_t ret; - - if (!handle->file.trusted) { - assert(!handle->file.chunk_hashes); - if (handle->file.seekable) { - ret = ocall_pwrite(handle->file.fd, buffer, count, offset); - } else { - ret = ocall_write(handle->file.fd, buffer, count); - } - return ret < 0 ? unix_to_pal_error(ret) : ret; + if (handle->file.seekable) { + ret = ocall_pwrite(handle->file.fd, buffer, count, offset); + } else { + ret = ocall_write(handle->file.fd, buffer, count); } - - /* case of trusted file: disallow writing completely */ - assert(handle->file.chunk_hashes); - log_warning("Writing to a trusted file (%s) is disallowed!", handle->file.realpath); - return PAL_ERROR_DENIED; + return ret < 0 ? unix_to_pal_error(ret) : ret; } static void file_destroy(PAL_HANDLE handle) { assert(handle->hdr.type == PAL_TYPE_FILE); - if (handle->file.trusted && handle->file.size) { - /* case of trusted file: the whole file was mmapped in untrusted memory */ - assert(handle->file.chunk_hashes); - assert(handle->file.umem); - ocall_munmap_untrusted(handle->file.umem, handle->file.size); - } - int ret = ocall_close(handle->file.fd); if (ret < 0) { log_error("closing file host fd %d failed: %s", handle->file.fd, unix_strerror(ret)); @@ -252,11 +122,7 @@ static int file_delete(PAL_HANDLE handle, enum pal_delete_mode delete_mode) { static int file_setlength(PAL_HANDLE handle, uint64_t length) { int ret = ocall_ftruncate(handle->file.fd, length); - if (ret < 0) - return unix_to_pal_error(ret); - - handle->file.size = length; - return 0; + return ret < 0 ? unix_to_pal_error(ret) : 0; } static int file_flush(PAL_HANDLE handle) { diff --git a/pal/src/host/linux-sgx/pal_host.h b/pal/src/host/linux-sgx/pal_host.h index 594ed0c63f..388c969aea 100644 --- a/pal/src/host/linux-sgx/pal_host.h +++ b/pal/src/host/linux-sgx/pal_host.h @@ -15,7 +15,6 @@ #include #include -#include "enclave_tf_structs.h" #include "list.h" #include "spinlock.h" @@ -49,12 +48,7 @@ typedef struct { struct { PAL_IDX fd; char* realpath; - size_t size; - bool seekable; /* regular files are seekable, FIFO pipes are not */ - /* below fields are used only for trusted files */ - sgx_chunk_hash_t* chunk_hashes; /* array of hashes of file chunks */ - void* umem; /* valid only when chunk_hashes != NULL and size > 0 */ - bool trusted; /* is this a Trusted File? */ + bool seekable; /* regular files are seekable, FIFO pipes are not */ } file; struct { diff --git a/pal/src/host/linux-sgx/pal_linux.h b/pal/src/host/linux-sgx/pal_linux.h index 914d75f1a4..f22e892993 100644 --- a/pal/src/host/linux-sgx/pal_linux.h +++ b/pal/src/host/linux-sgx/pal_linux.h @@ -107,6 +107,8 @@ int init_enclave(void); int init_reserved_ranges(void* urts_ptr, size_t urts_size); +int init_seal_key_material(void); + /* master key for all enclaves of one application, populated by the first enclave and inherited by * all other enclaves (children, their children, etc.); used as master key in pipes' encryption */ extern PAL_SESSION_KEY g_master_key; @@ -207,6 +209,5 @@ int _PalStreamSecureWrite(LIB_SSL_CONTEXT* ssl_ctx, const uint8_t* buf, size_t l int _PalStreamSecureSave(LIB_SSL_CONTEXT* ssl_ctx, const uint8_t** obuf, size_t* olen); void fixup_socket_handle_after_deserialization(PAL_HANDLE handle); -void fixup_file_handle_after_deserialization(PAL_HANDLE handle); #endif /* IN_ENCLAVE */ diff --git a/pal/src/host/linux-sgx/pal_linux_defs.h b/pal/src/host/linux-sgx/pal_linux_defs.h index 26e6c3d2a6..570d727336 100644 --- a/pal/src/host/linux-sgx/pal_linux_defs.h +++ b/pal/src/host/linux-sgx/pal_linux_defs.h @@ -36,8 +36,6 @@ static_assert(SSA_XSAVE_SIZE_MAX + /* GPRs size in SSA */176 <= SSA_FRAME_SIZE - #define DEBUG_ECALL 0 #define DEBUG_OCALL 0 -#define TRUSTED_CHUNK_SIZE (PRESET_PAGESIZE * 4UL) - #define MAX_ARGS_SIZE 10000000 #define MAX_ENV_SIZE 10000000 diff --git a/pal/src/host/linux-sgx/pal_main.c b/pal/src/host/linux-sgx/pal_main.c index 714a6de351..7dcbad4fc3 100644 --- a/pal/src/host/linux-sgx/pal_main.c +++ b/pal/src/host/linux-sgx/pal_main.c @@ -18,7 +18,6 @@ #include "api.h" #include "asan.h" #include "assert.h" -#include "enclave_tf.h" #include "gdb_integration/sgx_gdb.h" #include "init.h" #include "pal.h" @@ -406,10 +405,23 @@ static int import_and_init_extra_runtime_domain_names(struct pal_dns_host_conf* extern void* g_enclave_base; extern void* g_enclave_top; -extern bool g_allowed_files_warn; extern uint64_t g_tsc_hz; extern size_t g_unused_tcs_pages_num; +static bool need_warn_about_allowed_files_usage(void) { + if (!g_pal_common_state.parent_process) { + /* only warn in the master process, not in children */ + return false; + } + toml_table_t* manifest_sgx = toml_table_in(g_pal_public_state.manifest_root, "sgx"); + if (!manifest_sgx) + return false; + toml_array_t* toml_allowed_files = toml_array_in(manifest_sgx, "allowed_files"); + if (!toml_allowed_files) + return false; + return true; +} + static int print_warnings_on_insecure_configs(PAL_HANDLE parent_process) { int ret; @@ -426,11 +438,12 @@ static int print_warnings_on_insecure_configs(PAL_HANDLE parent_process) { bool allow_eventfd = false; bool experimental_flock = false; bool allow_all_files = false; - bool use_allowed_files = g_allowed_files_warn; + bool use_allowed_files = need_warn_about_allowed_files_usage(); bool encrypted_files_keys = false; bool memfaults_without_exinfo_allowed = g_pal_linuxsgx_state.memfaults_without_exinfo_allowed; char* log_level_str = NULL; + char* file_check_policy_str = NULL; ret = toml_string_in(g_pal_public_state.manifest_root, "loader.log_level", &log_level_str); if (ret < 0) @@ -468,7 +481,11 @@ static int print_warnings_on_insecure_configs(PAL_HANDLE parent_process) { if (ret < 0) goto out; - if (get_file_check_policy() == FILE_CHECK_POLICY_ALLOW_ALL_BUT_LOG) + ret = toml_string_in(g_pal_public_state.manifest_root, "sgx.file_check_policy", + &file_check_policy_str); + if (ret < 0) + goto out; + if (file_check_policy_str && !strcmp(file_check_policy_str, "allow_all_but_log")) allow_all_files = true; toml_table_t* manifest_fs = toml_table_in(g_pal_public_state.manifest_root, "fs"); @@ -548,6 +565,7 @@ static int print_warnings_on_insecure_configs(PAL_HANDLE parent_process) { ret = 0; out: + free(file_check_policy_str); free(log_level_str); return ret; } @@ -853,21 +871,6 @@ noreturn void pal_linux_main(void* uptr_libpal_uri, size_t libpal_uri_len, void* ocall_exit(1, /*is_exitgroup=*/true); } - if ((ret = init_file_check_policy()) < 0) { - log_error("Failed to load the file check policy: %s", pal_strerror(ret)); - ocall_exit(1, /*is_exitgroup=*/true); - } - - if ((ret = init_allowed_files()) < 0) { - log_error("Failed to initialize allowed files: %s", pal_strerror(ret)); - ocall_exit(1, /*is_exitgroup=*/true); - } - - if ((ret = init_trusted_files()) < 0) { - log_error("Failed to initialize trusted files: %s", pal_strerror(ret)); - ocall_exit(1, /*is_exitgroup=*/true); - } - ret = toml_bool_in(g_pal_public_state.manifest_root, "sys.enable_extra_runtime_domain_names_conf", /*defaultval*/false, &g_pal_public_state.extra_runtime_domain_names_conf); diff --git a/pal/src/host/linux-sgx/pal_misc.c b/pal/src/host/linux-sgx/pal_misc.c index 6aa582e182..113780f8ad 100644 --- a/pal/src/host/linux-sgx/pal_misc.c +++ b/pal/src/host/linux-sgx/pal_misc.c @@ -19,6 +19,7 @@ #include "pal_linux.h" #include "pal_linux_error.h" #include "seqlock.h" +#include "sgx_arch.h" #include "sgx_attest.h" #include "spinlock.h" #include "toml_utils.h" diff --git a/pal/src/host/linux-sgx/pal_streams.c b/pal/src/host/linux-sgx/pal_streams.c index 009376abdb..59af79efd5 100644 --- a/pal/src/host/linux-sgx/pal_streams.c +++ b/pal/src/host/linux-sgx/pal_streams.c @@ -164,9 +164,6 @@ static int handle_deserialize(PAL_HANDLE* handle, const void* data, size_t size, free(hdl); return PAL_ERROR_NOMEM; } - hdl->file.chunk_hashes = hdl->file.umem = NULL; /* set up in below fixup function */ - hdl->file.fd = host_fd; /* correct host FD must be set for below fixup function */ - fixup_file_handle_after_deserialization(hdl); break; } case PAL_TYPE_DIR: { diff --git a/python/gramine-manifest b/python/gramine-manifest index 6a9d86cfa0..aa1da6a896 100755 --- a/python/gramine-manifest +++ b/python/gramine-manifest @@ -30,8 +30,11 @@ def validate_define(_ctx, _param, values): help='check the manifest for correctness against builtin schema') @click.argument('infile', type=click.File('r'), required=False) @click.argument('outfile', type=click.File('wb'), default='-') +@click.option('--chroot', + type=click.Path(exists=True, dir_okay=True, file_okay=False), + help='Measure a chroot directory, not the host filesystem') @click.pass_context -def main(ctx, string, define, infile, outfile, check): +def main(ctx, string, define, infile, outfile, check, chroot): if not bool(string) ^ bool(infile): ctx.fail('specify exactly one of (infile, -c)') template = infile.read() if infile else string @@ -49,6 +52,7 @@ def main(ctx, string, define, infile, outfile, check): # TODO after 1.7: make this a hard error #ctx.exit(1) + manifest.expand_all_trusted_files(chroot=chroot) manifest.dump(outfile) if __name__ == '__main__':