Skip to content

Commit

Permalink
Add: improvements on Yao's millionaires example
Browse files Browse the repository at this point in the history
  • Loading branch information
grydz committed Dec 11, 2024
1 parent 817afea commit 98b26be
Show file tree
Hide file tree
Showing 12 changed files with 363 additions and 134 deletions.
2 changes: 1 addition & 1 deletion cli/cenclave/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ classifiers = [
dependencies = [
"cryptography>=43.0.3,<44.0.0",
"docker>=7.1.0,<8.0.0",
"intel-sgx-ra>=2.3.1,<3.0.0",
"intel-sgx-ra>=2.3.2,<3.0.0",
"jinja2>=3.1.4,<3.2.0",
"cenclave-lib-crypto>=1.0.0,<2.0.0",
"pydantic>=1.10.18,<2.0.0",
Expand Down
107 changes: 100 additions & 7 deletions examples/yaos_millionaires/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
# 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: c12ace0ca954b883fa918561dd647cf3de79861ea5996b6d2e4e4e8abc664a26
Public key (Base64): wSrODKlUuIP6kYVh3WR88955hh6lmWttLk5OirxmSiY=
Keypair wrote to /tmp/keypair1.bin
$ cenclave keygen --asymmetric --output /tmp/keypair2.bin
Public key: 4afc6cfc76abc1715bf7f89f451d3bdfcb76883db7d420ecf6295b9975c35a20
Public key (Base64): Svxs/HarwXFb9/ifRR0738t2iD231CDs9ilbmXXDWiA=
Keypair wrote to /tmp/keypair2.bin
```

then populate `src/config.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/ \
Expand All @@ -13,14 +38,82 @@ $ cenclave localtest --code src/ \
--test tests/
```

## 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 <HOST> --port <PORT> --size <ENCLAVE_SIZE> --package <TARBALL_FILE> --output sgx_operator/ --san <EXTERNAL_IP | DOMAIN_NAME | localhost> 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

```console
$ cenclave run yaos_millionaires
```

Note: if the code is encrypted, sealed secrets must be added to the command with `--sealed-secrets sealed_secrets.json.enc`.

### 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://<HOST>:<PORT>
$
$ # list participants
$ python main.py --keypair /tmp/keypair1.bin --list https://<HOST>:<PORT>
$
$ # push your fortune
$ python main.py --keypair /tmp/keypair1.bin --push 1_000_000 https://<HOST>:<PORT>
$ python main.py --keypair /tmp/keypair1.bin --push 2_000_000 https://<HOST>:<PORT>
$
$ # ask who is the richest with keypair1 (result encrypted for keypair1)
$ python main.py --keypair /tmp/keypair1.bin --result https://<HOST>:<PORT>
$ # ask who is the richest with keypair2 (result encrypted for keypair2)
$ python main.py --keypair /tmp/keypair2.bin --result https://<HOST>:<PORT>
```
208 changes: 163 additions & 45 deletions examples/yaos_millionaires/client/main.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,75 @@
"""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 os
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 intel_sgx_ra.maa.attest import verify_quote
from cenclave_lib_crypto.seal_box import seal, unseal
from intel_sgx_ra.attest import verify_quote
from intel_sgx_ra.log import LOGGER as INTEL_SGX_RA_LOGGER
from intel_sgx_ra.maa.attest import verify_quote as azure_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)

if response.status_code != 200:
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("<d", float(n))

if enclave_pk is not None:
encrypted_n: bytes = seal(encoded_n, enclave_pk)
data = {
"encrypted": True,
"n": base64.b64encode(encrypted_n).decode("utf-8"),
}
else:
data = {"encrypted": False, "n": base64.b64encode(encoded_n).decode("utf-8")}

response: requests.Response = session.post(
url=url,
json={"pk": base64.b64encode(pk).decode("utf-8"), "n": float(n)},
json={
"pk": base64.b64encode(pk).decode("utf-8"),
"data": data,
},
)

if response.status_code != 200:
Expand Down Expand Up @@ -78,45 +99,142 @@ def richest(session: requests.Session, url: str, pk: bytes, sk: bytes) -> str:
content: Dict[str, Any] = response.json()

encrypted_content: bytes = base64.b64decode(content["max"])
logging.info("Encrypted response: %s", encrypted_content.hex())

print(f"Encrypted content for {pk_b64}: {encrypted_content.hex()}")

logging.info("Trying to decrypt response for pk %s...", pk_b64)
pk_winner: bytes = unseal(encrypted_content, sk)
logging.info("Response successfully decrypted")

return base64.b64encode(pk_winner).decode("utf-8")


def main() -> int:
url: str = sys.argv[1]
def verify(url: str, pccs_url: Optional[str] = None) -> tuple[Path, bytes, bytes]:
hostname, port = url_parse(url)

session: requests.Session = requests.Session()

quote: Quote = ratls_verify_from_url(url)

if pccs_url:
_ = verify_quote(quote, None, pccs_url)
else: # try Azure Cloud
maa_result: Dict[str, Any] = azure_verify_quote(quote)
logging.debug("Microsoft Azure Attestation response: %s", maa_result)

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"))
logging.info("RA-TLS certificate written to %s", ratls_cert_path)

_: Dict[str, Any] = verify_quote(quote)
mr_enclave: bytes = quote.report_body.mr_enclave
enclave_pk: bytes = quote.report_body.report_data[32:]

session.verify = f"{ratls_cert_path}"
logging.info("MRENCLAVE: %s", mr_enclave.hex())
logging.info("Enclave's public key: %s", enclave_pk.hex())

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_cmd = parser.add_mutually_exclusive_group(required=True)
group_cmd.add_argument(
"--reset",
action="store_true",
help="Remove participant's data from the computation",
)
group_cmd.add_argument(
"--verify",
action="store_true",
help="Verify the enclave by doing the remote attestation",
)
group_cmd.add_argument(
"--list", action="store_true", help="List participant's public key"
)
group_cmd.add_argument(
"--push",
type=float,
metavar="NUMBER",
help="Push your wealth as number for the computation",
)
group_cmd.add_argument(
"--result", action="store_true", help="Get result of the computation"
)
parser.add_argument(
"--keypair",
type=Path,
metavar="PATH",
required=True,
help="Path to read the public/private keypair",
)
parser.add_argument(
"--mrenclave",
type=str,
metavar="HEXDIGEST",
help="Expected MRENCLAVE hash digest of the remote enclave",
)
parser.add_argument(
"--debug", action="store_true", help="Debug information to stdout"
)
parser.add_argument("url", help="URL of the remote enclave")

p_1 = (PK1, 100_398)
p_2 = (PK2, 100_399)
return parser.parse_args()

reset(session, url)

for pk, n in (p_1, p_2):
push(session, url, pk, n)
def main() -> int:
args: argparse.Namespace = cli_args()

logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
INTEL_SGX_RA_LOGGER.setLevel(logging.ERROR)

url: str = args.url

pk: bytes
sk: bytes

print(participants(session, url))
keypair: bytes = Path(args.keypair).read_bytes()
pk, sk = keypair[:32], keypair[32:]

p_1_result = richest(session, url, PK1, SK1)
p_2_result = richest(session, url, PK2, SK2)
session: requests.Session = requests.Session()

pccs_url: Optional[str] = os.environ.get("PCCS_URL")
ratls_cert_path, mr_enclave, enclave_pk = verify(url, pccs_url)

assert p_1_result == p_2_result
if expected_mrenclave := args.mrenclave:
assert (
bytes.fromhex(expected_mrenclave) == mr_enclave
), f"Unexpected MRENCLAVE {mr_enclave.hex()}"

session.verify = f"{ratls_cert_path}"

print(f"The richest participant is {p_1_result}")
if args.reset:
reset(session, url)
logging.info("Reset success")
elif args.verify:
logging.info(
"RA-TLS certificate:\n%s",
ratls_cert_path.read_text(),
)
elif args.list:
logging.info(participants(session, url))
elif number := args.push:
push(session, url, pk, float(number), enclave_pk)
logging.info(
"Pushed %s with public key %s",
float(number),
base64.b64encode(pk).decode("utf-8"),
)
elif args.result:
pk_winner: str = richest(session, url, pk, sk)
logging.info("The richest participant is %s", pk_winner)

if pk_winner == base64.b64encode(pk).decode("utf-8"):
logging.info("Congrats, you are the richest participant!")
else:
logging.info("Sorry, keep growing your fortune!")
else:
raise Exception("Unexpected command")

return 0

Expand Down
Loading

0 comments on commit 98b26be

Please sign in to comment.