diff --git a/examples/yaos_millionaires/README.md b/examples/yaos_millionaires/README.md index 6ba0277..149a749 100644 --- a/examples/yaos_millionaires/README.md +++ b/examples/yaos_millionaires/README.md @@ -1,26 +1,121 @@ # Yao's Millionaires application example -Yao's Millionaires problem solved using Cosmian Enclave. -Each participant reprensented by its public key can send the value of its wealth to known who is the richest. -The result is encrypted for each public key. +[Yao's Millionaires' problem](https://en.wikipedia.org/wiki/Yao%27s_Millionaires%27_problem) solved using Cosmian Enclave. +Each participant reprensented by its public key can send the value of its wealth to know who is the richest. +The result is encrypted using participant's public key. -## Test your app before creating the enclave +## Prequisites + +### Setup + +First, each participant generates a keypair to identify with their own public/private key: + +```console +$ cenclave keygen --asymmetric --output /tmp/keypair1.bin +Public key: e046f56688633e0d27ffd13127275a4278f11a6388abad043e50bbdefed5a304 +Public key (Base64): 4Eb1ZohjPg0n/9ExJydaQnjxGmOIq60EPlC73v7VowQ= +Keypair wrote to /tmp/keypair1.bin +$ cenclave keygen --asymmetric --output /tmp/keypair2.bin +Public key: 8b5a7b38699f8414dfac6ccb7a3ea0faadbe66f74980f8cc5c6b52b8dd2aee12 +Public key (Base64): i1p7OGmfhBTfrGzLej6g+q2+ZvdJgPjMXGtSuN0q7hI= +Keypair wrote to /tmp/keypair2.bin +``` + +then populate `secrets.json` with participant's public key base64-encoded: + +```console +{ + "participants": ["4Eb1ZohjPg0n/9ExJydaQnjxGmOIq60EPlC73v7VowQ=", "i1p7OGmfhBTfrGzLej6g+q2+ZvdJgPjMXGtSuN0q7hI="] +} +``` + +### Test the code locally without SGX (docker required) ```console $ cenclave localtest --code src/ \ --dockerfile Dockerfile \ --config config.toml \ - --test tests/ + --test tests/ \ + --secrets secrets.json ``` -## Create Cosmian Enclave package with the code and the container image +### Create a package for Cosmian Enclave ```console $ cenclave package --code src/ \ --dockerfile Dockerfile \ --config config.toml \ --test tests/ \ - --output code_provider + --output tarball/ ``` -The generated package can now be sent to the SGX operator. +The generated tarball file in `tarball/` folder can now be used on the SGX machine properly configured with Cosmian Enclave. + +Optionally use `--encrypt` if you want the code to be encrypted. + +## Running the code with Cosmian Enclave + +### Spawn the configuration server + +```console +$ cenclave spawn --host --port --size --package --output sgx_operator/ --san yaos_millionaires +``` + +- `host`: usually 127.0.0.1 for localhost or 0.0.0.0 to expose externally +- `port`: network port used by your application, usually 9999 +- `size`: memory size (in MB) of the enclave (must be a power of 2 greater than 2048) +- `package`: tarball file with the code and container image +- `san`: [Subject Alternative Name](https://en.wikipedia.org/wiki/Public_key_certificate#Subject_Alternative_Name_certificate) used for routing with SSL pass-through (either domain name, external IP address or localhost) + +### Seal code secret key (optionally) + +If you choose to encrypt the code with `--encrypt` then you need to verify your enclave first. +Ask the SGX operator to communicate the `evidence.json` file to do the remote attestation and verify that your code is running in the enclave: + +```console +$ cenclave verify --evidence evidence.json --package tarball/package_<><>.tar --output ratls.pem +``` + +if successful, then include the randomly generated secret key in `secrets_to_seal.json`: + +```text +{ + "code_secret_key": "HEX_CODE_SECRET_KEY" +} +``` + +and finally seal `secrets_to_seal.json` file: + +```console +$ cenclave seal --input secrets_to_seal.json --receiver-enclave ratls.pem --output code_provider/sealed_secrets.json.enc +``` + +### Run your application + +After sending `secrets.json` and `sealed_secrets.json.enc` (if the code is encrypted) to the SGX operator, it's now ready to run: + +```console +$ # add `--sealed-secrets sealed_secrets.json.enc` if needed +$ cenclave run --secrets secrets.json yaos_millionaires +``` + +### Use the client + +In the `client/` directory you can use the Python client to query your enclave: + +```console +$ # Verify the remote enclave and the MRENCLAVE hash digest +$ python main.py --verify https://: +$ +$ # list participants +$ python main.py --keypair /tmp/keypair1.bin --list https://: +$ +$ # push your fortune +$ python main.py --keypair /tmp/keypair1.bin --push 1_000_000 https://: +$ python main.py --keypair /tmp/keypair1.bin --push 2_000_000 https://: +$ +$ # ask who is the richest with keypair1 (result encrypted for keypair1) +$ python main.py --keypair /tmp/keypair1.bin --result https://: +$ # ask who is the richest with keypair2 (result encrypted for keypair2) +$ python main.py --keypair /tmp/keypair2.bin --result https://: +``` diff --git a/examples/yaos_millionaires/client/main.py b/examples/yaos_millionaires/client/main.py index 0a26d87..7fc7f68 100644 --- a/examples/yaos_millionaires/client/main.py +++ b/examples/yaos_millionaires/client/main.py @@ -1,43 +1,41 @@ -"""Simple Python client for Yao's Millionaires. +"""Simple Python client for Yao's Millionaires with Cosmian Enclave. ```console -$ pip install requests intel-sgx-ra -$ python main.py http://127.0.0.1:5000 # for local testing +$ pip install requests intel-sgx-ra cenclave-lib-crypto +$ python main.py --help +usage: Client for Yao's Millionaires in Cosmian Enclave + +positional arguments: + url URL of the remote enclave + +options: + -h, --help show this help message and exit + --reset Remove participant's data from the computation + --verify Verify the enclave by doing the remote attestation + --list List participant's public key + --push NUMBER Push your wealth as number for the computation + --result Get result of the computation + --keypair PATH Path of the public/private keypair + --debug Debug information to stdout ``` """ +import argparse import base64 -import sys +import logging +import struct import tempfile from pathlib import Path -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Optional, Union import requests -from cenclave_lib_crypto.seal_box import unseal +from cenclave_lib_crypto.seal_box import seal, unseal from intel_sgx_ra.maa.attest import verify_quote from intel_sgx_ra.quote import Quote from intel_sgx_ra.ratls import get_server_certificate, ratls_verify_from_url, url_parse -# Key generated with cenclave_lib_crypto.x2559.x25519_keygen() -PK1: bytes = bytes.fromhex( - "938b3e0e60ebbdbbd4348ba6a0468685043d540e332c32e8bc2ca40b858ad209" -) -PK1_B64: str = "k4s+DmDrvbvUNIumoEaGhQQ9VA4zLDLovCykC4WK0gk=" -SK1: bytes = bytes.fromhex( - "7957ceed56d44a384cf523619a00b2c129514daf422c0b799105fb2caa23ef97" -) - -PK2: bytes = bytes.fromhex( - "ff8b983287fec6aefcf1b55e8c1efeff984e5b8dfa8d4de62df521bd6ec57d14" -) -PK2_B64: str = "/4uYMof+xq788bVejB7+/5hOW436jU3mLfUhvW7FfRQ=" -SK2: bytes = bytes.fromhex( - "05e5aa1c56ec3d6bf707893e6a038a825d80a2802fdb565fd8fecb840735a954" -) - - def reset(session: requests.Session, url: str) -> None: response: requests.Response = session.delete(url) @@ -45,10 +43,30 @@ def reset(session: requests.Session, url: str) -> None: raise Exception(f"Bad response: {response.status_code}") -def push(session: requests.Session, url: str, pk: bytes, n: Union[int, float]) -> None: +def push( + session: requests.Session, + url: str, + pk: bytes, + n: Union[int, float], + enclave_pk: Optional[bytes] = None, +) -> None: + encoded_n: bytes = struct.pack(" str: encrypted_content: bytes = base64.b64decode(content["max"]) - print(f"Encrypted content for {pk_b64}: {encrypted_content.hex()}") + logging.debug("Encrypted content for %s: %s", pk_b64, encrypted_content.hex()) pk_winner: bytes = unseal(encrypted_content, sk) return base64.b64encode(pk_winner).decode("utf-8") -def main() -> int: - url: str = sys.argv[1] +def verify(url: str) -> tuple[Path, bytes, bytes]: hostname, port = url_parse(url) - session: requests.Session = requests.Session() - quote: Quote = ratls_verify_from_url(url) ratls_cert_path: Path = Path(tempfile.gettempdir()) / "ratls.pem" ratls_cert = get_server_certificate((hostname, port)) ratls_cert_path.write_bytes(ratls_cert.encode("utf-8")) - _: Dict[str, Any] = verify_quote(quote) + maa_result: Dict[str, Any] = verify_quote(quote) - session.verify = f"{ratls_cert_path}" + logging.debug("Microsoft Azure Attestation response: %s", maa_result) - p_1 = (PK1, 100_398) - p_2 = (PK2, 100_399) + mr_enclave: bytes = bytes.fromhex(maa_result["x-ms-sgx-mrenclave"]) + enclave_pk: bytes = quote.report_body.report_data[32:] + + return ratls_cert_path, mr_enclave, enclave_pk + + +def cli_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + usage="Client for Yao's Millionaires in Cosmian Enclave" + ) + + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--reset", + action="store_true", + help="Remove participant's data from the computation", + ) + group.add_argument( + "--verify", + action="store_true", + help="Verify the enclave by doing the remote attestation", + ) + group.add_argument( + "--list", action="store_true", help="List participant's public key" + ) + group.add_argument( + "--push", + type=float, + metavar="NUMBER", + help="Push your wealth as number for the computation", + ) + group.add_argument( + "--result", action="store_true", help="Get result of the computation" + ) + parser.add_argument( + "--keypair", + type=Path, + metavar="PATH", + help="Path of the public/private keypair", + ) + parser.add_argument( + "--debug", action="store_true", help="Debug information to stdout" + ) + parser.add_argument("url", help="URL of the remote enclave") + + return parser.parse_args() + + +def main() -> int: + args: argparse.Namespace = cli_args() + + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + + url: str = args.url + + session: requests.Session = requests.Session() + + ratls_cert_path, mr_enclave, enclave_pk = verify(url) + + session.verify = f"{ratls_cert_path}" - reset(session, url) + if args.reset: + reset(session, url) + logging.info("Reset success") - for pk, n in (p_1, p_2): - push(session, url, pk, n) + if args.verify: + logging.info( + "Verification successful, MRENCLAVE: %s", + mr_enclave.hex(), + ) + if args.list: + logging.info(participants(session, url)) - print(participants(session, url)) + keypair: bytes = Path(args.keypair).read_bytes() - p_1_result = richest(session, url, PK1, SK1) - p_2_result = richest(session, url, PK2, SK2) + pk, sk = keypair[:32], keypair[32:] - assert p_1_result == p_2_result + if args.push: + push(session, url, pk, float(args.push), enclave_pk) + logging.info( + "Pushed %s with public key %s", + float(args.push), + base64.b64encode(pk).decode("utf-8"), + ) - print(f"The richest participant is {p_1_result}") + if args.result: + logging.info("The richest participant is %s", richest(session, url, pk, sk)) return 0 diff --git a/examples/yaos_millionaires/secrets_to_seal.json b/examples/yaos_millionaires/secrets_to_seal.json index 9e26dfe..742a9d3 100644 --- a/examples/yaos_millionaires/secrets_to_seal.json +++ b/examples/yaos_millionaires/secrets_to_seal.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "code_secret_key": "HEX_CODE_SECRET_KEY" +} \ No newline at end of file diff --git a/examples/yaos_millionaires/src/app.py b/examples/yaos_millionaires/src/app.py index 3091225..36c4a61 100644 --- a/examples/yaos_millionaires/src/app.py +++ b/examples/yaos_millionaires/src/app.py @@ -2,101 +2,114 @@ import base64 import json -import logging import os +import struct from http import HTTPStatus from pathlib import Path from typing import Any, Optional +from cenclave_lib_crypto.seal_box import seal, unseal +from flask import Flask, Response, jsonify, request + import globs -from cenclave_lib_crypto.seal_box import seal -from flask import Flask, Response, request -from flask.logging import create_logger app = Flask(__name__) -LOG = create_logger(app) - -logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.DEBUG) - -SECRETS = json.loads(Path(os.getenv("SECRETS_PATH")).read_text(encoding="utf-8")) +SECRETS = json.loads(Path(os.environ["SECRETS_PATH"]).read_text(encoding="utf-8")) +ENCLAVE_SK: Optional[bytes] = ( + Path(os.environ["ENCLAVE_SK_PATH"]).read_bytes() + if "ENCLAVE_SK_PATH" in os.environ + else None +) @app.get("/health") -def health_check(): +def health_check() -> Response: """Health check of the application.""" return Response(response="OK", status=HTTPStatus.OK) @app.post("/") -def push(): +def push() -> Response: """Add a number to the pool.""" - data: Optional[Any] = request.get_json(silent=True) + content: Optional[Any] = request.get_json(silent=True) - if data is None or not isinstance(data, dict): - LOG.error("TypeError with data: '%s'", data) + if content is None or not isinstance(content, dict): + app.logger.error("TypeError with data: '%s'", content) return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) - n: Optional[float] = data.get("n") - pk: Optional[str] = data.get("pk") + data: Optional[Any] = content.get("data") + pk: Optional[str] = content.get("pk") - if n is None or not isinstance(n, (float, int)): - LOG.error("TypeError with data content: '%s' (%s)", n, type(n)) + if data is None or not isinstance(data, dict): + app.logger.error("TypeError with data content: '%s' (%s)", data, type(data)) return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) if pk is None or not isinstance(pk, str): - LOG.error("TypeError with data content: '%s' (%s)", pk, type(pk)) + app.logger.error("TypeError with data content: '%s' (%s)", pk, type(pk)) return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) if pk not in SECRETS["participants"]: - LOG.error( + app.logger.error( "The public key provided is not in the participants: '%s' (%s)", pk, SECRETS["participants"], ) return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) - globs.POOL.append((pk, n)) + if pk in dict(globs.POOL): + app.logger.error("Public key already pushed data") + return Response(status=HTTPStatus.CONFLICT) + + n: bytes = base64.b64decode(data["n"]) + + if data["encrypted"] and ENCLAVE_SK: + n = unseal(n, ENCLAVE_SK) - LOG.info("Successfully added (%s, %s)", n, pk) + deser_n, *_ = struct.unpack(" Response: """Get all the public keys of participants""" if "participants" not in SECRETS: - LOG.error("no participants found") - return {"participants": None} + app.logger.error("no participants found") + return jsonify({"participants": None}) - return {"participants": SECRETS["participants"]} + return jsonify({"participants": SECRETS["participants"]}) @app.post("/richest") def richest(): """Get the current max in pool.""" if len(globs.POOL) < 1: - LOG.error("need more than 1 value to compute the max") + app.logger.error("need more than 1 value to compute the max") return {"max": None} if "participants" not in SECRETS: - LOG.error("no participants found") + app.logger.error("no participants found") return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) data: Optional[Any] = request.get_json(silent=True) if data is None or not isinstance(data, dict): - LOG.error("TypeError with data: '%s'", data) + app.logger.error("TypeError with data: '%s'", data) return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) recipient_pk: Optional[str] = data.get("recipient_pk") if recipient_pk is None or not isinstance(recipient_pk, str): - LOG.error("TypeError with data content: '%s' (%s)", pk, type(pk)) + app.logger.error( + "TypeError with data content: '%s' (%s)", recipient_pk, type(recipient_pk) + ) return Response(status=HTTPStatus.UNPROCESSABLE_ENTITY) if recipient_pk not in SECRETS["participants"]: - LOG.error( + app.logger.error( "The public key provided is not in the participants: '%s' (%s)", recipient_pk, SECRETS["participants"], @@ -107,11 +120,11 @@ def richest(): (pk, _) = max(globs.POOL, key=lambda t: t[1]) - encrypted_b64_result: bytes = base64.b64encode( + encrypted_b64_result: str = base64.b64encode( seal(base64.b64decode(pk), raw_recipient_pk) ).decode("utf-8") - return {"max": encrypted_b64_result} + return jsonify({"max": encrypted_b64_result}) @app.delete("/") @@ -119,6 +132,6 @@ def reset(): """Reset the current pool.""" globs.POOL = [] - LOG.info("Reset successfully") + app.logger.info("Reset successfully") return Response(status=HTTPStatus.OK) diff --git a/examples/yaos_millionaires/src/globs.py b/examples/yaos_millionaires/src/globs.py index a49502f..348b080 100644 --- a/examples/yaos_millionaires/src/globs.py +++ b/examples/yaos_millionaires/src/globs.py @@ -1,3 +1,3 @@ """globs module.""" -POOL: list = [] +POOL: list[tuple[str, float]] = [] diff --git a/examples/yaos_millionaires/tests/test_app.py b/examples/yaos_millionaires/tests/test_app.py index 7ae2f47..df8cd92 100644 --- a/examples/yaos_millionaires/tests/test_app.py +++ b/examples/yaos_millionaires/tests/test_app.py @@ -1,6 +1,7 @@ """Unit test for our app.""" import base64 +import struct import requests from cenclave_lib_crypto.seal_box import unseal @@ -35,18 +36,20 @@ def test_participants(url, certificate, pk1, pk1_b64, pk2, pk2_b64): def test_richest(url, certificate, pk1_b64, sk1, pk2_b64, sk2): + n_b64 = base64.b64encode(struct.pack("