Skip to content

Commit

Permalink
[python] Add sgx-sign plugins
Browse files Browse the repository at this point in the history
Signed-off-by: Wojtek Porczyk <[email protected]>
  • Loading branch information
woju committed Jul 20, 2023
1 parent 24319e4 commit c1721d7
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 23 deletions.
1 change: 1 addition & 0 deletions .ci/ubuntu20.04.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions .ci/ubuntu22.04.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
5 changes: 4 additions & 1 deletion Documentation/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 -------------------------------------------------

Expand Down
1 change: 1 addition & 0 deletions Documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions Documentation/python/writing-sgx-sign-plugins.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.. 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. It will be called with
``standalone_mode=False``.

The command needs to return signing function that will be passed to
``Sigstruct.sign``. The signing function should return a |~| 3-tuple:

- exponent (always ``3``)
- modulus (int)
- signature (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`.

In addition to the signing function, you can return an iterable of local files
that were accessed during signature generation (for the purpose of tracking
dependencies).

Your 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 function 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.

.. 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',
]
}
)
1 change: 1 addition & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Depends:
libcurl4 (>= 7.58),
libprotobuf-c1,
python3,
python3 (>= 3.10) | python3-pkg-resources,
python3-click,
python3-cryptography,
python3-jinja2,
Expand Down
3 changes: 2 additions & 1 deletion debian/gramine.install
Original file line number Diff line number Diff line change
@@ -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
Expand Down
92 changes: 71 additions & 21 deletions python/gramine-sgx-sign
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,80 @@
# Borys Popławski <[email protected]>

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)

Expand All @@ -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())
Expand All @@ -54,15 +104,15 @@ 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
# possible to rebuild one without the other.
#
# 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:
Expand Down
5 changes: 5 additions & 0 deletions python/graminelibos.dist-info/METADATA.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Metadata-Version: 2.1
Name: @NAME@libos
Version: @VERSION@
Home-page: https://gramineproject.io/
License: @LICENSE@
2 changes: 2 additions & 0 deletions python/graminelibos.dist-info/entry_points.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[gramine.sgx_sign]
file = graminelibos.sgx_sign:sign_with_file
15 changes: 15 additions & 0 deletions python/graminelibos.dist-info/meson.build
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions python/graminelibos/sgx_sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
# Wojtek Porczyk <[email protected]>
#

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
Expand Down Expand Up @@ -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*.
Expand Down
1 change: 1 addition & 0 deletions python/meson.build
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
subdir('graminelibos')
subdir('graminelibos.dist-info')

install_data([
'gramine-gen-depend',
Expand Down

0 comments on commit c1721d7

Please sign in to comment.