diff --git a/Documentation/manpages/gsc.rst b/Documentation/manpages/gsc.rst index 52c7e5be..8d3949d4 100644 --- a/Documentation/manpages/gsc.rst +++ b/Documentation/manpages/gsc.rst @@ -49,15 +49,11 @@ addition, install the Docker client python package via pip. GSC requires Python sudo apt install docker.io python3 python3-pip pip3 install docker pyyaml jinja2 -Kernel modules and services ---------------------------- +SGX software stack +------------------ -To run with Intel SGX, please install the following kernel driver and -services: - -- `Intel SGX driver `__ -- `Intel SGX SDK `__ -- `Graphene SGX Driver (kernel module) `__ +To run with Intel SGX, please install the corresponding software stack as +described in :doc:`../building`. Host configuration ------------------ @@ -66,8 +62,7 @@ To create Docker images, the user must have access to Docker daemon. .. warning:: Please use this step with caution. By granting the user access to the Docker - group, the user may acquire root privileges via :command:`docker run`. You - could also run commands as root. + group, the user may acquire root privileges via :command:`docker run`. .. code-block:: sh @@ -94,9 +89,7 @@ Builds an unsigned graphenized Docker image of an application image called ``gsc--unsigned`` by compiling Graphene or relying on a prebuilt Graphene image. -Synopsis: - -:command:`gsc build` [*OPTIONS*] <*IMAGE-NAME*> <*APP1.MANIFEST*> [<*APP2.MANIFEST*> ... <*APPN.MANIFEST*>] +:command:`gsc build` [*OPTIONS*] <*IMAGE-NAME*> <*APP.MANIFEST*> .. option:: -d @@ -113,7 +106,7 @@ Synopsis: Allow untrusted arguments to be specified at :command:`docker run`. Otherwise any arguments specified during :command:`docker run` are ignored. -.. option:: -nc +.. option:: --no-cache Disable Docker's caches during :command:`gsc build`. This builds the unsigned graphenized image from scratch. @@ -148,9 +141,7 @@ Synopsis: Signs the enclave of an unsigned graphenized Docker image and creates a new Docker image called ``gsc-``. :command:`gsc sign-image` always removes intermediate Docker images, if successful or not, to ensure the removal -of the signing key in intermediate Docker images. - -Synopsis: +of the signing key in them. :command:`gsc sign-image` [*OPTIONS*] <*IMAGE-NAME*> <*KEY-FILE*> @@ -175,8 +166,6 @@ Builds a base Docker image including the Graphene sources and compiled runtime. This base image can be used as input for :command:`gsc build` via configuration parameter `Graphene.Image`. -Synopsis: - :command:`gsc build-graphene` [*OPTIONS*] <*IMAGE-NAME*> .. option:: -d @@ -190,7 +179,7 @@ Synopsis: :command:`gsc build` commands to include the Linux PAL using :option:`-L `. -.. option:: -nc +.. option:: --no-cache Disable Docker's caches during :command:`gsc build-graphene`. This builds the unsigned graphenized image from scratch. @@ -234,59 +223,47 @@ specified during :command:`docker run` are ignored. To be able to provide arguments at runtime, the image build has to enable this via the option :option:`--insecure-args `. -Docker images starting multiple applications --------------------------------------------- - -Depending on the use case, a Docker container may execute multiple applications. -The Docker image defines the entrypoint executable which could fork additional -executables. A common pattern in Docker images is executing an entrypoint script -which calls a set of executables. Similarly to Docker, Graphene has a -corresponding option (``libos.entrypoint``) which should point to the first -executable started inside Graphene namespace. - Stages of building graphenized SGX Docker images ------------------------------------------------ The build process of a graphenized Docker image from image ```` -follows four main stages and produces an image named ``gsc-``. -:command:`gsc build` generates the first two stages (building/pulling Graphene -and graphenizing the base image) and :command:`gsc sign-image` generates the -last two stages (signing the Intel SGX enclave and generating the final Docker -image). +follows three main stages and produces an image named ``gsc-``. +:command:`gsc build-graphene` performs only the first stage, +:command:`gsc build` performs the first two stages, and finally +:command:`gsc sign-image` performs the last stage. -Building or Pulling Graphene -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#. **Building Graphene.** The first stage builds Graphene from sources based on + the provided configuration (see :file:`config.yaml`) which includes the + distribution (e.g., Ubuntu 18.04), Graphene repository, and the Intel SGX + driver details. This stage can be skipped if :command:`gsc build` uses a + pre-built Graphene Docker image. -The first stage either compiles Graphene based on the provided configuration -(see :file:`config.yaml`) which includes the distribution (e.g., Ubuntu 18.04), -Graphene repository, and the Intel SGX driver details, or pulls a prebuilt -Docker image also defined via the configuration file. Prebuilt images will be -provided for popular cloud-provider offerings or can be created via -:command:`gsc build-graphene`. +#. **Graphenizing the application image.** The second stage copies the important + Graphene artifacts (e.g., the runtime and signer tool) from the first stage + (or if the first stage was skipped, it pulls a prebuilt Docker image defined + via the configuration file). It then prepares image-specific variables such + as the executable path and the library path, and scans the entire image to + generate a list of trusted files. GSC excludes files and paths starting with + :file:`/boot`, :file:`/dev`, :file:`/proc`, :file:`/var`, :file:`/sys` and + :file:`/etc/rc`, since checksums are required which either don't exist or may + vary across different deployment machines. GSC combines these variables and + list of trusted files into a new manifest file. In a last step the entrypoint + is changed to launch the :file:`apploader.sh` script which generates an Intel + SGX token and starts the :program:`pal-Linux-SGX` loader. Note that the + generated image (``gsc--unsigned``) cannot successfully load an + Intel SGX enclave, since essential files and the signature of the enclave are + still missing (see next stage). -Graphenizing the application image -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#. **Signing the Intel SGX enclave.** The third stage uses Graphene's signer + tool to generate SIGSTRUCT files for SGX enclave initialization. This tool + also generates an SGX-specific manifest file. The required signing key is + provided by the user via the :command:`gsc sign-image` command and copied + into this Docker build stage. The generated image is called + ``gsc-`` and includes all necessary files to start an Intel SGX + enclave. -The second stage copies the important Graphene artifacts (e.g., the runtime and -signer tool) from the first stage. It then prepares image-specific variables -such as the executable path and the library path, and scans the entire image to -generate a list of trusted files. GSC excludes files and paths starting with -:file:`/boot`, :file:`/dev`, :file:`/proc`, :file:`/var`, :file:`/sys` and -:file:`/etc/rc`, since checksums are required which either don't exist or may -vary across different deployment machines. GSC combines these variables and list -of trusted files to a new manifest file. In a last step the entrypoint is -changed to launch the :file:`apploader.sh` script which generates an Intel SGX -token and starts the :program:`pal-Linux-SGX` loader. The generated image -(``gsc--unsigned``) cannot successfully load an Intel SGX enclave, -since essential files and the signing of the enclave are missing. - -Signing the Intel SGX enclave -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The third stage uses Graphene's signer tool to generate SIGSTRUCT files for SGX -enclave initialization. This tool also generates an SGX-specific manifest files. -The required signing key is provided by the user via the :command:`gsc -sign-image` command and copied into this Docker build stage. +In the future we plan to provide prebuilt Graphene images for popular +cloud-provider offerings. Generating a signed graphenized Docker image ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -294,8 +271,7 @@ Generating a signed graphenized Docker image The last stage combines the graphenized Docker image with the signed enclave and manifest files. Therefore it copies the SIGSTRUCT files and the SGX-specific manifest file from the previous stage into the graphenized Docker image from the -second stage. The resulting image is called `gsc-` and includes all -necessary files to start an Intel SGX enclave. +second stage. Configuration ============= @@ -313,11 +289,11 @@ in :file:`config.yaml.template`. Source repository of Graphene. Default value: `https://github.com/oscarlab/graphene.git - `__ + `__. .. describe:: Graphene.Branch - Use this branch of the repository. Default value: master + Use this branch of the repository. Default value: master. .. describe:: Graphene.Image @@ -330,17 +306,17 @@ in :file:`config.yaml.template`. Source repository of the Intel SGX driver. Default value: `https://github.com/01org/linux-sgx-driver.git - `__ + `__. .. describe:: SGXDriver.Branch - Use this branch of the repository. Default value: sgx_driver_1.9 + Use this branch of the repository. Default value: sgx_driver_1.9. Run graphenized Docker images ============================= Execute :command:`docker run` command via Docker CLI and provide gsgx and -isgx/sgx device, and the PSW/AESM socket. Additional Docker options and +isgx/sgx devices and the PSW/AESM socket. Additional Docker options and executable arguments may be supplied to the :command:`docker run` command. .. warning:: @@ -350,12 +326,12 @@ executable arguments may be supplied to the :command:`docker run` command. .. program:: docker -:command:`docker run` --device=/dev/gsgx --device=/dev/isgx -v /var/run/aesmd/aesm.socket:/var/run/aesmd/aesm.socket [*OPTIONS*] gsc-<*IMAGE-NAME*> [<*ARGUMENTS*>] +:command:`docker run` [*OPTIONS*] gsc-<*IMAGE-NAME*> [<*ARGUMENTS*>] .. option:: OPTIONS :command:`docker run` options. Common options include ``-it`` (interactive - with terminal) or ``-d`` (detached). Please see + with terminal), ``-d`` (detached), ``--device`` (forward device). Please see `Docker manual `__ for details. @@ -382,7 +358,7 @@ sign the image via a :command:`gsc sign-image` command. .. envvar:: GSC_PAL - Specifies the pal loader + This environment variable specifies the pal loader. .. code-block:: sh @@ -393,7 +369,7 @@ Example The :file:`test` folder in :file:`Tools/gsc` describes how to graphenize Docker images and test them with sample inputs. The samples include Ubuntu-based Docker -images of Bash, Python, nodejs, Numpy, and Pytorch. +images of Bash, Python, Node.js, Numpy and Pytorch. .. warning:: All test images rely on insecure arguments to be able to set test-specific @@ -403,20 +379,26 @@ images of Bash, Python, nodejs, Numpy, and Pytorch. The example below shows how to graphenize the public Docker image of Python3. This example assumes that all prerequisites are installed and configured. -#. Pull public Python image from Dockerhub: - - .. code-block:: sh - - docker pull python - #. Create a configuration file: .. code-block:: sh cd Tools/gsc cp config.yaml.template config.yaml - # Adopt config.yaml to the installed Intel SGX driver and desired Graphene - # repository. + # Manually adopt config.yaml to the installed Intel SGX driver and desired + # Graphene repository/version. + +#. Generate the signing key (if you don't already have a key): + + .. code-block:: sh + + openssl genrsa -3 -out enclave-key.pem 3072 + +#. Pull public Python image from Dockerhub: + + .. code-block:: sh + + docker pull python #. Graphenize the Python image using :command:`gsc build`: @@ -428,16 +410,14 @@ This example assumes that all prerequisites are installed and configured. .. code-block:: sh - # Generate signing key (if you don't already have a key) - openssl genrsa -3 -out enclave-key.pem 3072 - # Sign graphenized Docker image with the key ./gsc sign-image python enclave-key.pem -#. Test the graphenized Docker image: +#. Test the graphenized Docker image (change ``--device=/dev/isgx`` to your + version of the Intel SGX driver if needed): .. code-block:: sh - docker run --device=/dev/gsgx --device=/dev/*sgx \ + docker run --device=/dev/gsgx --device=/dev/isgx \ -v /var/run/aesmd/aesm.socket:/var/run/aesmd/aesm.socket \ gsc-python -c 'print("HelloWorld!")' @@ -455,10 +435,8 @@ Dependency on Ubuntu 18.04 Docker images not based on Ubuntu 18.04 may not be compatible with GSC. GSC relies on Graphene to execute Linux applications inside Intel SGX enclaves and the installation of prerequisites depends on package manager and package -repositories. - -GSC can simply be extended to support other distributions by providing a -template for this distribution in :file:`Tools/gsc/templates`. +repositories. GSC can simply be extended to support other distributions by +providing a template for this distribution in :file:`Tools/gsc/templates`. Trusted data in Docker volumes ------------------------------ diff --git a/Tools/gsc/.gitignore b/Tools/gsc/.gitignore index 956618af..3209731e 100644 --- a/Tools/gsc/.gitignore +++ b/Tools/gsc/.gitignore @@ -1,2 +1,4 @@ -gsc-*/ +/build config.yaml +config.aks.*.yaml +*.pem diff --git a/Tools/gsc/Makefile b/Tools/gsc/Makefile index 224b1373..6f822298 100644 --- a/Tools/gsc/Makefile +++ b/Tools/gsc/Makefile @@ -2,7 +2,7 @@ # located in images/*. These files are then pulled by Docker Hub to generate the public Docker Hub # images of Graphene and cannot be generated by Docker Hub, since it does not execute commands. When # GSC changes substantially in its initial Graphene build (e.g. when -# template/Dockerfile.*.compile.template changes), these automatically generated files need to be +# templates/Dockerfile.*.compile.template changes), these automatically generated files need to be # updated. Generally, changes to Graphene do not require rebuilding these Docker files. IMAGES=graphene_aks @@ -22,12 +22,12 @@ config.aks.%.yaml: images/graphene_aks.latest.dockerfile: config.aks.master.yaml ./gsc build-graphene -f -c $< graphene-aks - mv graphene-aks/Dockerfile.compile $@ - $(RM) -r graphene-aks + mv build/gsc-graphene-aks/Dockerfile.compile $@ .PHONY: distclean distclean: clean $(RM) images/* + $(RM) -r build .PHONY: clean clean: diff --git a/Tools/gsc/config.yaml.template b/Tools/gsc/config.yaml.template index d19498f9..9657a238 100644 --- a/Tools/gsc/config.yaml.template +++ b/Tools/gsc/config.yaml.template @@ -1,7 +1,7 @@ Distro: "ubuntu18.04" Graphene: Repository: "https://github.com/oscarlab/graphene.git" - Branch: "a37a85baf5ab6e0901a777313072d62b4e75fc3a" # the last working commit. TODO: update + Branch: "master" SGXDriver: Repository: "https://github.com/01org/linux-sgx-driver.git" Branch: "sgx_driver_1.9" diff --git a/Tools/gsc/finalize_manifest.py b/Tools/gsc/finalize_manifest.py index 450b7573..01ddc8cc 100755 --- a/Tools/gsc/finalize_manifest.py +++ b/Tools/gsc/finalize_manifest.py @@ -1,22 +1,27 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later -# Copyright (C) 2020 Intel Corp. -# Anjo Vahldiek-Oberwagner - +# Copyright (C) 2020-2021 Intel Corp. +# Anjo Vahldiek-Oberwagner +# Dmitrii Kuvaiskii import argparse import os import re import subprocess import sys + import jinja2 -def is_ascii(chars): - return all(ord(c) < 128 for c in chars) +def is_utf8(filename_bytes): + try: + filename_bytes.decode('UTF-8') + return True + except UnicodeError: + return False + def generate_trusted_files(root_dir): cwd = os.getcwd() if os.getcwd() != '/' else '' - # Exclude files and paths from list of trusted files excluded_paths_regex = (r'^/(' r'boot/.*' r'|dev/.*' @@ -27,83 +32,69 @@ def generate_trusted_files(root_dir): r'|var/.*)' f'|^{cwd}/(' r'.*\.manifest' - r'|finalize_manifest\.py' - r'|sign_manifest\.py)$') + r'|finalize_manifest\.py)$') exclude_re = re.compile(excluded_paths_regex) + num_trusted = 0 trusted_files = '' - script_file = os.path.basename(__file__) - - for root, _, files in os.walk(root_dir, followlinks=False): + for root, _, files in os.walk(root_dir.encode('UTF-8'), followlinks=False): for file in files: filename = os.path.join(root, file) - if (not exclude_re.match(filename) - # The check for ascii-only characters is required, since the manifest syntax does - # not support other encodings (e.g., UTF-8). - and is_ascii(filename) - and os.path.isfile(filename) - and filename != script_file): - trusted_files += f'sgx.trusted_files.file{num_trusted} = "file:{filename}"\n' - num_trusted += 1 + if not os.path.isfile(filename): + # only regular files are added as trusted files + continue + if not is_utf8(filename): + # we append filenames as TOML strings which must be in UTF-8 + print(f'\t[from inside Docker container] File {filename} is not in UTF-8!') + sys.exit(1) - print(f'Found {num_trusted} files in \'{root_dir}\'.') + # convert from bytes to str for further string handling + filename = filename.decode('UTF-8') + if exclude_re.match(filename): + # exclude special files and paths from list of trusted files + continue + if '\n' in filename: + # we use TOML's basic single-line strings, can't have newlines + continue + escaped_filename = filename.translate(str.maketrans({'\\': r'\\', '"': r'\"'})) + trusted_files += f'sgx.trusted_files.file{num_trusted} = "file:{escaped_filename}"\n' + num_trusted += 1 + + print(f'\t[from inside Docker container] Found {num_trusted} files in `{root_dir}`.') return trusted_files + def generate_library_paths(): - ld_paths = subprocess.check_output('ldconfig -v', - stderr=subprocess.PIPE, shell=True).decode().splitlines() + encoding = sys.stdout.encoding if sys.stdout.encoding is not None else 'UTF-8' + ld_paths = subprocess.check_output('ldconfig -v', stderr=subprocess.PIPE, shell=True) + ld_paths = ld_paths.decode(encoding).splitlines() - # Library paths start without whitespace. Libraries found under a path start with an - # indentation. + # Library paths start without whitespace (in contrast to libraries found under this path) ld_paths = (line for line in ld_paths if not re.match(r'(^\s)', line)) - - ld_library_paths = os.getenv('LD_LIBRARY_PATH') - - return ''.join(ld_paths) + (ld_library_paths if ld_library_paths is not None else '') - -def get_binary_path(executable): - path = subprocess.check_output(f'which {executable}', - stderr=subprocess.STDOUT, shell=True).decode() - return path.replace('\n', '') + return ''.join(ld_paths) + os.getenv('LD_LIBRARY_PATH', default='') argparser = argparse.ArgumentParser() -argparser.add_argument('directory', default='/', - help='Search the directory tree from this root for files and generate list of trusted files') -argparser.add_argument('manifest', help='Manifest file to use.') +argparser.add_argument('-d', '--dir', default='/', + help='Search directory tree from this root to generate list of trusted files.') def main(args=None): args = argparser.parse_args(args[1:]) - - if not os.path.isdir(args.directory): - argparser.error(f'Could not find directory {args.directory}.') - - trusted_files = generate_trusted_files(args.directory) - library_paths = generate_library_paths() - env_path = os.getenv('PATH') - - print(f'LD_LIBRARY_PATH = \'{library_paths}\'\nPATH = \'{env_path}\'.') + if not os.path.isdir(args.dir): + argparser.error(f'\t[from inside Docker container] Could not find directory `{args.dir}`.') env = jinja2.Environment(loader=jinja2.FileSystemLoader('.')) - env.globals.update({ - 'library_paths': library_paths, - 'env_path': env_path - }) + env.globals.update({'library_paths': generate_library_paths(), 'env_path': os.getenv('PATH')}) - manifest = args.manifest - - # Write final manifest file with trusted files + manifest = 'entrypoint.manifest' rendered_manifest = env.get_template(manifest).render() + trusted_files = generate_trusted_files(args.dir) + with open(manifest, 'wb') as manifest_file: + trusted_files_string = '\n'.join((rendered_manifest, trusted_files, '\n')) + manifest_file.write(trusted_files_string.encode('UTF-8')) - with open(manifest, 'w') as manifest_file: - manifest_file.write('\n'.join(( - rendered_manifest, - trusted_files, - '\n') - )) - - print(f'\tWrote {manifest}.') + print(f'\t[from inside Docker container] Successfully finalized `{manifest}`.') if __name__ == '__main__': main(sys.argv) diff --git a/Tools/gsc/gsc.py b/Tools/gsc/gsc.py index 417f15a6..07941cd9 100755 --- a/Tools/gsc/gsc.py +++ b/Tools/gsc/gsc.py @@ -1,90 +1,59 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later -# Copyright (C) 2020 Intel Corp. -# Anjo Vahldiek-Oberwagner +# Copyright (C) 2020-2021 Intel Corp. +# Anjo Vahldiek-Oberwagner +# Dmitrii Kuvaiskii import argparse -import os import json -import re +import os import pathlib import shutil import sys + import jinja2 + import docker # pylint: disable=import-error import yaml # pylint: disable=import-error -def gsc_image_name(name): - return f'gsc-{name}' +def gsc_image_name(original_image_name): + return f'gsc-{original_image_name}' -def gsc_unsigned_image_name(name): - return f'gsc-{name}-unsigned' +def gsc_unsigned_image_name(original_image_name): + return f'gsc-{original_image_name}-unsigned' -def load_config(file): - return yaml.safe_load(file) +def gsc_tmp_build_path(original_image_name): + return pathlib.Path('build') / f'gsc-{original_image_name}' -# Generate manifest from a template (see template/manifest.template) based on the binary name. -# The generated manifest is only partially completed. Later, during the docker build it is -# finished by adding the list of trusted files, the path to the binary, and LD_LIBRARY_PATH. -def generate_manifest(image, env, user_manifest, binary): - user_mf = '' - if os.path.exists(user_manifest): - with open(user_manifest, 'r') as user_manifest_file: - user_mf = user_manifest_file.read() - manifest_path = (pathlib.Path(image) / binary).with_suffix('.manifest') - with open(manifest_path, 'w') as app_manifest: - app_manifest.write(env.get_template('manifest.template').render(binary=binary)) - app_manifest.write('\n') - app_manifest.write(user_mf) - app_manifest.write('\n') +def get_docker_image(docker_socket, image_name): + try: + docker_image = docker_socket.images.get(image_name) + return docker_image + except (docker.errors.ImageNotFound, docker.errors.APIError): + return None -# Generate app loader script which generates the SGX token and starts the Graphene PAL loader with -# the manifest as an input (see template/apploader.template). -def generate_app_loader(image, env, binary): - apploader_path = (pathlib.Path(image) / 'apploader').with_suffix('.sh') - with open(apploader_path, 'w') as apploader: - apploader.write(env.get_template('apploader.template').render(binary=binary)) -# Generate a dockerfile that compiles Graphene and includes the application image. This dockerfile -# is generated from a template (templates/Dockerfile.$distro.build.template). It follows a docker -# multistage build with two stages. The first stage compiles Graphene for the specified -# distribution. The second stage builds the final image based on the previously built Graphene and -# the base image. -def generate_build_dockerfile(image, env, binary): - dockerfile_path = (pathlib.Path(image) / 'Dockerfile.build') - with open(dockerfile_path, 'w') as dockerfile: - dockerfile.write(env.get_template( - f'Dockerfile.{env.globals["Distro"]}.build.template').render(binary=binary)) +def build_docker_image(docker_api, build_path, image_name, dockerfile, **kwargs): + build_path = str(build_path) # Docker API doesn't understand PathLib's PosixPath type + stream = docker_api.build(path=build_path, tag=image_name, dockerfile=dockerfile, + **kwargs) + for chunk in stream: + encoding = sys.stdout.encoding if sys.stdout.encoding is not None else 'UTF-8' + json_output = json.loads(chunk.decode(encoding)) + if 'stream' in json_output: + for line in json_output['stream'].splitlines(): + print(line) -def prepare_build_context(image, manifest, env, binary): - gsc_image = gsc_image_name(image) - # create directory for image specific files - os.makedirs(gsc_image, exist_ok=True) - # generate dockerfile to build graphenized docker image - generate_build_dockerfile(gsc_image, env, binary) - - # generate app specific loader script - generate_app_loader(gsc_image, env, binary) - - # generate manifest stub for this app - generate_manifest(gsc_image, env, manifest, binary) - - fm_path = (pathlib.Path(gsc_image) / 'finalize_manifest').with_suffix('.py') - shutil.copyfile('finalize_manifest.py', fm_path) - sm_path = (pathlib.Path(gsc_image) / 'sign_manifest').with_suffix('.py') - shutil.copyfile('sign_manifest.py', sm_path) - -def extract_binary_cmd_from_image_config(config): +def extract_binary_cmd_from_image_config(config, env): entrypoint = config['Entrypoint'] or [] num_starting_entrypoint_items = len(entrypoint) cmd = config['Cmd'] or [] - # Some Docker images only use the optional CMD and have an empty entrypoint + # Some Docker images only use the optional CMD and have an empty entrypoint; # GSC has to make it explicit to prepare scripts and Intel SGX signatures entrypoint.extend(cmd) - if not entrypoint: print('Could not find the entrypoint binary to the application image.') sys.exit(1) @@ -102,72 +71,22 @@ def extract_binary_cmd_from_image_config(config): last_bin_arg = 0 binary_arguments = '' - # Place the remaining optional arguments previously specified as command, in the - # new command. This is necessary, since the first element of the command may be the - # binary of the resulting image. - cmd = entrypoint[last_bin_arg + 1 : ] if len(entrypoint) > last_bin_arg + 1 else '' + # Place the remaining optional arguments previously specified as command in the new command. + # Necessary since the first element of the command may be the binary of the resulting image. + cmd = entrypoint[last_bin_arg + 1:] if len(entrypoint) > last_bin_arg + 1 else '' cmd = [s.replace('\\', '\\\\').replace('"', '\\"') for s in cmd] - return binary, binary_arguments, cmd + env.globals.update({'binary': binary, 'binary_arguments': binary_arguments, 'cmd': cmd}) -def extract_working_dir(image): - working_dir = image.attrs['Config']['WorkingDir'] + +def extract_working_dir_from_image_config(config, env): + working_dir = config['WorkingDir'] if working_dir == '': working_dir = '/' elif working_dir[-1] != '/': working_dir = working_dir + '/' + env.globals.update({'working_dir': working_dir}) - return working_dir - -def prepare_env(base_image, image, args): - env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates/')) - - env.globals.update(vars(args)) - config = load_config(args.config_file) - env.globals.update(config) - - # Image names follow the format distro/package:tag - image_re = re.match(r'([^:]*)(:?)(.*)', image[image.rfind('/')+1:]) - - # Extract binary and command from base_image - binary, binary_arguments, cmd = extract_binary_cmd_from_image_config( - base_image.attrs['Config']) - - working_dir = extract_working_dir(base_image) - - env.globals.update({ - 'app_image': image, - 'app': image_re.group(1), - 'binary_arguments': binary_arguments, - 'cmd': cmd, - 'working_dir': working_dir, - }) - - return env, binary - -def get_docker_image(docker_socket, image): - try: - docker_image = docker_socket.images.get(image) - return docker_image - - except (docker.errors.ImageNotFound, docker.errors.APIError): - return None - -def build_docker_image(path, name, dockerfile, **kwargs): - docker_api = docker.APIClient(base_url='unix://var/run/docker.sock') - # Docker build returns stream of json output - stream = docker_api.build(path=path, - tag=name, - dockerfile=dockerfile, - **kwargs) - - # Print continuously the stream of output by docker build - for chunk in stream: - json_output = json.loads(chunk.decode(sys.stdout.encoding - if sys.stdout.encoding is not None else 'UTF-8')) - if 'stream' in json_output: - for line in json_output['stream'].splitlines(): - print(line) def extract_build_args(args): buildargs_dict = {} @@ -176,155 +95,181 @@ def extract_build_args(args): key, value = item.split('=', 1) buildargs_dict[key] = value else: - # user specified a --build-arg flag with key and without value, let's retrieve value - # from environment + # user specified --build-arg with key and without value, let's retrieve value from env if item in os.environ: buildargs_dict[item] = os.environ[item] else: print(f'Could not set build arg `{item}` from environment.') sys.exit(1) - return buildargs_dict -# Build graphenized docker image. args has to follow [] -# [ ...]. + +# Command 1: Build unsigned graphenized Docker image from original app Docker image. def gsc_build(args): - image = args.image + original_image_name = args.image # input original-app image name + unsigned_image_name = gsc_unsigned_image_name(args.image) # output unsigned image name + signed_image_name = gsc_image_name(args.image) # final signed image name (to check) + tmp_build_path = gsc_tmp_build_path(args.image) # pathlib obj with build artifacts docker_socket = docker.from_env() - if get_docker_image(docker_socket, gsc_image_name(image)) is not None: - print(f'Image {gsc_image_name(image)} already exists, no gsc build required.') + if get_docker_image(docker_socket, signed_image_name) is not None: + print(f'Final graphenized image `{signed_image_name}` already exists.') sys.exit(0) - base_image = get_docker_image(docker_socket, image) - if base_image is None: - print(f'Unable to find base image {image}') + original_image = get_docker_image(docker_socket, original_image_name) + if original_image is None: + print(f'Cannot find original application Docker image `{original_image_name}`.') sys.exit(1) - print(f'Building graphenized image from base image {image}') + print(f'Building unsigned graphenized Docker image `{unsigned_image_name}` from original ' + f'application image `{original_image_name}`...') - env, binary = prepare_env(base_image, image, args) + # initialize Jinja env with configurations extracted from the original Docker image - prepare_build_context(image, args.manifest, env, binary) + env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates/')) + env.globals.update(yaml.safe_load(args.config_file)) + env.globals.update(vars(args)) + env.globals.update({'app_image': original_image_name}) + extract_binary_cmd_from_image_config(original_image.attrs['Config'], env) + extract_working_dir_from_image_config(original_image.attrs['Config'], env) - buildargs_dict = extract_build_args(args) + os.makedirs(tmp_build_path, exist_ok=True) - build_docker_image(gsc_image_name(image), gsc_unsigned_image_name(image), 'Dockerfile.build', - rm=args.rm, nocache=args.no_cache, buildargs=buildargs_dict) + # generate Dockerfile.build from Jinja-style templates/Dockerfile..build.template + # using the user-provided config file with info on OS distro, Graphene version and SGX driver + # and other env configurations generated above + build_template = env.get_template(f'Dockerfile.{env.globals["Distro"]}.build.template') + with open(tmp_build_path / 'Dockerfile.build', 'w') as dockerfile: + dockerfile.write(build_template.render()) + + # generate apploader.sh from Jinja-style templates/apploader.template + with open(tmp_build_path / 'apploader.sh', 'w') as apploader: + apploader.write(env.get_template('apploader.template').render()) + + # generate entrypoint.manifest from Jinja-style templates/entrypoint.manifest.template and + # append additional, user-provided manifest options + user_manifest_contents = '' + if os.path.exists(args.manifest): + with open(args.manifest, 'r') as user_manifest_file: + user_manifest_contents = user_manifest_file.read() + + with open(tmp_build_path / 'entrypoint.manifest', 'w') as entrypoint_manifest: + entrypoint_manifest.write(env.get_template('entrypoint.manifest.template').render()) + entrypoint_manifest.write('\n') + entrypoint_manifest.write(user_manifest_contents) + entrypoint_manifest.write('\n') + + # copy helper script to finalize the manifest from within graphenized Docker image + shutil.copyfile('finalize_manifest.py', tmp_build_path / 'finalize_manifest.py') + + build_docker_image(docker_socket.api, tmp_build_path, unsigned_image_name, 'Dockerfile.build', + rm=args.rm, nocache=args.no_cache, buildargs=extract_build_args(args)) # Check if docker build failed - if get_docker_image(docker_socket, gsc_unsigned_image_name(image)) is None: - print(f'Failed to build graphenized image for {image}') + if get_docker_image(docker_socket, unsigned_image_name) is None: + print(f'Failed to build unsigned graphenized docker image `{unsigned_image_name}`.') sys.exit(1) - print(f'Successfully graphenized docker image {image} into docker image ' - f'{gsc_unsigned_image_name(image)}') + print(f'Successfully built an unsigned graphenized Docker image `{unsigned_image_name}` from ' + f'original application image `{original_image_name}`.') -# Generate a dockerfile that compiles Graphene. This dockerfile is generated from a template -# (templates/Dockerfile.$distro.compile.template). -def generate_compile_dockerfile(image, env): - dockerfile_path = (pathlib.Path(image) / 'Dockerfile.compile') - with open(dockerfile_path, 'w') as dockerfile: - dockerfile.write(env.get_template( - f'Dockerfile.{env.globals["Distro"]}.compile.template').render()) -# Build a Docker image which includes the sources and the compiled runtime of Graphene. +# Command 2: Build a "base Graphene" Docker image with the compiled runtime of Graphene. def gsc_build_graphene(args): - image = args.image + graphene_image_name = gsc_image_name(args.image) # output base-Graphene image name + tmp_build_path = gsc_tmp_build_path(args.image) # pathlib obj with build artifacts + + config = yaml.safe_load(args.config_file) + if 'Image' in config['Graphene']: + print('`gsc build-graphene` does not allow `Graphene.Image` to be set.') + sys.exit(1) docker_socket = docker.from_env() - if get_docker_image(docker_socket, image) is not None: - print(f'Image {image} already exists, no gsc build-graphene required.') + if get_docker_image(docker_socket, graphene_image_name) is not None: + print(f'Base-Graphene Docker image `{graphene_image_name}` already exists.') sys.exit(0) - print(f'Building Graphene image {image}') + print(f'Building base-Graphene image `{graphene_image_name}`...') + + # generate Dockerfile.compile from Jinja-style templates/Dockerfile..compile.template + # using the user-provided config file with info on OS distro, Graphene version and SGX driver + # and other user-provided args (see argparser::gsc_build_graphene below) env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates/')) - - env.globals.update(vars(args)) - config = load_config(args.config_file) - # Building Graphene should never be done on a base Image of Graphene - if 'Image' in config['Graphene']: - print('Configuration error: `gsc build-graphene` does not allow `Graphene.Image` ' - 'to be set.') - sys.exit(1) env.globals.update(config) + env.globals.update(vars(args)) + compile_template = env.get_template(f'Dockerfile.{env.globals["Distro"]}.compile.template') - # create directory for image specific files - os.makedirs(image, exist_ok=True) - - # generate dockerfile to build graphenized docker image - generate_compile_dockerfile(image, env) + os.makedirs(tmp_build_path, exist_ok=True) + with open(tmp_build_path / 'Dockerfile.compile', 'w') as dockerfile: + dockerfile.write(compile_template.render()) if args.file_only: - print(f'Successfully created Dockerfile for image {image}.') - else: - buildargs_dict = extract_build_args(args) + print(f'Successfully created Dockerfile.compile for base-Graphene image ' + f'`{graphene_image_name}`.') + return - build_docker_image(image, image, 'Dockerfile.compile', - rm=args.rm, nocache=args.no_cache, buildargs=buildargs_dict) + build_docker_image(docker_socket.api, tmp_build_path, graphene_image_name, 'Dockerfile.compile', + rm=args.rm, nocache=args.no_cache, buildargs=extract_build_args(args)) - # Check if docker build failed - if get_docker_image(docker_socket, image) is None: - print(f'Failed to build graphenized image for {image}') - sys.exit(1) + if get_docker_image(docker_socket, graphene_image_name) is None: + print(f'Failed to build a base-Graphene image `{graphene_image_name}`.') + sys.exit(1) - print(f'Successfully built Graphene-only Docker image {image}.') + print(f'Successfully built a base-Graphene image `{graphene_image_name}`.') -def generate_dockerfile_sign_manifest(image, env): - dockerfile_path = (pathlib.Path(gsc_image_name(image)) / 'Dockerfile.sign_manifest') - with open(dockerfile_path, 'w') as dockerfile: - dockerfile.write(env.get_template( - f'Dockerfile.{env.globals["Distro"]}.sign_manifest.template') - .render(image=gsc_unsigned_image_name(image))) - -# Sign Docker image which was previously built via `gsc build`. +# Command 3: Sign Docker image which was previously built via `gsc build`. def gsc_sign_image(args): - - image = args.image - key = args.key + unsigned_image_name = gsc_unsigned_image_name(args.image) # input image name + signed_image_name = gsc_image_name(args.image) # output image name + tmp_build_path = gsc_tmp_build_path(args.image) # pathlib obj with build artifacts docker_socket = docker.from_env() - gsc_image = get_docker_image(docker_socket, gsc_unsigned_image_name(image)) - if gsc_image is None: - print(f'Could not find graphenized Docker image of {image}.\n' - f'Please make sure to build the graphenized image first by using gsc build command.') + unsigned_image = get_docker_image(docker_socket, unsigned_image_name) + if unsigned_image is None: + print(f'Cannot find unsigned graphenized Docker image `{unsigned_image_name}`.\n' + f'You must first build this image via `gsc build` command.') sys.exit(1) + print(f'Signing graphenized Docker image `unsigned_image_name` -> `{signed_image_name}`...') + + # generate Dockerfile.sign from Jinja-style templates/Dockerfile..sign.template + # using the user-provided config file with info on OS distro, Graphene version and SGX driver env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates/')) - config = load_config(args.config_file) - env.globals.update(config) - env.globals.update({'working_dir': extract_working_dir(gsc_image)}) + env.globals.update(yaml.safe_load(args.config_file)) + sign_template = env.get_template(f'Dockerfile.{env.globals["Distro"]}.sign.template') - generate_dockerfile_sign_manifest(image, env) + os.makedirs(tmp_build_path, exist_ok=True) + with open(tmp_build_path / 'Dockerfile.sign', 'w') as dockerfile: + dockerfile.write(sign_template.render(image=unsigned_image_name)) - fk_path = (pathlib.Path(gsc_image_name(image)) / 'gsc-signer-key').with_suffix('.pem') - shutil.copyfile(os.path.abspath(key), fk_path) + # copy user-provided signing key to our tmp build dir (to copy it later inside Docker image) + tmp_build_key_path = tmp_build_path / 'gsc-signer-key.pem' + shutil.copyfile(os.path.abspath(args.key), tmp_build_key_path) try: - # We force the removal of intermediate Docker images to not leave the signing - # key in a Docker container. - build_docker_image(gsc_image_name(image), gsc_image_name(image), - 'Dockerfile.sign_manifest', forcerm=True) - + # `forcerm` parameter forces removal of intermediate Docker images even after unsuccessful + # builds, to not leave the signing key lingering in any Docker containers + build_docker_image(docker_socket.api, tmp_build_path, signed_image_name, 'Dockerfile.sign', + forcerm=True) finally: - # Remove key file from the temporary folder - os.remove(fk_path) + os.remove(tmp_build_key_path) - # Check if docker build failed - if get_docker_image(docker_socket, gsc_image_name(image)) is None: - print(f'Failed to sign graphenized image for {image}') - sys.exit(1) + if get_docker_image(docker_socket, signed_image_name) is None: + print(f'Failed to build a signed graphenized Docker image `{signed_image_name}`.') + sys.exit(1) + + print(f'Successfully built a signed Docker image `{signed_image_name}` from ' + f'`{unsigned_image_name}`.') - print(f'Successfully signed docker image {gsc_unsigned_image_name(image)} into docker ' - f'image {gsc_image_name(image)}.') argparser = argparse.ArgumentParser() subcommands = argparser.add_subparsers(metavar='') subcommands.required = True + sub_build = subcommands.add_parser('build', help="Build graphenized Docker image") sub_build.set_defaults(command=gsc_build) sub_build.add_argument('-d', '--debug', action='store_true', @@ -342,12 +287,11 @@ sub_build.add_argument('--build-arg', action='append', default=[], help='Set build-time variables (same as "docker build --build-arg").') sub_build.add_argument('-c', '--config_file', type=argparse.FileType('r', encoding='UTF-8'), default='config.yaml', help='Specify configuration file.') -sub_build.add_argument('image', - help='Name of the application Docker image.') +sub_build.add_argument('image', help='Name of the application Docker image.') sub_build.add_argument('manifest', help='Manifest file to use.') sub_build_graphene = subcommands.add_parser('build-graphene', - help="Build base Graphene Docker image") + help="Build base-Graphene Docker image") sub_build_graphene.set_defaults(command=gsc_build_graphene) sub_build_graphene.add_argument('-d', '--debug', action='store_true', help='Compile Graphene with debug flags and output.') @@ -365,16 +309,14 @@ sub_build_graphene.add_argument('-c', '--config_file', sub_build_graphene.add_argument('-f', '--file-only', action='store_true', help='Stop after Dockerfile is created and do not build the Docker image.') sub_build_graphene.add_argument('image', - help='Name of the output Docker image.') + help='Name of the output base-Graphene Docker image.') sub_sign = subcommands.add_parser('sign-image', help="Sign graphenized Docker image") sub_sign.set_defaults(command=gsc_sign_image) sub_sign.add_argument('-c', '--config_file', type=argparse.FileType('r', encoding='UTF-8'), default='config.yaml', help='Specify configuration file.') -sub_sign.add_argument('image', - help='Name of the application Docker image.') -sub_sign.add_argument('key', - help='Key for signing the image.') +sub_sign.add_argument('image', help='Name of the application (base) Docker image.') +sub_sign.add_argument('key', help='Key to sign the Intel SGX enclaves inside the Docker image.') def main(args): args = argparser.parse_args() diff --git a/Tools/gsc/images/graphene_aks.latest.dockerfile b/Tools/gsc/images/graphene_aks.latest.dockerfile index 4dcb3afa..5402a395 100644 --- a/Tools/gsc/images/graphene_aks.latest.dockerfile +++ b/Tools/gsc/images/graphene_aks.latest.dockerfile @@ -1,6 +1,5 @@ FROM ubuntu:18.04 AS graphene -# Add steps here to set up dependencies RUN env DEBIAN_FRONTEND=noninteractive apt-get update \ && env DEBIAN_FRONTEND=noninteractive apt-get install -y \ autoconf \ @@ -15,24 +14,22 @@ RUN env DEBIAN_FRONTEND=noninteractive apt-get update \ python3-protobuf \ wget -# Clone Graphene RUN git clone https://github.com/oscarlab/graphene.git /graphene -# Init submodules RUN cd /graphene \ && git fetch origin master \ && git checkout master -# Create SGX driver for header files RUN cd /graphene/Pal/src/host/Linux-SGX \ && git clone https://github.com/intel/SGXDataCenterAttestationPrimitives.git linux-sgx-driver \ && cd linux-sgx-driver \ && git checkout DCAP_1.7 && cp -r driver/linux/* . -# Build Graphene-SGX -RUN cd /graphene && ISGX_DRIVER_PATH=/graphene/Pal/src/host/Linux-SGX/linux-sgx-driver \ - make -s -j4 SGX=1 WERROR=1 \ - && true +RUN cd /graphene \ + && ISGX_DRIVER_PATH=/graphene/Pal/src/host/Linux-SGX/linux-sgx-driver \ + make -s -j WERROR=1 SGX=1 + + # Translate runtime symlinks to files RUN for f in $(find /graphene/Runtime -type l); do cp --remove-destination $(realpath $f) $f; done diff --git a/Tools/gsc/sign_manifest.py b/Tools/gsc/sign_manifest.py deleted file mode 100644 index 12d14a18..00000000 --- a/Tools/gsc/sign_manifest.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: LGPL-3.0-or-later -# Copyright (C) 2020 Intel Corp. -# Anjo Vahldiek-Oberwagner - -import os -import sys -import subprocess - -def generate_signature(exec_): - sign_process = subprocess.Popen([ - '/graphene/python/graphene-sgx-sign', - '-libpal', '/graphene/Runtime/libpal-Linux-SGX.so', - '-key', '/gsc-signer-key.pem', - '-output', f'{exec_}.manifest.sgx', - '-manifest', f'{exec_}.manifest', - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=os.environ.update({'PYTHONDONTWRITEBYTECODE':'1'})) - - _, err = sign_process.communicate() - - if (sign_process.returncode != 0 - or not os.path.exists(os.path.join('./', exec_ + '.manifest.sgx')) - or not os.path.exists(os.path.join('./', exec_ + '.sig'))): - print(err.decode()) - print('Finalize manifests failed due to graphene-sgx-sign failure.') - sys.exit(1) - -def main(args=None): - print('Signing application:', args[1]) - generate_signature(args[1]) - -if __name__ == '__main__': - main(sys.argv) diff --git a/Tools/gsc/templates/Dockerfile.ubuntu18.04.build.template b/Tools/gsc/templates/Dockerfile.ubuntu18.04.build.template index b27d02c1..cdac1cdd 100644 --- a/Tools/gsc/templates/Dockerfile.ubuntu18.04.build.template +++ b/Tools/gsc/templates/Dockerfile.ubuntu18.04.build.template @@ -1,38 +1,34 @@ -{% if not Graphene.Image %} - -{% include "Dockerfile.ubuntu18.04.compile.template" %} - +# Include previously-prepared Docker image with Graphene (if any) or compile Graphene from sources +{% if Graphene.Image %} +FROM gsc-{{Graphene.Image}} AS graphene {% else %} - -# Include graphene via Docker image -From {{Graphene.Image}} AS graphene - +{% include "Dockerfile.ubuntu18.04.compile.template" %} {% endif %} -# Integrate Graphene into the app image -# This file is used in a multistage docker build process, in which the previous image is named "graphene" +# Combine Graphene image with the original app image FROM {{app_image}} -# Update any packages RUN apt-get update \ && env DEBIAN_FRONTEND=noninteractive apt-get install -y \ + binutils \ + libprotobuf-c-dev \ + locales \ + locales-all \ + openssl \ python3 \ python3-pip \ python3-protobuf \ - libprotobuf-c-dev \ - binutils \ - openssl \ && python3 -B -m pip install protobuf jinja2 {% if debug %} -# Install GDB and related tools when Debug is enabled -RUN env DEBIAN_FRONTEND=noninteractive apt-get install -y \ - gdb \ - less \ - strace \ - vim +RUN env DEBIAN_FRONTEND=noninteractive apt-get install -y gdb less strace vim {% endif %} +RUN locale-gen en_US.UTF-8 +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + # Copy Graphene runtime and signer tools to /graphene RUN mkdir -p /graphene \ && mkdir -p /graphene/Runtime \ @@ -43,6 +39,7 @@ COPY --from=graphene /graphene/Runtime/ /graphene/Runtime COPY --from=graphene /graphene/python /graphene/python COPY --from=graphene /graphene/Pal/src/host/Linux-SGX/generated_offsets.py /graphene/python/ COPY --from=graphene /graphene/Tools/argv_serializer /graphene/Tools + {% if debug %} COPY --from=graphene /graphene/Pal/src/host/Linux-SGX/gdb_integration/sgx_gdb.so \ /graphene/Pal/src/host/Linux-SGX/gdb_integration/ @@ -52,21 +49,24 @@ COPY --from=graphene /graphene/Pal/src/host/Linux-SGX/gdb_integration/graphene_s /graphene/Pal/src/host/Linux-SGX/gdb_integration/ {% endif %} -# Copy template scripts and manifests -COPY apploader.sh ./ -COPY *.manifest ./ -COPY *.py ./ +# Copy helper scripts and Graphene manifest +COPY *.py / +COPY apploader.sh / +COPY entrypoint.manifest / +# Generate trusted arguments if required {% if not insecure_args %} -# Generate trusted arguments RUN /graphene/Tools/argv_serializer {{binary}} {{binary_arguments}} "{{"\" \"".join(cmd)}}" > trusted_argv {% endif %} -# Mark apploader.sh executable, finalize manifests, and remove intermediate scripts +# Docker entrypoint/cmd typically contains only the basename of the executable so create a symlink +RUN which {{binary}} | xargs ln -s || true + +# Mark apploader.sh executable, finalize manifest, and remove intermediate scripts RUN chmod u+x apploader.sh \ - && python3 -B finalize_manifest.py / {{binary}} \ + && python3 -B finalize_manifest.py \ && rm -f finalize_manifest.py # Define default command -ENTRYPOINT ["/bin/sh", "./apploader.sh"] +ENTRYPOINT ["/bin/bash", "./apploader.sh"] CMD [{% if insecure_args %} "{{'", "'.join(cmd)}}" {% endif %}] diff --git a/Tools/gsc/templates/Dockerfile.ubuntu18.04.compile.template b/Tools/gsc/templates/Dockerfile.ubuntu18.04.compile.template index 115d6ffb..22c22e07 100644 --- a/Tools/gsc/templates/Dockerfile.ubuntu18.04.compile.template +++ b/Tools/gsc/templates/Dockerfile.ubuntu18.04.compile.template @@ -1,6 +1,5 @@ FROM ubuntu:18.04 AS graphene -# Add steps here to set up dependencies RUN env DEBIAN_FRONTEND=noninteractive apt-get update \ && env DEBIAN_FRONTEND=noninteractive apt-get install -y \ autoconf \ @@ -15,24 +14,25 @@ RUN env DEBIAN_FRONTEND=noninteractive apt-get update \ python3-protobuf \ wget -# Clone Graphene RUN git clone {{Graphene.Repository}} /graphene -# Init submodules RUN cd /graphene \ && git fetch origin {{Graphene.Branch}} \ && git checkout {{Graphene.Branch}} -# Create SGX driver for header files RUN cd /graphene/Pal/src/host/Linux-SGX \ && git clone {{SGXDriver.Repository}} linux-sgx-driver \ && cd linux-sgx-driver \ && git checkout {{SGXDriver.Branch}} -# Build Graphene-SGX -RUN cd /graphene && ISGX_DRIVER_PATH=/graphene/Pal/src/host/Linux-SGX/linux-sgx-driver \ - make -s -j4 SGX=1 {% if debug %} DEBUG=1 {% endif %}WERROR=1 \ - {% if linux %} && make -s -j4 WERROR=1{% if debug %} DEBUG=1{% endif %}{% else %} && true{%endif %} +RUN cd /graphene \ + && ISGX_DRIVER_PATH=/graphene/Pal/src/host/Linux-SGX/linux-sgx-driver \ + make -s -j WERROR=1 SGX=1 {% if debug %} DEBUG=1 {% endif %} + +{% if linux %} +RUN cd /graphene \ + && make -s -j WERROR=1 {% if debug %} DEBUG=1 {% endif %} +{% endif %} # Translate runtime symlinks to files RUN for f in $(find /graphene/Runtime -type l); do cp --remove-destination $(realpath $f) $f; done diff --git a/Tools/gsc/templates/Dockerfile.ubuntu18.04.sign.template b/Tools/gsc/templates/Dockerfile.ubuntu18.04.sign.template new file mode 100644 index 00000000..f5626dae --- /dev/null +++ b/Tools/gsc/templates/Dockerfile.ubuntu18.04.sign.template @@ -0,0 +1,24 @@ +# Sign image in a separate stage to ensure that signing key is never part of the final image + +FROM {{image}} as unsigned_image + +RUN locale-gen en_US.UTF-8 +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +COPY gsc-signer-key.pem /gsc-signer-key.pem + +RUN /graphene/python/graphene-sgx-sign \ + -libpal /graphene/Runtime/libpal-Linux-SGX.so \ + -key /gsc-signer-key.pem \ + -manifest /entrypoint.manifest \ + -output /entrypoint.manifest.sgx + +# This trick removes all temporary files from the previous commands (including gsc-signer-key.pem) +FROM {{image}} + +COPY --from=unsigned_image /*.sig / +COPY --from=unsigned_image /*.sgx / + +RUN rm /graphene/python/graphene-sgx-sign /graphene/python/graphenelibos/sgx_sign.py diff --git a/Tools/gsc/templates/Dockerfile.ubuntu18.04.sign_manifests.template b/Tools/gsc/templates/Dockerfile.ubuntu18.04.sign_manifests.template deleted file mode 100644 index 9a987943..00000000 --- a/Tools/gsc/templates/Dockerfile.ubuntu18.04.sign_manifests.template +++ /dev/null @@ -1,14 +0,0 @@ -# We sign the image in a multi-stage build to ensure that the signing key is never part of the -# final image. -FROM {{image}} as unsigned_image - -COPY gsc-signer-key.pem /gsc-signer-key.pem - -RUN python3 -B sign_manifests.py #TODO: missing arg here? - -FROM {{image}} - -COPY --from=unsigned_image {{working_dir}}*.sig ./ -COPY --from=unsigned_image {{working_dir}}*.sgx ./ - -RUN rm sign_manifests.py /graphene/python/graphene-sgx-sign /graphene/python/graphenelibos/sgx_sign.py diff --git a/Tools/gsc/templates/apploader.template b/Tools/gsc/templates/apploader.template index 6ac49820..81800513 100644 --- a/Tools/gsc/templates/apploader.template +++ b/Tools/gsc/templates/apploader.template @@ -1,19 +1,13 @@ -#!/bin/sh +#!/usr/bin/env bash set -ex # Set default PAL to Linux-SGX if [ -z "$GSC_PAL" ] || [ "$GSC_PAL" == "Linux-SGX" ] then - # Set SGX default values for PAL GSC_PAL=Linux-SGX - - # Generate EINITOKEN - /graphene/python/graphene-sgx-get-token -output {{binary}}.token -sig {{binary}}.sig - - # Run the application in Graphene - /graphene/Runtime/pal-$GSC_PAL /graphene/Runtime/libpal-$GSC_PAL.so init {{binary}} {% if insecure_args %}{{binary_arguments}} "${@}"{% endif %} + /graphene/python/graphene-sgx-get-token -output entrypoint.token -sig entrypoint.sig + /graphene/Runtime/pal-$GSC_PAL /graphene/Runtime/libpal-$GSC_PAL.so init entrypoint {% if insecure_args %}{{binary_arguments}} "${@}"{% endif %} else - # Run the application in Graphene - /graphene/Runtime/pal-$GSC_PAL /graphene/Runtime/libpal-$GSC_PAL.so init {{binary}} {{binary_arguments}} "${@}" + /graphene/Runtime/pal-$GSC_PAL /graphene/Runtime/libpal-$GSC_PAL.so init entrypoint {{binary_arguments}} "${@}" fi diff --git a/Tools/gsc/templates/manifest.template b/Tools/gsc/templates/entrypoint.manifest.template similarity index 65% rename from Tools/gsc/templates/manifest.template rename to Tools/gsc/templates/entrypoint.manifest.template index d684e09e..f83207dc 100644 --- a/Tools/gsc/templates/manifest.template +++ b/Tools/gsc/templates/entrypoint.manifest.template @@ -1,9 +1,6 @@ +libos.entrypoint = "file:/{{binary}}" loader.preload = "file:/graphene/Runtime/libsysdb.so" -{% if insecure_args %} -loader.arg0_override = "{{binary}}" -{% endif %} -libos.entrypoint = "file:{{binary}}" loader.env.LD_LIBRARY_PATH = "/graphene/Runtime:{{"{{library_paths}}"}}" loader.env.PATH = "{{"{{env_path}}"}}" loader.log_level = {% if debug %} "debug" {% else %} "error" {% endif %} @@ -12,18 +9,16 @@ fs.root.type = "chroot" fs.root.path = "/" fs.root.uri = "file:/" -# With fs.root Graphene's working directory is '/', setting fs.start_dir changes the -# working directory to the desired location +# Graphene's default working dir is '/', so change the working directory to the desired one fs.start_dir = "{{working_dir}}" -# Start at static addresses (otherwise breaks when non-relocatable executables are used) sgx.nonpie_binary = 1 {% if insecure_args %} -# !! INSECURE !! Allow passing command-line arguments from the host without validation +# !! INSECURE !! Allow passing command-line arguments from the host without validation. # Most Docker images rely on runtime arguments and hence, a more general technique is required. -# The issue is documented for future release of GSC in issue #1520. -# https://github.com/oscarlab/graphene/issues/1520 +# The issue is documented at https://github.com/oscarlab/graphene/issues/1520. +loader.arg0_override = "{{binary}}" loader.insecure__use_cmdline_argv = 1 {% else %} loader.argv_src_file = "file:trusted_argv" diff --git a/Tools/gsc/test/Makefile b/Tools/gsc/test/Makefile index c84cee89..69ca1981 100644 --- a/Tools/gsc/test/Makefile +++ b/Tools/gsc/test/Makefile @@ -1,6 +1,11 @@ TESTCASES ?= python3 python3-trusted-args base-python3 hello-world nodejs bash numpy pytorch DISTRIBUTIONS ?= ubuntu18.04 -GRAPHENE_BRANCH ?= a37a85baf5ab6e0901a777313072d62b4e75fc3a # the last working commit. TODO: update + +GRAPHENE_REPO ?= https://github.com/oscarlab/graphene.git +GRAPHENE_BRANCH ?= master +SGXDRIVER_REPO ?= https://github.com/01org/linux-sgx-driver.git +SGXDRIVER_BRANCH ?= sgx_driver_1.9 + DOCKER_BUILD_FLAGS ?= --rm --no-cache GSC_BUILD_FLAGS ?= --rm --no-cache TESTS = $(foreach D,$(DISTRIBUTIONS),$(foreach T,$(TESTCASES),$D-$T)) @@ -18,9 +23,13 @@ IMAGE_SUFFIX ?= .PHONY: all all: $(KEY_FILE) for d in $(DISTRIBUTIONS); do \ - sed -e 's/Distro: \".*\"/Distro: \"'$${d}'\"/' \ - -e 's/Branch: \"master\"/Branch: \"'$(GRAPHENE_BRANCH)'\"/' \ - ../config.yaml.template > config-$${d}.yaml; \ + echo "Distro: \"$${d}\"" > config-$${d}.yaml; \ + echo "Graphene:" >> config-$${d}.yaml; \ + echo " Repository: \"$(GRAPHENE_REPO)\"" >> config-$${d}.yaml; \ + echo " Branch: \"$(GRAPHENE_BRANCH)\"" >> config-$${d}.yaml; \ + echo "SGXDriver:" >> config-$${d}.yaml; \ + echo " Repository: \"$(SGXDRIVER_REPO)\"" >> config-$${d}.yaml; \ + echo " Branch: \"$(SGXDRIVER_BRANCH)\"" >> config-$${d}.yaml; \ $(MAKE) $(addprefix gsc-$${d}-, $(TESTCASES)) || exit 1; \ done diff --git a/Tools/gsc/test/ubuntu18.04-nodejs.dockerfile b/Tools/gsc/test/ubuntu18.04-nodejs.dockerfile index dc17db6d..b7492ab5 100644 --- a/Tools/gsc/test/ubuntu18.04-nodejs.dockerfile +++ b/Tools/gsc/test/ubuntu18.04-nodejs.dockerfile @@ -2,13 +2,11 @@ From ubuntu:18.04 WORKDIR /app -RUN apt-get update - -RUN apt-get -y install nodejs \ +RUN apt-get update \ + && apt-get -y install nodejs \ && mkdir -p /graphene/Examples -# The build environment of this Dockerfile should point to the root of Graphene's Examples -# directory. +# Build environment of this Dockerfile should point to the root of Graphene's Examples/ COPY nodejs/ /graphene/Examples CMD ["node"] diff --git a/Tools/gsc/test/ubuntu18.04-numpy.dockerfile b/Tools/gsc/test/ubuntu18.04-numpy.dockerfile index 50c5ca6b..d7ba92f4 100644 --- a/Tools/gsc/test/ubuntu18.04-numpy.dockerfile +++ b/Tools/gsc/test/ubuntu18.04-numpy.dockerfile @@ -1,13 +1,11 @@ From ubuntu:18.04 -RUN apt-get update - -RUN apt-get install -y python3 python3-pip git \ +RUN apt-get update \ + && apt-get install -y python3 python3-pip git \ && pip3 install numpy \ && mkdir -p /graphene/Examples -# The build environment of this Dockerfile should point to the root of Graphene's Examples -# directory. +# Build environment of this Dockerfile should point to the root of Graphene's Examples/ COPY python-scipy-insecure/ /graphene/Examples CMD ["python3"] diff --git a/Tools/gsc/test/ubuntu18.04-python3-trusted-args.dockerfile b/Tools/gsc/test/ubuntu18.04-python3-trusted-args.dockerfile index 7549d2eb..e691e3ed 100644 --- a/Tools/gsc/test/ubuntu18.04-python3-trusted-args.dockerfile +++ b/Tools/gsc/test/ubuntu18.04-python3-trusted-args.dockerfile @@ -1,7 +1,6 @@ From ubuntu:18.04 -RUN apt-get update - -RUN apt-get install -y python3 +RUN apt-get update \ + && apt-get install -y python3 CMD ["python3", "-c", "print('HelloWorld!')"] diff --git a/Tools/gsc/test/ubuntu18.04-python3.dockerfile b/Tools/gsc/test/ubuntu18.04-python3.dockerfile index f7fa6da4..79859c18 100644 --- a/Tools/gsc/test/ubuntu18.04-python3.dockerfile +++ b/Tools/gsc/test/ubuntu18.04-python3.dockerfile @@ -1,12 +1,10 @@ From ubuntu:18.04 -RUN apt-get update - -RUN apt-get install -y python3 \ +RUN apt-get update \ + && apt-get install -y python3 \ && mkdir -p /graphene/Examples -# The build environment of this Dockerfile should point to the root of Graphene's Examples -# directory. +# Build environment of this Dockerfile should point to the root of Graphene's Examples/ COPY python-simple/ /graphene/Examples CMD ["python3"] diff --git a/Tools/gsc/test/ubuntu18.04-pytorch.dockerfile b/Tools/gsc/test/ubuntu18.04-pytorch.dockerfile index b53a3970..d1ee1e37 100644 --- a/Tools/gsc/test/ubuntu18.04-pytorch.dockerfile +++ b/Tools/gsc/test/ubuntu18.04-pytorch.dockerfile @@ -1,13 +1,11 @@ From ubuntu:18.04 -RUN apt-get update - -RUN apt-get install -y python3 python3-pip \ +RUN apt-get update \ + && apt-get install -y python3 python3-pip \ && pip3 install torch torchvision \ && mkdir -p /graphene/Examples -# The build environment of this Dockerfile should point to the root of Graphene's Examples -# directory. +# Build environment of this Dockerfile should point to the root of Graphene's Examples/ COPY pytorch/ /graphene/Examples WORKDIR /graphene/Examples diff --git a/python/graphenelibos/sgx_sign.py b/python/graphenelibos/sgx_sign.py index 7ca39779..f1f950d3 100644 --- a/python/graphenelibos/sgx_sign.py +++ b/python/graphenelibos/sgx_sign.py @@ -65,7 +65,7 @@ def parse_size(value): def read_manifest(filename): manifest = dict() manifest_layout = [] - with open(filename, 'r') as file: + with open(filename, 'r', encoding='UTF-8') as file: for line in file: if line == '': manifest_layout.append((None, None)) @@ -107,7 +107,7 @@ def exec_sig_manifest(args): def output_manifest(filename, manifest, manifest_layout): - with open(filename, 'w') as file: + with open(filename, 'w', encoding='UTF-8') as file: written = [] file.write('# DO NOT MODIFY. THIS FILE WAS AUTO-GENERATED.\n\n') @@ -831,7 +831,7 @@ def make_depend(args): dependencies.add(args['libpal']) dependencies.add(args['key']) - with open(output, 'w') as file: + with open(output, 'w', encoding='UTF-8') as file: manifest_sgx = output if manifest_sgx.endswith('.d'): manifest_sgx = manifest_sgx[:-len('.d')]