diff --git a/.ci/ubuntu20.04.dockerfile b/.ci/ubuntu20.04.dockerfile index 92dfb70a6d..32e2444257 100644 --- a/.ci/ubuntu20.04.dockerfile +++ b/.ci/ubuntu20.04.dockerfile @@ -56,6 +56,7 @@ RUN apt-get update && env DEBIAN_FRONTEND=noninteractive apt-get install -y \ python3-lxml \ python3-numpy \ python3-pip \ + python3-pkg-resources \ python3-protobuf \ python3-pyelftools \ python3-pytest \ diff --git a/.ci/ubuntu22.04.dockerfile b/.ci/ubuntu22.04.dockerfile index b89ae35610..7979c8500f 100644 --- a/.ci/ubuntu22.04.dockerfile +++ b/.ci/ubuntu22.04.dockerfile @@ -57,6 +57,7 @@ RUN apt-get update && env DEBIAN_FRONTEND=noninteractive apt-get install -y \ python3-lxml \ python3-numpy \ python3-pip \ + python3-pkg-resources \ python3-protobuf \ python3-pyelftools \ python3-pytest \ diff --git a/Documentation/conf.py b/Documentation/conf.py index a103723554..aa954b34ad 100644 --- a/Documentation/conf.py +++ b/Documentation/conf.py @@ -156,7 +156,10 @@ def setup(app): manpages_url = 'https://manpages.debian.org/{path}' -intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'click': ('https://click.palletsprojects.com/en/latest', None), +} # -- Options for HTML output ------------------------------------------------- diff --git a/Documentation/index.rst b/Documentation/index.rst index 1490701c19..68dd9b2887 100644 --- a/Documentation/index.rst +++ b/Documentation/index.rst @@ -191,6 +191,7 @@ Indices and tables devel/debugging devel/packaging python/api + python/writing-sgx-sign-plugins devel/new-syscall libos/libos-init pal/host-abi diff --git a/Documentation/python/writing-sgx-sign-plugins.rst b/Documentation/python/writing-sgx-sign-plugins.rst new file mode 100644 index 0000000000..dc398105f0 --- /dev/null +++ b/Documentation/python/writing-sgx-sign-plugins.rst @@ -0,0 +1,90 @@ +.. default-domain:: py +.. highlight:: py + +Writing plugins for signing SGX enclaves +======================================== + +SGX cryptosystem uses RSA-3072 with modulus 3 for signing a SIGSTRUCT. However, +there are different arrangements where suitable keys are kept and used for +operations. A |~| keyfile is not always available (e.g., HSMs explicitly prevent +users from extracting keys), so we need adaptable ways of signing enclaves. This +document describes how to implement a |~| plugin that allows Gramine to access +different APIs for signing SGX enclaves. + +You need to provide a |~| click subcommand, which is a |~| Python function +wrapped in :func:`click.command` decorator. This command can accept any +command-line arguments you need to complete the signing (like path to keyfile, +URL to some external API, PIN to smartcard). It is strongly recommended that you +provide ``--help-PLUGIN`` option (with your plugin name substituted for +``PLUGIN``). Also, consider prefixing your options with ``--PLUGIN-`` to avoid +conflicting with generic options. + +Furthermore, your subcommand needs to be packaged into Python distribution, +which will include an entry point from ``gramine.sgx_sign`` group. The entry +point needs to be named as your plugin and the callable it points to needs to be +the click command. + +The click command will be called with ``standalone_mode=False``. It needs to +return signing function that will be passed to ``Sigstruct.sign``. The signing +function should return a |~| 3-tuple: + +- exponent (always ``3``) +- modulus (:class:`int`) +- signature (:class:`int`) + +The signing function accepts a |~| single argument, the data to be signed. If +your signing function needs to accept additional arguments, use +:func:`functools.partial`. + +Alternatively, the click command can return 2-tuple of: + +- the signing function, as described above; +- iterable of local files that were accessed during signature generation, for + the purpose of tracking dependencies + +If you return just the function, it's equivalent to returning 2-tuple with empty +iterable, i.e. no dependent files. + +.. seealso:: + + https://setuptools.pypa.io/en/latest/userguide/entry_point.html#advertising-behavior + Introduction to entrypoints + + https://packaging.python.org/en/latest/specifications/entry-points/ + Entrypoints specification + +Example +------- + +For full example, please see ``sgx_sign.py`` file (note that ``graminelibos`` +package is not packaged with ``setuptools``, so metadata is provided manually). + +The relevant parts are: + +.. code-block:: python + :caption: sgx_sign.py + + @click.command(add_help_option=False) + @click.help_option('--help-file') + @click.option('--key', '-k', metavar='FILE', + type=click.Path(exists=True, dir_okay=False), + default=os.fspath(SGX_RSA_KEY_PATH), + help='specify signing key (.pem) file') + def sign_with_file(key): + return functools.partial(sign, key=key), [key] + + def sign(data, *, key): + # sign data with key + return exponent, modulus, signature + +.. code-block:: python + :caption: setup.py + + setuptools.setup( + ..., + entry_points={ + 'gramine.sgx_sign': [ + 'file = graminelibos.sgx_sign:sign_with_file', + ] + } + ) diff --git a/debian/control b/debian/control index 106e06141e..8d7fa5eccf 100644 --- a/debian/control +++ b/debian/control @@ -37,6 +37,7 @@ Depends: libcurl4 (>= 7.58), libprotobuf-c1, python3, + python3 (>= 3.10) | python3-pkg-resources, python3-click, python3-cryptography, python3-jinja2, diff --git a/debian/gramine.install b/debian/gramine.install index 5bb28165ba..7eaf2cb1e2 100644 --- a/debian/gramine.install +++ b/debian/gramine.install @@ -1,7 +1,8 @@ usr/bin/gramine-* usr/bin/is-sgx-available -usr/lib/python3/dist-packages/graminelibos/ usr/lib/python3/dist-packages/_graminelibos_offsets.py +usr/lib/python3/dist-packages/graminelibos/ +usr/lib/python3/dist-packages/graminelibos.dist-info/ usr/lib/${DEB_HOST_MULTIARCH}/gramine/direct/libpal.so usr/lib/${DEB_HOST_MULTIARCH}/gramine/direct/loader usr/lib/${DEB_HOST_MULTIARCH}/gramine/libsysdb.so diff --git a/python/gramine-sgx-sign b/python/gramine-sgx-sign index b3ecf4af14..69973e23b6 100755 --- a/python/gramine-sgx-sign +++ b/python/gramine-sgx-sign @@ -4,30 +4,80 @@ # Borys Popławski import datetime -import os +import sys +import textwrap import click from graminelibos import ( - Manifest, get_tbssigstruct, sign_with_local_key, SGX_LIBPAL, SGX_RSA_KEY_PATH, + Manifest, get_tbssigstruct, SGX_LIBPAL, ) -@click.command() -@click.option('--output', '-o', type=click.Path(), required=True, - help='Output .manifest.sgx file (manifest augmented with autogenerated fields)') -@click.option('--libpal', '-l', type=click.Path(exists=True, dir_okay=False), default=SGX_LIBPAL, - help='Input libpal file') -@click.option('--key', '-k', type=click.Path(exists=True, dir_okay=False), - default=os.fspath(SGX_RSA_KEY_PATH), - help='specify signing key (.pem) file') -@click.option('--manifest', '-m', 'manifest_file', type=click.File('r', encoding='utf-8'), - required=True, help='Input .manifest file') -@click.option('--sigfile', '-s', help='Output .sig file') -@click.option('--depfile', type=click.File('w'), help='Generate dependencies for .manifest.sgx ' - 'and .sig files') -@click.option('--verbose/--quiet', '-v/-q', default=True, help='Display details (on by default)') -def main(output, libpal, key, manifest_file, sigfile, depfile, verbose): - # pylint: disable=too-many-arguments +# TODO: after python (>= 3.10) simplify this +# NOTE: we can't `try: importlib.metadata`, because the API has changed between 3.9 and 3.10 +# (in 3.9 and in backported importlib_metadata entry_points() doesn't accept group argument) +if sys.version_info >= (3, 10): + from importlib.metadata import entry_points # pylint: disable=import-error,no-name-in-module +else: + from pkg_resources import iter_entry_points as entry_points + +def list_sgx_sign_plugins(): + return tuple(ep.name for ep in entry_points(group='gramine.sgx_sign')) + +_sgx_sign_plugins = list_sgx_sign_plugins() + +def get_sgx_sign_plugin(name): + for ep in entry_points(group='gramine.sgx_sign'): + if ep.name == name: + return ep.load() + raise KeyError(name) + +@click.command( + context_settings={'ignore_unknown_options': True}, + epilog=textwrap.dedent(f''' + Use --with=PLUGIN --help-PLUGIN to get help about particular plugin. + + Available plugins: {", ".join(_sgx_sign_plugins)}'''), +) +@click.option('--with', 'with_', metavar='PLUGIN', + type=click.Choice(_sgx_sign_plugins), + default='file', + help='Choose plugin with which to sign the enclave (default: file)') +@click.option('--output', '-o', + type=click.Path(), + required=True, + help='Output .manifest.sgx file (manifest augmented with autogenerated fields)') +@click.option('--libpal', '-l', + type=click.Path(exists=True, dir_okay=False), + default=SGX_LIBPAL, + help='Input libpal file') +@click.option('--manifest', '-m', 'manifest_file', + type=click.File('r', encoding='utf-8'), + required=True, + help='Input .manifest file') +@click.option('--sigfile', '-s', + help='Output .sig file') +@click.option('--depfile', + type=click.File('w'), + help='Generate dependencies for .manifest.sgx and .sig files') +@click.option('--verbose/--quiet', '-v/-q', + default=True, + help='Display details (on by default)') +@click.argument('plugin_args', + nargs=-1, + type=click.UNPROCESSED) +def main(with_, output, libpal, manifest_file, sigfile, depfile, verbose, plugin_args): + # pylint: disable=too-many-arguments, too-many-locals + + sign_func = get_sgx_sign_plugin(with_)(args=plugin_args, standalone_mode=False) + try: + it = iter(sign_func) + # no TypeError, therefore we've got tuple or list, or sth else iterable + sign_func, extra_deps = it + except TypeError: + # sign_func is probably just a callable (no need to check, will break later if it isn't) + # and extra dependencies were not provided + extra_deps = () manifest = Manifest.load(manifest_file) @@ -45,7 +95,7 @@ def main(output, libpal, key, manifest_file, sigfile, depfile, verbose): today = datetime.date.today() sigstruct = get_tbssigstruct(output, today, libpal, verbose=verbose) - sigstruct.sign(sign_with_local_key, key) + sigstruct.sign(sign_func) with open(sigfile, 'wb') as f: f.write(sigstruct.to_bytes()) @@ -54,7 +104,7 @@ def main(output, libpal, key, manifest_file, sigfile, depfile, verbose): # Dependencies: # # - `.manifest.sgx` depends on all files we just expanded - # - `.sig` additionally depends on libpal and key + # - `.sig` additionally depends on libpal # # TODO (Ninja 1.10): We print all these as dependencies for `.manifest.sgx`. This will still # cause `.sig` to be rebuilt when necessary: we build both these files together, so it's not @@ -62,7 +112,7 @@ def main(output, libpal, key, manifest_file, sigfile, depfile, verbose): # # This is a workaround for the fact that Ninja prior to version 1.10 does not # support depfiles with multiple outputs (and parses such depfiles incorrectly). - deps = [*expanded, libpal, key] + deps = [*expanded, libpal, *extra_deps] depfile.write(f'{output}:') for filename in deps: diff --git a/python/graminelibos.dist-info/METADATA.in b/python/graminelibos.dist-info/METADATA.in new file mode 100644 index 0000000000..f20b5e2bce --- /dev/null +++ b/python/graminelibos.dist-info/METADATA.in @@ -0,0 +1,5 @@ +Metadata-Version: 2.1 +Name: @NAME@libos +Version: @VERSION@ +Home-page: https://gramineproject.io/ +License: @LICENSE@ diff --git a/python/graminelibos.dist-info/entry_points.txt b/python/graminelibos.dist-info/entry_points.txt new file mode 100644 index 0000000000..4eb116f1b4 --- /dev/null +++ b/python/graminelibos.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[gramine.sgx_sign] +file = graminelibos.sgx_sign:sign_with_file diff --git a/python/graminelibos.dist-info/meson.build b/python/graminelibos.dist-info/meson.build new file mode 100644 index 0000000000..ec879ff744 --- /dev/null +++ b/python/graminelibos.dist-info/meson.build @@ -0,0 +1,15 @@ +install_dir = python3_platlib / 'graminelibos.dist-info' +conf = configuration_data() +conf.set('NAME', meson.project_name()) +conf.set('VERSION', meson.project_version()) +conf.set('LICENSE', ', '.join(meson.project_license())) + +# https://packaging.python.org/en/latest/specifications/core-metadata/ +configure_file( + input: 'METADATA.in', + output: 'METADATA', + install_dir: install_dir, + configuration: conf, +) + +install_data('entry_points.txt', install_dir: install_dir) diff --git a/python/graminelibos/sgx_sign.py b/python/graminelibos/sgx_sign.py index e04dcd362c..0310bc62dd 100644 --- a/python/graminelibos/sgx_sign.py +++ b/python/graminelibos/sgx_sign.py @@ -6,12 +6,15 @@ # Wojtek Porczyk # +import functools import hashlib import os import pathlib import struct import subprocess +import click + from cryptography.hazmat import backends from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -539,6 +542,15 @@ def get_tbssigstruct(manifest_path, date, libpal=SGX_LIBPAL, verbose=False): return sig +@click.command(add_help_option=False) +@click.help_option('--help-file') +@click.option('--key', '-k', metavar='FILE', + type=click.Path(exists=True, dir_okay=False), + default=os.fspath(SGX_RSA_KEY_PATH), + help='specify signing key (.pem) file') +def sign_with_file(key): + return functools.partial(sign_with_local_key, key=key), [key] + def sign_with_local_key(data, key): """Signs *data* using *key*. diff --git a/python/meson.build b/python/meson.build index f083ff7588..dd673d2f5c 100644 --- a/python/meson.build +++ b/python/meson.build @@ -1,4 +1,5 @@ subdir('graminelibos') +subdir('graminelibos.dist-info') install_data([ 'gramine-gen-depend',