"""Main module for ``auditee`` tool."""
import json
import os
import pathlib
import tempfile
import subprocess
from collections import namedtuple
from urllib.parse import urlparse
import git
import yaml
from blessings import Terminal
from colorama import init as init_colorama # , Fore, Back, Style
from python_on_whales import docker
from auditee.errors import AuditeeError
from auditee.sgx import Sigstruct, sign as sgx_sign
from auditee.bindings.quote import read_sgx_quote_body_b64
init_colorama()
term = Terminal()
ReproducibilityReport = namedtuple(
"ReproducibilityReport", ("mrenclave", "isvprodid", "isvsvn", "mrsigner")
)
ReportItem = namedtuple("ReportItem", ("matches", "expected", "computed"))
[docs]def build(source_code, *, docker_build_progress=False):
"""Build an enclave binary for the given source code.
The source code is expected to contain a file :file:`.auditee.yml`,
which instructs how to build the enclave. The supported builders
are ``nix-build`` and ``docker``.
Parameters
----------
source_code: str
Local file path to the source code where the enclave to be built
is located.
Raises
------
IOError:
If the ``.auditee.yml`` file is not found.
Returns
-------
str
File path to the enclave binary that was built.
"""
source_code_path = pathlib.Path(source_code).resolve()
auditee_file = source_code_path.joinpath(".auditee.yml")
with open(auditee_file) as f:
auditee_config = yaml.safe_load(f)
enclave_build_config = auditee_config["enclaves"][0]
# Get absolute path of enclave config
# enclave_config = source_code_path.joinpath(enclave_build_config["enclave_config"])
build_config = enclave_build_config["build"]
builder = build_config["builder"]
build_kwargs = build_config["build_kwargs"]
os.chdir(source_code_path)
if builder == "docker":
docker.build(
output={"type": "local", "dest": "/tmp/out"},
progress=docker_build_progress,
**build_kwargs,
)
elif builder == "nix-build":
popenargs = [builder, build_kwargs.get("file", "default.nix")]
returncode = subprocess.run(popenargs).returncode
if returncode != 0:
raise RuntimeError(f"Error building enclave with {builder}")
os.chdir("..")
return source_code_path.joinpath(
build_config["output_dir"], build_config["enclave_file"]
)
[docs]def sign(
unsigned_enclave,
enclave_config,
*,
signed_enclave="/tmp/enclave.signed.so",
signing_key=None,
):
"""Sign the given enclave.
Parameters
----------
unsigned_enclave: str
Local file path to the unsigned enclave binary.
enclave_config: str
Local file path to the enclave configuration file.
signed_enclave: str, optional
Local file path where the signed enclave should be written to.
Defaults to :file:`/tmp/enclave.signed.so`.
signing_key: str, optional
Local file path to a signing key with which to sign the enclave.
When signing an enclave just for test purposes, such as
verifying the reproducibility of an enclave, one can fall back
on the default which is a key file that is packaged with
``auditee``.
Raises
------
:py:exc:`~.errors.SGXSignError`:
If something wrong happen when invoking the ``sgx_sign`` tool.
Returns
-------
str:
File path where the signed enclave was written to.
"""
if signing_key is None:
signing_key = (
pathlib.Path(__file__).parent.resolve().joinpath("signing_key.pem")
)
else:
signing_key = pathlib.Path(signing_key).resolve()
sgx_sign(
unsigned_enclave,
key=signing_key,
out=signed_enclave,
config=pathlib.Path(enclave_config).resolve(),
)
return signed_enclave
def build_and_sign(
source_code, *, signed_enclave="/tmp/enclave.signed.so", signing_key=None
):
# parse auditee config file in source code
# TODO extract this functionality into a function
source_code = pathlib.Path(source_code).resolve()
auditee_file = source_code.joinpath(".auditee.yml")
with open(auditee_file) as f:
auditee_config = yaml.safe_load(f)
enclave_build_config = auditee_config["enclaves"][0]
# Get absolute path of enclave config
enclave_config = source_code.joinpath(enclave_build_config["enclave_config"])
unsigned_enclave = build(source_code)
return sign(
unsigned_enclave,
enclave_config,
signed_enclave=signed_enclave,
signing_key=signing_key,
)
def extract_sigstruct(signed_enclave):
""" """
return Sigstruct.from_enclave_file(signed_enclave)
[docs]def verify_ias_report(report, signed_enclave):
"""Verify whether the ``MRENCLAVE`` in the given IAS report matches
against the MRENCLAVE of the given signed enclave.
Parameters
----------
report: str
Local file path to the IAS report, in json format.
signed_enclave: str
Path to the signed enclave file.
Returns
-------
bool:
``True`` if the MRENCLAVEs match, ``False`` otherwise.
Examples
--------
>>> from auditee.enclave import verify_ias_report
>>> verify_ias_report('ias_report.json', 'enclave.signed.so')
Succeed.
- Provided enclave MRENCLAVE: b7af1907e21b4eb240d3c3c6880e3892e45af383196d7aa326c35e2a8c71ef63
- IAS report MRENCLAVE: b7af1907e21b4eb240d3c3c6880e3892e45af383196d7aa326c35e2a8c71ef63
MRENCLAVES match!
True
"""
sigstruct = Sigstruct.from_enclave_file(signed_enclave)
return _verify_ias_report(report, sigstruct)
def _verify_ias_report(report, sigstruct):
""" """
with open(report) as f:
report_data = json.load(f)
report_body = report_data["body"]
isv_enclave_quote_body = report_body["isvEnclaveQuoteBody"]
quote_body = read_sgx_quote_body_b64(isv_enclave_quote_body)
report_mrenclave = bytes(quote_body.report_body.mr_enclave.m)
print(
f"- Provided enclave MRENCLAVE: "
f"\t\t{term.bold}{sigstruct.mrenclave.hex()}{term.normal}"
)
print(
f"- IAS report MRENCLAVE: "
f"\t\t{term.bold}{report_mrenclave.hex()}{term.normal}"
)
print()
match = report_mrenclave == sigstruct.mrenclave
if match:
print(f"{term.green}MRENCLAVES match!{term.normal}")
else:
print(f"{term.red}MRENCLAVES do not match!{term.normal}")
return match
def verify_signed_enclave(signed_enclave, sigstruct_data):
""" """
def print_report(dev_mrenclave, audit_mrenclave, *, ias_report_mrenclave=None):
print(f"\n{term.bold}Reproducibility Report\n----------------------{term.normal}")
print(f"- Signed enclave MRENCLAVE: \t\t\t{term.bold}{dev_mrenclave}{term.normal}")
print(
f"- Built-from-source enclave MRENCLAVE: "
f"\t\t{term.bold}{audit_mrenclave}{term.normal}"
)
if ias_report_mrenclave:
print(
f"- IAS report MRENCLAVE: "
f"\t\t\t{term.bold}{ias_report_mrenclave}{term.normal}"
)
print()
[docs]def verify_mrenclave(
source_code,
signed_enclave,
*,
ias_report=None,
signing_key=None,
docker_build_progress=False,
):
"""Given some source code, a signed enclave, and optionally, a
remote attestation verification report from Intel's attestation
service (IAS), verify whether the signed enclave binary can be
reproduced from the given source code, and whether the IAS report
corresponds to the given signed enclave.
Parameters
----------
source_code: str
Local file path to the source code where the enclave to be built
is located.
signed_enclave: str
Path to the signed enclave file.
ias_report: str, optional
Local file path to the IAS report, in json format.
signing_key: str, optional
Local file path to a signing key with which to sign the enclave.
When signing an enclave just for test purposes, such as
verifying the reproducibility of an enclave, one can fall back
on the default which is a key file that is packaged with
``auditee``.
Raises
------
:py:exc:`~.errors.SGXSignError`:
If something wrong happen when invoking the ``sgx_sign`` tool.
Returns
-------
bool:
``True`` if the MRENCLAVEs match, ``False`` otherwise.
Examples
--------
>>> from auditee.enclave import verify_mrenclave
>>> verify_mrenclave('sgx-iot/', 'enclave.signed.so', ias_report='ias_report.json')
# ...
Reproducibility Report
----------------------
- Signed enclave MRENCLAVE: b7af1907e21b4eb240d3c3c6880e3892e45af383196d7aa326c35e2a8c71ef63
- Built-from-source enclave MRENCLAVE: b7af1907e21b4eb240d3c3c6880e3892e45af383196d7aa326c35e2a8c71ef63
- IAS report MRENCLAVE: b7af1907e21b4eb240d3c3c6880e3892e45af383196d7aa326c35e2a8c71ef63
# ...
MRENCLAVES match!
# ...
Report data
-----------
The following REPORT DATA contained in the remote attestation verification report CAN be trusted.
6e979bd31dd119faf99a423e97563e67dc7937944347c8a98f59977b76dd55cd911a8be4420bec78116e4e51f47def30c72c631556e960378e39e3aab7ccbe08
>>> True
"""
unsigned_enclave = build(source_code, docker_build_progress=docker_build_progress)
source_code = pathlib.Path(source_code).resolve()
auditee_file = source_code.joinpath(".auditee.yml")
with open(auditee_file) as f:
auditee_config = yaml.safe_load(f)
enclave_build_config = auditee_config["enclaves"][0]
# Get absolute path of enclave config
enclave_config = source_code.joinpath(enclave_build_config["enclave_config"])
rebuilt_signed_enclave = sign(
unsigned_enclave, enclave_config, signing_key=signing_key
)
if ias_report is not None:
ias_report = pathlib.Path(ias_report).resolve()
auditor_sigstruct = Sigstruct.from_enclave_file(rebuilt_signed_enclave)
dev_sigstruct = Sigstruct.from_enclave_file(signed_enclave)
if not ias_report:
print_report(dev_sigstruct.mrenclave.hex(), auditor_sigstruct.mrenclave.hex())
if auditor_sigstruct.mrenclave == dev_sigstruct.mrenclave:
print(f"{term.green}MRENCLAVE match!{term.normal}")
else:
print(f"{term.red}MRENCLAVE do not match!{term.normal}")
return auditor_sigstruct.mrenclave == dev_sigstruct.mrenclave
with open(ias_report) as f:
ias_report_data = json.load(f)
report_body = ias_report_data["body"]
# TODO verify certificate & signature -- see issue #5
# https://github.com/initc3/auditee/issues/5
# report_headers = ias_report_data["headers"]
isv_enclave_quote_body = report_body["isvEnclaveQuoteBody"]
quote_body = read_sgx_quote_body_b64(isv_enclave_quote_body)
report_mrenclave = bytes(quote_body.report_body.mr_enclave.m)
print_report(
dev_sigstruct.mrenclave.hex(),
auditor_sigstruct.mrenclave.hex(),
ias_report_mrenclave=report_mrenclave.hex(),
)
mrenclave_match = (
auditor_sigstruct.mrenclave == dev_sigstruct.mrenclave == report_mrenclave
)
if mrenclave_match:
print(f"{term.green}MRENCLAVES match!{term.normal}")
else:
print(f"{term.red}MRENCLAVES do not match!{term.normal}")
print(f"\n{term.bold}Report data\n-----------{term.normal}")
can_or_cannot = (
f"{term.bold_green}CAN{term.normal}"
if mrenclave_match
else f"{term.bold_red}CANNOT{term.normal}"
)
print(
f"The following {term.bold}REPORT DATA{term.normal} contained in "
f"the remote attestation verification report {can_or_cannot} be trusted."
)
report_data = bytes(quote_body.report_body.report_data.d).hex()
print(f"{report_data}")
return mrenclave_match
def _verify_mrenclave(
signed_enclave,
enclave_config,
*,
ias_report=None,
unsigned_enclave_filename="Enclave.so",
docker_build_attrs,
):
"""Verifies if the MRENCLAVE of the provided signed enclave matches
with the one obtained when rebuilding the enclave from source, and
the one in the IAS report.
Example
-------
docker_build_attrs = {'target': 'export-stage'}
"""
context_path = docker_build_attrs.pop("context_path", ".")
output = docker_build_attrs.pop("output", {"type": "local", "dest": "out"})
# target="export-stage", output={"type": "local", "dest": "out"}
docker.build(context_path, output=output, **docker_build_attrs)
unsigned_enclave = pathlib.Path(output["dest"]).joinpath(unsigned_enclave_filename)
signing_key = pathlib.Path(__file__).parent.resolve().joinpath("signing_key.pem")
dev_sigstruct = Sigstruct.from_enclave_file(signed_enclave)
out = "/tmp/audit_enclave.signed.so"
sgx_sign(unsigned_enclave, key=signing_key, out=out, config=enclave_config)
auditor_sigstruct = Sigstruct.from_enclave_file(out)
if not ias_report:
print(
f"\nsigned enclave MRENCLAVE: \t\t{term.bold}{dev_sigstruct.mrenclave.hex()}{term.normal}"
)
print(
f"built-from-source enclave MRENCLAVE: \t\t{term.bold}{auditor_sigstruct.mrenclave.hex()}{term.normal}\n"
)
if auditor_sigstruct.mrenclave == dev_sigstruct.mrenclave:
print(f"{term.green}MRENCLAVE match!{term.normal}")
else:
print(f"{term.red}MRENCLAVE do not match!{term.normal}")
return auditor_sigstruct.mrenclave == dev_sigstruct.mrenclave
raise NotImplementedError
def verify(
signed_enclave,
unsigned_enclave,
enclave_config,
signing_key=None,
show_mrsigner=False,
verbose=True,
):
"""Sign the enclave_so, and compare with the signed_enclave.
:param str signed_enclave: The signed enclave to check against.
:param str unsigned_enclave: The enclave to sign and verify
against the signed enclave.
:param str enclave_config: The enclave configuration used to sign
the enclave.
:param str signing_key: The private key used to sign the enclave.
:param bool show_mrsigner: Whether to show the mrsigner field or not.
Defaults to ``False``.
:param bool verbose: Whether to use verbose mode or not.
Defaults to ``True``.
"""
if not signing_key:
signing_key = (
pathlib.Path(__file__).parent.resolve().joinpath("signing_key.pem")
)
dev_sigstruct = Sigstruct.from_enclave_file(signed_enclave)
out = "/tmp/audit_enclave.signed.so"
sgx_sign(unsigned_enclave, key=signing_key, out=out, config=enclave_config)
auditor_sigstruct = Sigstruct.from_enclave_file(out)
report_data = {
attr: ReportItem(
matches=val,
expected=getattr(dev_sigstruct, attr),
computed=getattr(auditor_sigstruct, attr),
)
for attr, val in auditor_sigstruct.cmp(dev_sigstruct).items()
}
report = ReproducibilityReport(**report_data)
if verbose:
_print_report(report, show_mrsigner=show_mrsigner)
return report
def _print_report(report, show_mrsigner=False):
print(f"\n{term.bold}Reproducibility Report\n----------------------")
for attr, val in report._asdict().items():
if attr == "mrsigner" and not show_mrsigner:
continue
print(f"{term.bold}{attr}:{term.normal}")
for k, v in val._asdict().items():
if attr in ("mrenclave", "mrsigner") and k in ("expected", "computed"):
v = v.hex()
print(f" {k}: ", end="")
if k == "matches":
if v:
matches = True
print(f"{term.green}{v}{term.normal}")
else:
matches = False
print(f"{term.red}{v}{term.normal}")
else:
if not matches and k == "computed":
print(f"{term.red}{v}{term.normal}")
else:
print(f"{v}")
class Enclave:
def __init__(
self, *, src=None, unsigned_enclave_path=None, signed_enclave_path=None
):
self.src = src
self.unsigned_enclave_path = unsigned_enclave_path
self.signed_enclave_path = signed_enclave_path
if self.src:
self._parse_auditee_config(self.src)
if unsigned_enclave_path:
self.unsigned_bytes = self._unsigned_bytes()
if signed_enclave_path:
self.signed_bytes = self._signed_bytes()
def _unsigned_bytes(self):
with open(self.unsigned_enclave_path, "rb") as f:
unsigned_bytes = f.read()
return unsigned_bytes
def _signed_bytes(self):
with open(self.signed_enclave_path, "rb") as f:
signed_bytes = f.read()
return signed_bytes
def _parse_auditee_config(self, src):
src = pathlib.Path(src).resolve()
# TODO Raise error if auditee file is missing
# TODO should probably support auditee.yaml also
self.auditee_file = src.joinpath(".auditee.yml")
with open(self.auditee_file) as f:
self.auditee_config = yaml.safe_load(f)
# TODO For now, just support one enclave config, not a list
# i.e. remove [0]
# i.e.: enclave_build_config = auditee_config["enclaves"]
enclave_build_config = self.auditee_config["enclaves"][0]
self.config = src.joinpath(enclave_build_config["enclave_config"])
@classmethod
def from_src(cls, src):
""" """
if urlparse(src).scheme == "https":
tempdir = tempfile.mkdtemp(prefix="enclave-", suffix="-src", dir="/tmp")
try:
url, rev = src.split("@")
except ValueError:
url, rev = src, None
repo = git.Repo.clone_from(url, to_path=tempdir)
if rev:
repo.git.checkout(rev)
_src = tempdir
elif urlparse(src).scheme == "http":
raise AuditeeError("HTTP scheme is not supported. Use HTTPS.")
elif pathlib.Path(src).is_dir():
# shutil.copytree(src, cls.tmp_src_path)
_src = src
else:
raise AuditeeError(f"Cannot build from given source: {src}")
unsigned_enclave_path = build(_src)
# unsigned_enclave_path = build(cls.tmp_src_path)
# cls(src=cls.tmp_src_path, unsigned_enclave_path=unsigned_enclave_path)
return cls(src=_src, unsigned_enclave_path=unsigned_enclave_path)
@classmethod
def build_from_nixfile(cls, nixfile, *, unsigned_enclave_path):
popenargs = ["nix-build", nixfile]
returncode = subprocess.run(popenargs).returncode
if returncode != 0:
raise AuditeeError("Error building enclave with nix-build")
return cls(unsigned_enclave_path=unsigned_enclave_path)
def sign(self, *, config=None, key=None, to_path=None):
if to_path:
self.signed_enclave_path = to_path
elif not self.signed_enclave_path:
self.signed_enclave_path = "/tmp/enclave.signed.so"
if not self.unsigned_bytes:
raise AuditeeError("Must set Enclave instance `unsigned_bytes` first!")
if not config:
config = self.config
if not config:
raise AuditeeError(
"An enclave config is required to sign the enclave binary!"
)
sign(
self.unsigned_enclave_path,
config,
signed_enclave=self.signed_enclave_path,
signing_key=key,
)
return self.signed_enclave_path
def sigstruct(self):
if not self.signed_enclave_path:
raise AuditeeError("A signed enclave is required to get the sigstruct!")
return Sigstruct.from_enclave_file(self.signed_enclave_path)
def mrenclave(self):
return self.sigstruct().mrenclave
def mrsginer(self):
return self.sigstruct().mrsigner