diff --git a/examples/sgx_container/Dockerfile b/examples/sgx_container/Dockerfile new file mode 100644 index 00000000..61dcc933 --- /dev/null +++ b/examples/sgx_container/Dockerfile @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +FROM registry.scontain.com/sconecuratedimages/crosscompilers:ubuntu24.04-scone6.0.5 AS builder + +WORKDIR /myapp + +RUN apt-get update && apt-get install -y \ + git pkg-config libtss2-dev protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* + +ENV PKG_CONFIG_ALLOW_CROSS=1 + +RUN git clone https://github.com/edgeless-project/edgeless.git \ + && cd edgeless && git checkout 7412d3b^ && cd .. + +COPY edgeless_container_function/src/container_function.rs \ + /myapp/edgeless/edgeless_container_function/src/container_function.rs + +WORKDIR /myapp/edgeless/edgeless_container_function + +# Building the NON-debug binary +RUN scone-cargo build -p edgeless_container_function --release --verbose + +# Check if the binary has SCONE-specific symbols. If missing, the binary will run as a "native" app and ignore SCONE_CONFIG_ID. +RUN nm /myapp/edgeless/target/x86_64-scone-linux-musl/release/edgeless_container_function_d | grep -q scone_ || \ + (echo "ERROR: Binary is not SCONE-compiled!" && exit 1) + + +# ---- SIGNER AND RUNTIME STAGE ---- +FROM registry.scontain.com/sconecuratedimages/crosscompilers:runtime-scone6.0.5 AS runner + +WORKDIR /app + +# Copy the debug enclave binary produced by the crosscompiler +COPY --from=builder \ + /myapp/edgeless/target/x86_64-scone-linux-musl/release/edgeless_container_function_d \ + /usr/local/bin/edgeless_container_function + +# Create a production-signed (non-debug) enclave binary. +# CAUTION: If you ship a debug enclave ("*_d"), CAS will treat it as untrustworthy and refuse to release secrets/config. +# BuildKit secret "enclave_key" is mounted at /run/secrets/enclave_key FOR THIS STEP ONLY and is NOT stored in any image layer. +# Use && to ensure the build ABORTS if any step fails. +# We also check if the secret file actually exists before trying to use it. + +RUN --mount=type=secret,id=enclave_key \ + set -e; \ + export SCONE_SIGNER_VERBOSE=1; \ + \ + if [ ! -f /run/secrets/enclave_key ]; then \ + echo "ERROR: enclave_key secret not found! Did you forget --secret? Use --secret id=enclave_key,src=path/to/key"; \ + exit 1; \ + fi; \ + \ + echo "== BEFORE SIGNING =="; \ + scone-signer info /usr/local/bin/edgeless_container_function | head -n 20; \ + \ + echo "== SIGNING ENCLAVE =="; \ + scone-signer sign --production --key /run/secrets/enclave_key /usr/local/bin/edgeless_container_function; \ + \ + echo "== AFTER SIGNING =="; \ + scone-signer info /usr/local/bin/edgeless_container_function > /tmp/sign_info; \ + head -n 40 /tmp/sign_info; \ + echo "--- Verification ---"; \ + # Check the Public Key Modulus (Ensures it was signed by YOUR key) + grep -A 2 "Modulus:" /tmp/sign_info; \ + # Verify Debug is 0 (required for --production) + grep "Debug:" /tmp/sign_info; \ + # Extract the Enclave Measurement (MRENCLAVE) + grep "MRENCLAVE:" /tmp/sign_info; \ + # File hash for audit trail + sha256sum /usr/local/bin/edgeless_container_function; \ + rm /tmp/sign_info + +EXPOSE 7101 + +ENV RUST_LOG=info \ + SCONE_MODE=hw \ + SCONE_VERSION=1 \ + SCONE_LAS_ADDR=172.17.0.1:18766 \ + SCONE_CAS_ADDR=172.17.0.1:18765 \ + SCONE_CONFIG_ID=policy/edgeless_function + +# NOTE: These addresses use the Docker host bridge IP (172.17.0.1). This assumes LAS/CAS ports are published on the host +# and reachable from the container via the default Docker bridge routing. + +CMD ["edgeless_container_function", "--endpoint", "http://0.0.0.0:7101"] diff --git a/examples/sgx_container/README.md b/examples/sgx_container/README.md new file mode 100644 index 00000000..17dd458d --- /dev/null +++ b/examples/sgx_container/README.md @@ -0,0 +1,156 @@ +# EDGELESS SGX Container Function Example (SCONE) + + +This example demonstrates how to run **EDGELESS** functions inside +**Intel SGX–protected containers** using **SCONE** and the EDGELESS +**container runtime**. + +It provides a minimal, reproducible setup that: +- Builds a **Rust-based function** as a container image +- Cross-compiles the function with **SCONE** +- Includes scripts to setup a local PCCS/DCAP service, SCONE's LAS and CAS +- Includes a workflow containing a **single function** + +--- + +## Prerequisites + +To reproduce this example, the following requirements must be met. + +### 1. Local EDGELESS Build + +You must build EDGELESS locally and configure it appropriately: + +- Enable the **container runtime** in `node.toml` +- Set `guest_api_host_url` to an address **reachable from inside containers** + - ❌ Do **not** use `127.0.0.1` or `0.0.0.0` + +--- + +### 2. SCONE Infrastructure Access + +Access to the SCONE container registry is required in order to pull the appropriate images. + +1. Register at +2. Create a **Personal Access Token** with appropriate permissions +3. Log in to the SCONE registry: + +```bash +docker login registry.scontain.com +# username: +# password: +``` +___ +## Function Logic + +The function logic must be implemented in: + +```text +edgeless/edgeless_container_function/src/container_function.rs +``` +Folder layout: + +```text +~/edgeless/ +├── edgeless_container_function/ +│ └── src/container_function.rs <- Add your function logic here +├── edgeless_api/ +├── Cargo.toml +├── Cargo.lock +└── examples/ + └── sgx_container/ + ├── Dockerfile <- Dockerfile lives here + ├── build.sh + ├── workflow.json + ├── ... + └── modified_files <- files that were modified to run the example +``` +### About modified files: +- `container_devices.rs` goes to `edgeless_node/src/container_runner/` in order to choose the in-tree SGX driver + +- `container_function.rs`: implements the function logic, see folder structure above. +___ +## Building the function image + +To build the image, first generate an enclave signing private RSA key for scone-signer: + +```bash +./generate-enclave-signing-key.sh +``` +Then use the provided build script: +```bash +./build.sh +``` +___ +## Setup SGX Provisioning Certificate Caching Service (PCCS) + +Use the provided script to start a PCCS Docker container, generate and save admin/user auth tokens, patch the PCCS config with your Intel API key and token hashes, verify the service is reachable, and, optionally register the host platform with PCKIDRetrievalTool. + + +```bash +./setup-and-run-pccs.sh +``` + +--- + +## Setup and run SCONE's Local Attestation Service (LAS) + +LAS runs on the node. It helps SGX apps create attestation quotes and connects them to the attestation infrastructure (like PCCS), so the enclave can prove it’s genuine and running trusted code. + +```bash +./setup-las.sh +docker compose -f docker-las-compose.yml up -d +``` +___ + +## Setup and run SCONE's Configuration and Attestation Service (CAS) +The role of CAS is to verify enclave attestation and securely deliver the function’s secrets only to trusted enclaves. + +### CAS setup and run + +```bash +./setup-cas.sh +docker compose -f docker-cas-compose.yml up -d +``` + +This script sets up CAS locally by preparing the CAS config files, detecting SGX devices and host SGX-related group IDs, generating the Docker Compose setup (including cas-init), computing and saving the CAS hash (SCONE_HASH=1), and mounting sgx_default_qcnl.conf if it exists next to the script. + +### SCONE CAS provisioning + +CAS must first be provisioned, meaning it is initialized as a trusted service (with its own identity/keys) so it can securely manage secrets and attestation decisions. + +```bash +./provision-cas.sh +``` + +The script provisions CAS in HW mode by detecting SGX devices/groups, asks whether to do attested provisioning, and then provisions cas in a Docker container using LAS and your provisioning token/key hash (optionally verifying CAS via CAS_MRENCLAVE). +___ + +### Policy upload to CAS + +After provisioning, CAS is ready to accept policies (i.e. security configuration and rules for apps). CAS uses those policies to decide which enclaves are allowed to receive secrets based on enclave identity measurements like: + +- MRENCLAVE: the exact enclave code hash (specific build) +- MRSIGNER: the signer identity (who signed the enclave), allowing trust across compatible versions signed by the same key + + +```bash +./attest-cas-upload_policy.sh +``` + +This script attests CAS (optionally pinned to a specific MRENCLAVE), uploads a policy/session file to it, and finally verifies that the policy was stored successfully. + +A dummy `policy.yml` file is provided. Also, in case you want to completely remove CAS by deleting the CAS container, remove the CAS data volume, and clean the generated local CAS files/artifacts, use: + +```bash +./reset-cas.sh +``` +___ + + +## Notes +1. SCONE cross-compilation currently uses an **older EDGELESS commit** that does **not** depend on `aws-lc-sys`. Newer versions introduce a dependency on `aws-lc-sys`, a low-level +crate wrapping C and assembly code, which prevents successful cross-compilation. This does not refer to the local copy of your EDGELESS but to the commit brought in the build stage. + +2. The image name *must* contain `edgeless-sgx-function- ` otherwise EDGELESS will not pass the sgx driver to docker. See [docker_utils.rs](https://github.com/edgeless-project/edgeless/blob/main/edgeless_node/src/container_runner/docker_utils.rs) and [container_devices.rs](https://github.com/edgeless-project/edgeless/blob/main/edgeless_node/src/container_runner/container_devices.rs) + diff --git a/examples/sgx_container/attest-cas-upload_policy.sh b/examples/sgx_container/attest-cas-upload_policy.sh new file mode 100644 index 00000000..0334128a --- /dev/null +++ b/examples/sgx_container/attest-cas-upload_policy.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# Attest CAS and upload a policy/session to it. +# Works with debug CAS using "only_for_testing" flags. + +POLICY_FILE="${1:-policy.yaml}" +CAS_ADDR="${CAS_ADDR:-cas:8081}" +LAS_ADDR="${LAS_ADDR:-las:18766}" +SCONE_NET="${SCONE_NET:-scone-net}" +CLI_IMAGE="${CLI_IMAGE:-registry.scontain.com/sconecuratedimages/crosscompilers:ubuntu24.04-scone6.0.5}" +CAS_MRENCLAVE="${CAS_MRENCLAVE:-}" + +if [[ ! -f "$POLICY_FILE" ]]; then + echo "ERROR: Policy file not found: $POLICY_FILE" >&2 + exit 1 +fi + +echo "--- Configuration ---" +echo "CAS_ADDR: $CAS_ADDR" +echo "LAS_ADDR: $LAS_ADDR" +echo "SCONE_NET: $SCONE_NET" +echo "POLICY_FILE: $POLICY_FILE" + +if [[ -n "$CAS_MRENCLAVE" ]]; then + echo "CAS_MRENCLAVE pinned: $CAS_MRENCLAVE" +else + echo "CAS_MRENCLAVE not set -> using --only_for_testing-trust-any (debug CAS)" +fi +echo "----------------------" + +# We mount $PWD to /side so the policy file is visible inside the CLI container. +# We mount ~/.cas to /root/.cas because the SCONE CLI stores its client identity + attestation cache there. +# NOTE: Without --user, this container runs as root and may create/overwrite root-owned files in ~/.cas on the host. +docker run --rm -it \ + --network "$SCONE_NET" \ + -v "$PWD:/side" \ + -v "$HOME/.cas:/root/.cas" \ + -w /side \ + -e "SCONE_CAS_ADDR=$CAS_ADDR" \ + -e "SCONE_LAS_ADDR=$LAS_ADDR" \ + -e "CAS_MRENCLAVE=$CAS_MRENCLAVE" \ + -e "POLICY_FILE=$POLICY_FILE" \ + "$CLI_IMAGE" \ + bash -lc ' + set -euo pipefail + + ATTEST_OPTS=( + --only_for_testing-ignore-signer + --only_for_testing-debug + --accept-group-out-of-date + --accept-configuration-needed + ) + + echo "Attesting CAS" + if [[ -n "${CAS_MRENCLAVE:-}" ]]; then + scone cas attest "${ATTEST_OPTS[@]}" --mrenclave "${CAS_MRENCLAVE}" "${SCONE_CAS_ADDR}" + else + scone cas attest "${ATTEST_OPTS[@]}" --only_for_testing-trust-any "${SCONE_CAS_ADDR}" + fi + + echo "Creating session from policy" + scone session create --cas="${SCONE_CAS_ADDR}" "/side/${POLICY_FILE}" + + echo "Verifying stored session in CAS" + scone session read --cas="${SCONE_CAS_ADDR}" policy \ + | grep -nE "name:|version:|mrenclaves:|DEMO_SECRET|SCONE_ALLOW_DLOPEN|SCONE_LOG" || true + + echo "Success: Policy uploaded." + ' + diff --git a/examples/sgx_container/build-function.sh b/examples/sgx_container/build-function.sh new file mode 100644 index 00000000..4d77674b --- /dev/null +++ b/examples/sgx_container/build-function.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# Go to repo root (two levels up from examples/sgx_container) +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# CAUTION: only images whose name contains edgeless-sgx-function- will get the SGX device mapping +# see edgeless/edgeless_node/src/container_runner/docker_utils.rs +IMAGE_TAG="edgeless-sgx-function-rust:dev" + +KEY_FILE="${SCRIPT_DIR}/enclave-key.pem" + +echo "Building container function image: ${IMAGE_TAG}" +echo "Repo root: ${REPO_ROOT}" +echo "Signing key: ${KEY_FILE}" + +if [[ ! -f "$KEY_FILE" ]]; then + echo "ERROR: Signing key not found at: $KEY_FILE" + echo "Generate it first with ./gen-enclave-key.sh)" + exit 1 +fi + +DOCKER_BUILDKIT=1 docker build --progress=plain --no-cache \ + --secret id=enclave_key,src="$KEY_FILE" \ + -t "${IMAGE_TAG}" \ + -f "${REPO_ROOT}/examples/sgx_container/Dockerfile" \ + "${REPO_ROOT}" 2>&1 | tee "${SCRIPT_DIR}/build.log" + +echo "Build complete." + diff --git a/examples/sgx_container/dsd.sh b/examples/sgx_container/dsd.sh new file mode 100644 index 00000000..ccf0332f --- /dev/null +++ b/examples/sgx_container/dsd.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +# Determine SGX device nodes. Usage: source ./dsd.sh + +export SGX_ENCLAVE_DEV="" +export SGX_PROVISION_DEV="" + +# Detect Enclave Device +if [[ -c /dev/sgx_enclave ]]; then + export SGX_ENCLAVE_DEV="/dev/sgx_enclave" +elif [[ -c /dev/sgx/enclave ]]; then + export SGX_ENCLAVE_DEV="/dev/sgx/enclave" +elif [[ -c /dev/isgx ]]; then + export SGX_ENCLAVE_DEV="/dev/isgx" +fi + +# Detect Provision Device +if [[ -c /dev/sgx_provision ]]; then + export SGX_PROVISION_DEV="/dev/sgx_provision" +elif [[ -c /dev/sgx/provision ]]; then + export SGX_PROVISION_DEV="/dev/sgx/provision" +fi diff --git a/examples/sgx_container/generate-enclave-signing-key.sh b/examples/sgx_container/generate-enclave-signing-key.sh new file mode 100644 index 00000000..07a178b5 --- /dev/null +++ b/examples/sgx_container/generate-enclave-signing-key.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# Generates an SGX/SCONE enclave signing key (RSA) for scone-signer +# SCONE/SGX Requirement: 3072 bits and Public Exponent = 3 +KEY_FILE="${KEY_FILE:-enclave-key.pem}" +BITS="3072" # Fixed to SGX standard + +command -v openssl >/dev/null 2>&1 || { echo "ERROR: openssl not found"; exit 1; } + +# If the key already exists, don't overwrite it without warning +if [[ -f "$KEY_FILE" ]]; then + echo "Warning: $KEY_FILE already exists. Backing up to ${KEY_FILE}.bak" + mv "$KEY_FILE" "${KEY_FILE}.bak" +fi + +umask 077 +# The '-3' flag sets the public exponent to 3 +openssl genrsa -out "$KEY_FILE" -3 "$BITS" >/dev/null 2>&1 +chmod 600 "$KEY_FILE" + +echo "Success: Wrote SGX-compatible key to $(pwd)/$KEY_FILE" +echo "--- Verification ---" +openssl rsa -in "$KEY_FILE" -text -noout | grep -E "Public-Key|Exponent" diff --git a/examples/sgx_container/get-function-mrenclave.sh b/examples/sgx_container/get-function-mrenclave.sh new file mode 100644 index 00000000..4b552952 --- /dev/null +++ b/examples/sgx_container/get-function-mrenclave.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + + +LOG_FILE="build.log" + +#Extracts MRENCLAVEs, clean up the output and get the last two unique values (Non-EDMM is listed first in the log) +MRENCLAVES=$(sed -n '/== AFTER SIGNING ==/,$p' "$LOG_FILE" | \ + grep "MRENCLAVE:" | \ + grep -oE '[a-f0-9]{64}' | \ + tail -n 2) + +# Assign to variables +NON_EDMM=$(echo "$MRENCLAVES" | sed -n '1p') +EDMM=$(echo "$MRENCLAVES" | sed -n '2p') + +echo "Function MRENCLAVE go into the session/policy YAML that will be uploaded to CAS" +echo "" +echo "mrenclave:" +echo " - \"$EDMM\" # Use for SGX2 (Modern CPUs/Dynamic Memory Management)" +echo " - \"$NON_EDMM\" # Use for SGX1 (Legacy CPUs/Static Memory Management)" +echo "You can use both to ensure your function runs on any SGX hardware." diff --git a/examples/sgx_container/install-intel-pckidrt-debian12.sh b/examples/sgx_container/install-intel-pckidrt-debian12.sh new file mode 100644 index 00000000..1b0b30c1 --- /dev/null +++ b/examples/sgx_container/install-intel-pckidrt-debian12.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# Versions/URLs (from Intel DCAP "latest" Debian12 directory) +DCAP_BASE="https://download.01.org/intel-sgx/latest/dcap-latest/linux" +DEB12_DIR="$DCAP_BASE/distro/Debian12" +LOCAL_REPO_TGZ="sgx_debian_local_repo.tgz" +PCK_TGZ="PCKIDRetrievalTool_v1.24.100.2.tar.gz" +SHA_CFG="$DCAP_BASE/SHA256SUM_dcap_1.24.cfg" + +# Install locations +INTEL_OPT="/opt/intel" +REPO_DIR="$INTEL_OPT/sgx_debian_local_repo" +KEYRING_DIR="/etc/apt/keyrings" +KEYRING_FILE="$KEYRING_DIR/intel-sgx-keyring.asc" +APT_LIST="/etc/apt/sources.list.d/sgx_debian_local_repo.list" + +WORKDIR="${WORKDIR:-/tmp/intel-sgx-install}" +mkdir -p "$WORKDIR" +cd "$WORKDIR" + +need_cmd() { command -v "$1" >/dev/null 2>&1; } +as_root() { if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then echo "Run as root (sudo)."; exit 1; fi; } + +as_root + +# Base deps +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +apt-get install -y --no-install-recommends \ + ca-certificates curl gnupg tar gzip coreutils awk sed grep + +download() { + local url="$1" out="$2" + if [[ -f "$out" ]]; then + echo "Already have $out" + else + echo "Downloading $url" + curl -fsSLo "$out" "$url" + fi +} + +verify_sha256_from_cfg() { + local file="$1" cfg="$2" path_in_cfg="$3" + local local_sum remote_sum + local_sum="$(sha256sum "$file" | awk '{print $1}')" + remote_sum="$(curl -fsSL "$cfg" | tr ' ' '\n' | paste - - | awk -v p="$path_in_cfg" '$2==p {print $1}')" + + if [[ -z "$remote_sum" ]]; then + echo "ERROR: Could not find checksum entry for $path_in_cfg in $cfg" + exit 1 + fi + if [[ "$local_sum" != "$remote_sum" ]]; then + echo "ERROR: Checksum mismatch for $file" + echo " local: $local_sum" + echo " remote: $remote_sum" + exit 1 + fi + echo "Checksum OK for $file" +} + +# 1) Local repo tgz +download "$DEB12_DIR/$LOCAL_REPO_TGZ" "$LOCAL_REPO_TGZ" +verify_sha256_from_cfg "$LOCAL_REPO_TGZ" "$SHA_CFG" "distro/Debian12/$LOCAL_REPO_TGZ" + +mkdir -p "$INTEL_OPT" +# Re-extract cleanly each run to avoid stale repo content +rm -rf "$REPO_DIR" +tar xzf "$LOCAL_REPO_TGZ" -C "$INTEL_OPT" + +# 2) Add APT source (Debian 12 = bookworm) +mkdir -p "$KEYRING_DIR" +cp -f "$REPO_DIR/keys/intel-sgx.key" "$KEYRING_FILE" + +cat > "$APT_LIST" < $BIN_PATH" +fi + +echo +echo "Done." +echo "Verify:" +echo " systemctl status sgx-aesm.service --no-pager" +echo " PCKIDRetrievalTool || true" diff --git a/examples/sgx_container/install-retrieval-tool.sh b/examples/sgx_container/install-retrieval-tool.sh new file mode 100644 index 00000000..b9842908 --- /dev/null +++ b/examples/sgx_container/install-retrieval-tool.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# 1. Download and xtract to a system-wide path (/opt/intel) +echo "Downloading and extracting to /opt/intel" +curl -fsSLO https://download.01.org/intel-sgx/latest/linux-latest/distro/Debian12/sgx_debian_local_repo.tgz +sudo mkdir -p /opt/intel +sudo tar xzf sgx_debian_local_repo.tgz -C /opt/intel + +# 2. Fix Permissions (rucial for Debian 12 _apt user) +echo "Fixing permissions" +sudo chown -R root:root /opt/intel/sgx_debian_local_repo +sudo find /opt/intel/sgx_debian_local_repo -type d -exec chmod 755 {} + +sudo find /opt/intel/sgx_debian_local_repo -type f -exec chmod 644 {} + + +# 3. Setup Keyring +echo "Setting up keyring" +sudo mkdir -p /etc/apt/keyrings +sudo cp /opt/intel/sgx_debian_local_repo/keys/intel-sgx.key /etc/apt/keyrings/intel-sgx-keyring.asc + +# 4. Create the correct local source list +echo "deb [signed-by=/etc/apt/keyrings/intel-sgx-keyring.asc arch=amd64] file:///opt/intel/sgx_debian_local_repo bookworm main" | \ + sudo tee /etc/apt/sources.list.d/sgx_debian_local_repo.list + +# 5. Update and Install +sudo apt-get update +sudo apt-get install -y sgx-pck-id-retrieval-tool diff --git a/examples/sgx_container/modified_files/container_devices.rs b/examples/sgx_container/modified_files/container_devices.rs new file mode 100644 index 00000000..bc1716b4 --- /dev/null +++ b/examples/sgx_container/modified_files/container_devices.rs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: © 2024 Technical University of Crete +// SPDX-License-Identifier: MIT + +use rs_docker::container::DeviceStruct; + +pub fn get_sgx_out_of_tree_driver() -> DeviceStruct { + DeviceStruct { + CgroupPermissions: "rwm".to_string(), + PathOnHost: "/dev/isgx".to_string(), + PathInContainer: "/dev/isgx".to_string(), + } +} + +pub fn get_sgx_in_tree_driver() -> DeviceStruct { + DeviceStruct { + CgroupPermissions: "rwm".to_string(), + PathOnHost: "/dev/sgx_enclave".to_string(), + PathInContainer: "/dev/sgx_enclave".to_string(), + } +} + +pub fn get_sgx_nuc_driver() -> DeviceStruct { + get_sgx_in_tree_driver() +} diff --git a/examples/sgx_container/modified_files/container_function.rs b/examples/sgx_container/modified_files/container_function.rs new file mode 100644 index 00000000..248e0e56 --- /dev/null +++ b/examples/sgx_container/modified_files/container_function.rs @@ -0,0 +1,269 @@ +// SPDX-FileCopyrightText: © 2024 Claudio Cicconetti +// SPDX-License-Identifier: MIT + +use edgeless_api::outer::container_runtime::ContainerRuntimeAPI; +use futures::{Future, SinkExt, StreamExt}; + +pub struct ContainerFunction { + sender: futures::channel::mpsc::UnboundedSender, +} + +enum ContainerFunctionRequest { + Boot( + edgeless_api::guest_api_function::BootData, + tokio::sync::oneshot::Sender>, + ), + Init( + edgeless_api::guest_api_function::FunctionInstanceInit, + tokio::sync::oneshot::Sender>, + ), + Cast(edgeless_api::guest_api_function::InputEventData), + Call( + edgeless_api::guest_api_function::InputEventData, + tokio::sync::oneshot::Sender>, + ), + Stop(), +} + +enum FiniteStateMachine { + PreBoot, + Booted, + Initialized, + Stopped, + Error, +} + +impl ContainerFunction { + pub fn new() -> (Self, std::pin::Pin + Send>>) { + log::debug!("new container function created"); + let (sender, receiver) = futures::channel::mpsc::unbounded(); + + let main_task = Box::pin(async move { + Self::main_task(receiver).await; + }); + + (Self { sender }, main_task) + } + + async fn main_task(receiver: futures::channel::mpsc::UnboundedReceiver) { + let mut receiver = receiver; + + // Always-visible diagnostic (doesn't depend on log backend) + eprintln!("MAIN_TASK started"); + + // Initialized in BOOT + let mut host_client; + let mut host_client_api = None; + + // InstanceID assigned to this function, initialized in BOOT + let mut instance_id = None; + + // Finite state machine of the function + let mut fsm = FiniteStateMachine::PreBoot; + + // Main loop that reacts to messages on the receiver channel + while let Some(req) = receiver.next().await { + match req { + ContainerFunctionRequest::Boot(boot_data, ack) => { + eprintln!( + "BOOT received (endpoint={}, instance_id={})", + boot_data.guest_api_host_endpoint, boot_data.instance_id + ); + + if std::mem::discriminant(&fsm) != std::mem::discriminant(&FiniteStateMachine::PreBoot) { + let _ = ack.send(Err(anyhow::anyhow!("boot received while not in PreBoot"))); + log::error!("received boot command while not in a pre-boot state: ignored"); + } else { + match edgeless_api::grpc_impl::outer::container_runtime::ContainerRuntimeAPIClient::new( + boot_data.guest_api_host_endpoint.as_str(), + None, + ) + .await + { + Ok(client) => { + host_client = Some(client); + host_client_api = Some(host_client.unwrap().guest_api_host()); + instance_id = Some(boot_data.instance_id); + fsm = FiniteStateMachine::Booted; + + let _ = ack.send(Ok(())); + eprintln!("BOOT done -> state Booted"); + } + Err(err) => { + log::error!("container function boot error: {}", err); + fsm = FiniteStateMachine::Error; + + let _ = ack.send(Err(anyhow::anyhow!("boot error: {}", err))); + eprintln!("BOOT error: {}", err); + } + } + } + } + ContainerFunctionRequest::Init(init_data, ack) => { + eprintln!( + "INIT received (payload_len={}, state_len={})", + init_data.init_payload.len(), + init_data.serialized_state.len() + ); + + if std::mem::discriminant(&fsm) != std::mem::discriminant(&FiniteStateMachine::Booted) { + let _ = ack.send(Err(anyhow::anyhow!("init received while not in Booted"))); + log::error!("received init command while not in a booted state: ignored"); + } else { + // Add init logic here + eprintln!("INIT_HOOK hello from inside init"); + + // IMPORTANT: don't log secret values in production; log presence only. + match std::env::var("DEMO_SECRET") { + Ok(value) => eprintln!("DEMO_SECRET is: {}", value), + Err(e) => eprintln!("Couldn't read DEMO_SECRET: {}", e), + } + + fsm = FiniteStateMachine::Initialized; + + eprintln!("INIT_HOOK done -> state Initialized"); + let _ = ack.send(Ok(())); + } + } + ContainerFunctionRequest::Cast(event) => { + log::info!("cast, src {}, msg {} bytes", event.src, event.msg.len()); + if std::mem::discriminant(&fsm) != std::mem::discriminant(&FiniteStateMachine::Initialized) { + log::error!("received cast command while not in an initialized state: ignored"); + } else { + // Add cast logic here. + log::info!("CAST_HOOK hello from inside cast"); + + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + if let Some(ref mut host_client_api) = host_client_api { + match host_client_api + .cast(edgeless_api::guest_api_host::OutputEventData { + originator: instance_id.unwrap(), + alias: "output".to_string(), + msg: event.msg, + }) + .await + { + Err(err) => { + fsm = FiniteStateMachine::Error; + log::error!("error when casting an event to alias \"output\": {}", err); + } + _ => { + log::info!("event recasted successfully to \"output\""); + } + } + } + } + } + ContainerFunctionRequest::Call(event, reply_sender) => { + log::info!("call, src {}, msg {} bytes", event.src, event.msg.len()); + let res = match std::mem::discriminant(&fsm) == std::mem::discriminant(&FiniteStateMachine::Initialized) { + false => { + log::error!("received call command while not in an initialized state: ignored"); + edgeless_api::guest_api_function::CallReturn::Err + } + true => { + // Add call logic here. + edgeless_api::guest_api_function::CallReturn::Reply(event.msg) + } + }; + match reply_sender.send(Ok(res)) { + Ok(_) => {} + Err(err) => { + fsm = FiniteStateMachine::Error; + log::error!("internal communication error on call command: {:?}", err); + } + } + } + ContainerFunctionRequest::Stop() => { + log::info!("stop"); + if std::mem::discriminant(&fsm) != std::mem::discriminant(&FiniteStateMachine::Initialized) { + log::error!("received stop command while not in an initialized state: ignored"); + } else { + // Add stop logic here. + fsm = FiniteStateMachine::Stopped; + } + } + } + } + } + + pub fn get_api_client(&mut self) -> Box { + Box::new(ContainerFunctionClient { + container_function_client: Box::new(GuestAPIFunctionClient { sender: self.sender.clone() }), + }) + } +} + +pub struct ContainerFunctionClient { + container_function_client: Box, +} + +impl edgeless_api::outer::container_function::ContainerFunctionAPI for ContainerFunctionClient { + fn guest_api_function(&mut self) -> Box { + self.container_function_client.clone() + } +} + +#[derive(Clone)] +pub struct GuestAPIFunctionClient { + sender: futures::channel::mpsc::UnboundedSender, +} + +#[async_trait::async_trait] +impl edgeless_api::guest_api_function::GuestAPIFunction for GuestAPIFunctionClient { + async fn boot(&mut self, boot_data: edgeless_api::guest_api_function::BootData) -> anyhow::Result<()> { + let (ack_tx, ack_rx) = tokio::sync::oneshot::channel::>(); + + self.sender + .send(ContainerFunctionRequest::Boot(boot_data.clone(), ack_tx)) + .await + .map_err(|err| anyhow::anyhow!("GuestAPIFunction::boot channel error: {}", err))?; + + ack_rx + .await + .map_err(|err| anyhow::anyhow!("GuestAPIFunction::boot ack dropped: {}", err))? + } + + async fn init(&mut self, init_data: edgeless_api::guest_api_function::FunctionInstanceInit) -> anyhow::Result<()> { + let (ack_tx, ack_rx) = tokio::sync::oneshot::channel::>(); + + self.sender + .send(ContainerFunctionRequest::Init(init_data.clone(), ack_tx)) + .await + .map_err(|err| anyhow::anyhow!("GuestAPIFunction::init channel error: {}", err))?; + + ack_rx + .await + .map_err(|err| anyhow::anyhow!("GuestAPIFunction::init ack dropped: {}", err))? + } + + async fn cast(&mut self, event: edgeless_api::guest_api_function::InputEventData) -> anyhow::Result<()> { + match self.sender.send(ContainerFunctionRequest::Cast(event.clone())).await { + Ok(_) => Ok(()), + Err(err) => Err(anyhow::anyhow!("GuestAPIFunction::cast channel error: {}", err)), + } + } + + async fn call( + &mut self, + event: edgeless_api::guest_api_function::InputEventData, + ) -> anyhow::Result { + let (reply_sender, reply_receiver) = + tokio::sync::oneshot::channel::>(); + match self.sender.send(ContainerFunctionRequest::Call(event.clone(), reply_sender)).await { + Ok(_) => match reply_receiver.await { + Ok(ret) => ret, + Err(err) => Err(anyhow::anyhow!("GuestAPIFunction::call error: {}", err)), + }, + Err(err) => Err(anyhow::anyhow!("GuestAPIFunction::call channel error: {}", err)), + } + } + + async fn stop(&mut self) -> anyhow::Result<()> { + match self.sender.send(ContainerFunctionRequest::Stop()).await { + Ok(_) => Ok(()), + Err(err) => Err(anyhow::anyhow!("GuestAPIFunction::stop channel error: {}", err)), + } + } +} + diff --git a/examples/sgx_container/policy.yaml b/examples/sgx_container/policy.yaml new file mode 100644 index 00000000..4bcf4dc1 --- /dev/null +++ b/examples/sgx_container/policy.yaml @@ -0,0 +1,39 @@ +name: policy +version: "0.3.10" +#predecessor: "6bc7726822ae70d66b13673f54402b22d0b6d6d4062bf1bb4c4eb622e7a537ef" + +security: + attestation: + tolerate: + - "insecure-configuration" + - "outdated-tcb" + ignore_advisories: + - "INTEL-SA-00289" + - "INTEL-SA-00334" + - "INTEL-SA-00614" + - "INTEL-SA-00615" + - "INTEL-SA-00617" + - "INTEL-SA-00828" + +# A service in a SCONE session is a SCONE program with a specific software state and configuration +services: + - name: edgeless_function + #the command to be executed; match the CMD in function's Dockerfile + command: ["edgeless_container_function", "--endpoint", "http://0.0.0.0:7101"] + #the process working directory from which path resolution of relative paths starts + pwd: / + attestation: + mrenclave: + - b59a1afc14c69927bcd6847f033bff976e10e48c0a5c1b860576c70cd8ede1b1 + environment: + #Values that are needed to provide the expected service + #Pre-existing variables are not trusted, only those in the policy will be available to SCONE program + DEMO_SECRET: "$$SCONE::enclave_secret$$" + #used to inject the secret into the service's environment + SCONE_ALLOW_DLOPEN: "2" + SCONE_LOG: "DEBUG" + +secrets: + - name: enclave_secret + kind: ascii + value: "hello, EDGELESS enclave" diff --git a/examples/sgx_container/provision-cas.sh b/examples/sgx_container/provision-cas.sh new file mode 100644 index 00000000..85ea5670 --- /dev/null +++ b/examples/sgx_container/provision-cas.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/dsd.sh" +source "$SCRIPT_DIR/sgx-groups.sh" +sgx_detect_groups + +if [[ -z "${SGX_ENCLAVE_DEV:-}" ]]; then + echo "Error: No SGX enclave device found. HW mode required." + exit 1 +fi + +# --- CONFIG --- +CAS_ADDR="${CAS_ADDR:-cas:8081}" +CAS_PROVISIONING_TOKEN="${CAS_PROVISIONING_TOKEN:?set CAS_PROVISIONING_TOKEN}" +CAS_KEY_HASH="${CAS_KEY_HASH:?set CAS_KEY_HASH}" + +echo "Using SGX enclave device: $SGX_ENCLAVE_DEV" +[[ -n "${SGX_PROVISION_DEV:-}" ]] && echo "Using SGX provision device: $SGX_PROVISION_DEV" +echo "Using CAS_ADDR: $CAS_ADDR" +echo "Using LAS via Docker DNS: las:18766" +[[ -n "${SGX_GID:-}" ]] && echo "Host sgx GID: ${SGX_GID}" +[[ -n "${SGX_PRV_GID:-}" ]] && echo "Host sgx_prv GID: ${SGX_PRV_GID}" + +# Ask user about attestation +read -r -p "Do you want to attest CAS during provisioning? (y/n): " ATTEST +ATTEST="${ATTEST,,}" # lowercase + +PROVISION_MODE_ARGS=() +if [[ "$ATTEST" == "y" || "$ATTEST" == "yes" ]]; then + if [[ -z "${CAS_MRENCLAVE:-}" ]]; then + echo "CAS_MRENCLAVE is not set." + echo "Use: get_mrenclave.sh and export to the variable CAS_MRENCLAVE. Then exit." + exit 1 + fi + PROVISION_MODE_ARGS=(with-attestation --mrenclave "$CAS_MRENCLAVE") +else + PROVISION_MODE_ARGS=(only_for_testing-without-attestation) +fi + +# Build group-add args (may be empty) +GROUP_ARGS="$(sgx_group_add_docker_args || true)" + +# shellcheck disable=SC2086 +docker run --rm -it \ + --network scone-net \ + -v "$PWD:$PWD" -w "$PWD" \ + --device "$SGX_ENCLAVE_DEV" \ + ${SGX_PROVISION_DEV:+--device "$SGX_PROVISION_DEV"} \ + ${GROUP_ARGS} \ + -e SCONE_MODE=HW \ + -e SCONE_LAS_ADDR=las:18766 \ + registry.scontain.com/sconecuratedimages/crosscompilers:ubuntu24.04-scone6.0.5 \ + scone cas provision \ + --token "$CAS_PROVISIONING_TOKEN" \ + -c "$CAS_KEY_HASH" \ + --config-file ./cas/cas-default-owner-config.toml \ + "$CAS_ADDR" \ + "${PROVISION_MODE_ARGS[@]}" + +echo "CAS provisioning complete." + diff --git a/examples/sgx_container/reset-cas.sh b/examples/sgx_container/reset-cas.sh new file mode 100644 index 00000000..5f75f801 --- /dev/null +++ b/examples/sgx_container/reset-cas.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +set -euo pipefail + +# Reset SCONE CAS containers + CAS data volume + generated local files for this repo. +# This is destructive: it removes CAS state (DB/keys), containers, and local artifacts. + +CAS_CONTAINER="${CAS_CONTAINER:-cas}" +CAS_INIT_CONTAINER="${CAS_INIT_CONTAINER:-cas-init}" + +# Your repo shows the volume name as sgx_container_cas-data (compose-style). +CAS_VOLUME="${CAS_VOLUME:-sgx_container_cas-data}" + +# Local artifacts in the current directory you want removed +FILES_TO_REMOVE=( + "cas" + "cas.mrenclave.out" + "docker-cas-compose.yml" +) + +echo "Stopping/removing containers (ignore if missing): $CAS_CONTAINER $CAS_INIT_CONTAINER" +docker rm -f "$CAS_CONTAINER" >/dev/null 2>&1 || true +docker rm -f "$CAS_INIT_CONTAINER" >/dev/null 2>&1 || true + +echo "Removing CAS data volume (ignore if missing): $CAS_VOLUME" +docker volume rm "$CAS_VOLUME" >/dev/null 2>&1 || true + +echo "Removing local artifacts:" +for f in "${FILES_TO_REMOVE[@]}"; do + if [[ -e "$f" ]]; then + rm -rf -- "$f" + echo " removed: $f" + else + echo " missing: $f" + fi +done + +echo "Done." diff --git a/examples/sgx_container/scone-list-registry-tags.sh b/examples/sgx_container/scone-list-registry-tags.sh new file mode 100644 index 00000000..9315061e --- /dev/null +++ b/examples/sgx_container/scone-list-registry-tags.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +# +# How to use: +# export SCONE_HUB_ACCESS_TOKEN= +# export REPO= # e.g. "cli", "services", "crosscompilers" +# ./list-images.sh +# +# This script: +# 1) Checks required environment variables +# 2) Finds the GitLab project ID for the given repo +# 3) Lists available Docker image tags in the registry +# + +set -euo pipefail + +# 1) Check required environment variables +if [[ -z "${SCONE_HUB_ACCESS_TOKEN:-}" ]]; then + echo "Error: SCONE_HUB_ACCESS_TOKEN is not set" + exit 1 +fi + +if [[ -z "${REPO:-}" ]]; then + echo "Error: REPO is not set" + exit 1 +fi + +# 2) Query GitLab for the project ID +PROJECT_ID=$( + curl -s \ + -H "PRIVATE-TOKEN: $SCONE_HUB_ACCESS_TOKEN" \ + "https://gitlab.scontain.com/api/v4/projects?search=$REPO" \ + | jq -r '.[0].id' +) + +if [[ -z "$PROJECT_ID" || "$PROJECT_ID" == "null" ]]; then + echo "Error: Could not find project ID for repo '$REPO'" + exit 1 +fi + +export PROJECT_ID +echo "Found PROJECT_ID=$PROJECT_ID for repo '$REPO'" + +# 3) List image tags in the repository +echo "Available image tags:" +curl -s \ + -H "PRIVATE-TOKEN: $SCONE_HUB_ACCESS_TOKEN" \ + "https://gitlab.scontain.com/api/v4/projects/$PROJECT_ID/registry/repositories?tags=1" \ +| jq -r '.[].tags[].name' diff --git a/examples/sgx_container/setup-and-run-pccs.sh b/examples/sgx_container/setup-and-run-pccs.sh new file mode 100644 index 00000000..e0180d19 --- /dev/null +++ b/examples/sgx_container/setup-and-run-pccs.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +# Usage: +# # Start PCCS (requires Intel PCS API key): +# INTEL_API_KEY="YOUR_INTEL_PCS_API_KEY" ./setup-and-run-pccs.sh +# +# # Start PCCS and also register this machine's platform using PCKIDRetrievalTool: +# INTEL_API_KEY="YOUR_INTEL_PCS_API_KEY" REGISTER_PLATFORM=true ./setup-and-run-pccs.sh +# +# Notes: +# - The script prints (and also writes to ./pccs-tokens.env) the raw ADMIN_TOKEN and USER_TOKEN. +# - Use USER_TOKEN with PCKIDRetrievalTool and ADMIN_TOKEN for PCCS admin API calls. +# - PCCS runs on https://localhost:8081 and typically uses a self-signed cert (curl uses -k). + +set -euo pipefail + +# 1) Check required environment variable +if [[ -z "${INTEL_API_KEY:-}" ]]; then + echo "Error: INTEL_API_KEY is not set" + exit 1 +fi + +# 2) Generate random tokens (raw passwords) +ADMIN_TOKEN=$(openssl rand -hex 16) +USER_TOKEN=$(openssl rand -hex 16) + +# 3) Generate SHA-512 hashes for the PCCS config (PCCS expects hashes, not raw tokens) +ADMIN_HASH=$(echo -n "$ADMIN_TOKEN" | sha512sum | awk '{print $1}') +USER_HASH=$(echo -n "$USER_TOKEN" | sha512sum | awk '{print $1}') + +PCCS_IMAGE="registry.scontain.com/sconecuratedimages/services:pccs" +PCCS_CONTAINER_NAME="pccs" +PCCS_PORT=8081 +PCCS_DATA_VOLUME="pccs-data" +SCONE_NETWORK="scone-net" + +echo "Pulling PCCS image: $PCCS_IMAGE" +docker pull "$PCCS_IMAGE" + +# 4) Run PCCS container +echo "Starting PCCS on port $PCCS_PORT..." +docker run -d \ + --name "$PCCS_CONTAINER_NAME" \ + --network "$SCONE_NETWORK" \ + -p "${PCCS_PORT}:${PCCS_PORT}" \ + -e APIKEY="$INTEL_API_KEY" \ + -e PCCS_PORT="$PCCS_PORT" \ + -e ADMIN_TOKEN_HASH="$ADMIN_HASH" \ + -e USER_TOKEN_HASH="$USER_HASH" \ + -e PCCS_LOG_LEVEL=debug \ + -v "${PCCS_DATA_VOLUME}:/var/lib/pccs" \ + "$PCCS_IMAGE" >/dev/null + +# Persist tokens for automation +umask 077 +TOKENS_FILE="${TOKENS_FILE:-./pccs-tokens.env}" +cat > "$TOKENS_FILE" </dev/null + +echo "Restarting PCCS to pick up config changes..." +docker restart "$PCCS_CONTAINER_NAME" >/dev/null + +# Auth readiness (admin token) - only after patch+restart +echo "Verifying PCCS auth" +auth_ok=false +last_code="" +for i in {1..30}; do + last_code="$(curl -k -s -o /dev/null -w "%{http_code}" \ + -H "admin-token: ${ADMIN_TOKEN}" \ + "https://localhost:${PCCS_PORT}/sgx/certification/v4/platforms" || true)" + + if [[ "$last_code" != "401" && "$last_code" != "000" ]]; then + auth_ok=true + break + fi + sleep 1 +done + +if [[ "$auth_ok" != "true" ]]; then + echo "ERROR: PCCS auth check failed (last HTTP code: ${last_code:-})" + echo "Dumping configured hashes in container:" + docker exec -it "$PCCS_CONTAINER_NAME" sh -lc 'grep -nE "ApiKey|UserTokenHash|AdminTokenHash" /opt/intel/sgx-dcap-pccs/config/default.json' || true + docker logs --tail 200 "$PCCS_CONTAINER_NAME" || true + exit 1 +fi + +echo "---------------------------------------------------" +echo "PCCS is running with Authentication Enabled" +echo "---------------------------------------------------" +echo "Admin Token (Raw): $ADMIN_TOKEN" +echo "User Token (Raw): $USER_TOKEN" +echo "Tokens saved to: $TOKENS_FILE" +echo "---------------------------------------------------" +echo "Test Admin Access with:" +echo "curl -k -H \"admin-token: $ADMIN_TOKEN\" https://localhost:${PCCS_PORT}/sgx/certification/v4/platforms" + +# 5) Optionally register this platform in PCCS (recommended for local dev) +if [[ "${REGISTER_PLATFORM:-false}" == "true" ]]; then + if ! command -v PCKIDRetrievalTool >/dev/null 2>&1; then + echo "REGISTER_PLATFORM=true but PCKIDRetrievalTool not found in PATH." + echo "Install it first or add it to PATH." + exit 1 + fi + + echo "Registering platform with PCCS (this host)..." + + # Avoid sudo creating an unreadable /tmp file: + OUT="${OUT:-$HOME/pckid_retrieval.csv}" + + sudo PCKIDRetrievalTool \ + -f "$OUT" \ + -url "https://localhost:${PCCS_PORT}" \ + -user_token "$USER_TOKEN" \ + -use_secure_cert false + + # Ensure current user can read it + sudo chown "${SUDO_USER:-$USER}:${SUDO_USER:-$USER}" "$OUT" 2>/dev/null || true + sudo chmod 0644 "$OUT" 2>/dev/null || true + + echo "Platform registration attempt complete. Output: $OUT" + head -n 5 "$OUT" || true +fi diff --git a/examples/sgx_container/setup-cas.sh b/examples/sgx_container/setup-cas.sh new file mode 100644 index 00000000..0243f1b0 --- /dev/null +++ b/examples/sgx_container/setup-cas.sh @@ -0,0 +1,187 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +# +# Script to set up SCONE CAS locally: +# - pulls CAS image +# - creates ./cas directory +# - writes cas/cas-default-owner-config.toml and cas/cas.toml +# - sources dsd.sh to detect SGX devices +# - generates docker-cas-compose.yml (with cas-init) +# - computes & saves CAS hash output (SCONE_HASH=1) +# - mounts sgx_default_qcnl.conf if present next to this script +# - detects host GIDs for sgx + sgx_prv and adds group_add with numeric IDs +# +# IMPORTANT: +# source ./setup-cas.sh +# SGX_SIM_MODE=1 source ./setup-cas.sh + +set -euo pipefail +export CAS="registry.scontain.com/sconecuratedimages/services:cas" +#export CAS="registry.scontain.com/sconecuratedimages/services:cas.preprovisioned.scone.cloud" +#docker pull "$CAS" + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +CAS_DIR="$SCRIPT_DIR/cas" + +mkdir -p "$CAS_DIR" + +# Avoid root-owned ./cas trap +if [[ ! -w "$CAS_DIR" ]]; then + echo "ERROR: '$CAS_DIR' is not writable by $(id -un)." + echo "Fix: sudo chown -R $USER:$USER '$CAS_DIR'" + exit 1 +fi + +# --- Save CAS hash output --- +HASH_OUT="$SCRIPT_DIR/cas.mrenclave.out" +echo "Computing CAS enclave measurement (SCONE_HASH=1) ..." +docker run --rm -e SCONE_HASH=1 "$CAS" 2>&1 | tee "$HASH_OUT" >/dev/null +echo "Wrote: $HASH_OUT" + +# Create CAS default owner config +cat > "$CAS_DIR/cas-default-owner-config.toml" <<'EOF' +[ias] +spid = "00000000000000000000000000000000" +linkable_quotes = true +sp_key = "00000000000000000000000000000000" + +[dcap] +subscription_key = "00000000000000000000000000000000" +EOF + +# Create CAS service config +cat > "$CAS_DIR/cas.toml" <<'EOF' +[api] +api_listen = "0.0.0.0:8081" +enclave_listen = "0.0.0.0:18765" + +[database] +path = "/var/lib/cas/cas.db" +EOF + +# Detect devices via dsd.sh +source "$SCRIPT_DIR/dsd.sh" + +# Detect SGX group IDs on the host (numeric) +source "$SCRIPT_DIR/sgx-groups.sh" +sgx_detect_groups + +# SIM vs HW +SCONE_MODE="HW" +DEVICES_YAML="" + +if [[ -z "${SGX_ENCLAVE_DEV:-}" || "${SGX_SIM_MODE:-0}" == "1" ]]; then + SCONE_MODE="SIM" + echo "Configuring CAS for SIMULATION mode." +else + DEVICES_YAML=" devices: + - \"${SGX_ENCLAVE_DEV}:${SGX_ENCLAVE_DEV}\"" + if [[ -n "${SGX_PROVISION_DEV:-}" ]]; then + DEVICES_YAML="${DEVICES_YAML} + - \"${SGX_PROVISION_DEV}:${SGX_PROVISION_DEV}\"" + fi +fi + +# group_add snippet only makes sense in HW mode +GROUP_ADD_YAML="" +if [[ "${SCONE_MODE}" == "HW" ]]; then + GROUP_ADD_YAML="$(sgx_group_add_yaml || true)" + if [[ -z "${GROUP_ADD_YAML}" ]]; then + echo "WARN: Could not detect host groups 'sgx'/'sgx_prv'. CAS may not access SGX devices unless it runs as root." + fi +fi + +# Mount sgx_default_qcnl.conf if present +QCNL_VOL_LINE="" +if [[ -f "$SCRIPT_DIR/sgx_default_qcnl.conf" ]]; then + QCNL_VOL_LINE=" - \"./sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf:ro\"" + echo "Found $SCRIPT_DIR/sgx_default_qcnl.conf -> will mount into CAS." +else + echo "No $SCRIPT_DIR/sgx_default_qcnl.conf found -> not mounting QCNL config." +fi + +# CAS image runs as uid=1000/gid=1000 (you verified this) +CAS_UID="1000" +CAS_GID="1000" + +# Generate docker-compose for CAS (with cas-init) +cat > "$SCRIPT_DIR/docker-cas-compose.yml" < + sh -c "mkdir -p /var/lib/cas && + chown -R ${CAS_UID}:${CAS_GID} /var/lib/cas && + chmod -R u+rwX /var/lib/cas" + restart: "no" + + cas: + image: $CAS + container_name: cas + depends_on: + - cas-init + networks: + - scone-net + command: cas -c /etc/cas/cas.toml + environment: + - SCONE_LOG=info + - SCONE_MODE=${SCONE_MODE} + - SCONE_LAS_ADDR=las:18766 + privileged: true +EOF + +# Append group_add stanza (if available) +if [[ -n "${GROUP_ADD_YAML}" ]]; then + cat >> "$SCRIPT_DIR/docker-cas-compose.yml" <> "$SCRIPT_DIR/docker-cas-compose.yml" <> "$SCRIPT_DIR/docker-cas-compose.yml" </dev/null 2>&1; } + +as_root +need_cmd curl +need_cmd tar + +echo "[1/6] Base deps" +export DEBIAN_FRONTEND=noninteractive +apt-get update -y +apt-get install -y --no-install-recommends ca-certificates curl gnupg tar gzip findutils coreutils + +mkdir -p "$WORKDIR" +cd "$WORKDIR" + +download() { + local url="$1" out="$2" + if [[ -f "$out" ]]; then + echo " - Already have $out" + else + echo " - Downloading $url" + curl -fsSLo "$out" "$url" + fi +} + +echo "[2/6] Remove old SGX local repo list(s)" +rm -f /etc/apt/sources.list.d/sgx_local_repo.list \ + /etc/apt/sources.list.d/sgx_debian_local_repo.list \ + /etc/apt/sources.list.d/*sgx*.list || true + +echo "[3/6] Download + extract Intel SGX local repo into $INTEL_OPT" +download "$DEB12_DIR/$LOCAL_REPO_TGZ" "$LOCAL_REPO_TGZ" + +mkdir -p "$INTEL_OPT" +rm -rf "$REPO_DIR" +tar xzf "$LOCAL_REPO_TGZ" -C "$INTEL_OPT" +chown -R root:root "$REPO_DIR" +chmod -R a+rX "$REPO_DIR" + +# Sanity: ensure Release exists +if [[ ! -f "$REPO_DIR/dists/bookworm/Release" ]]; then + echo "ERROR: $REPO_DIR/dists/bookworm/Release not found. Repo extract looks wrong." + echo "Contents:" + ls -la "$REPO_DIR" || true + exit 1 +fi + +echo "[4/6] Add APT key + file:// repo source" +mkdir -p "$KEYRING_DIR" +cp -f "$REPO_DIR/keys/intel-sgx.key" "$KEYRING_FILE" +chmod 0644 "$KEYRING_FILE" + +cat > "$APT_LIST" <&2 +fi + +# Create docker-compose file +cat > "$SCRIPT_DIR/docker-las-compose.yml" <> "$SCRIPT_DIR/docker-las-compose.yml" <> "$SCRIPT_DIR/docker-las-compose.yml" <<'EOF' + restart: on-failure + ports: + - "18766:18766" + +networks: + scone-net: + external: true +EOF + +echo "Wrote: $SCRIPT_DIR/docker-las-compose.yml" +echo "AESM socket mounted: $AESM_SOCKET" diff --git a/examples/sgx_container/sgx-groups.sh b/examples/sgx_container/sgx-groups.sh new file mode 100644 index 00000000..2b9c06ac --- /dev/null +++ b/examples/sgx_container/sgx-groups.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# SPDX-FileCopyrightText: © 2026 Technical University of Crete +# SPDX-License-Identifier: MIT + +# sgx-groups.sh +# +# WHY THIS IS USED: +# SGX device nodes (/dev/sgx_enclave, /dev/sgx_provision) are usually 0660 and owned by +# root:sgx / root:sgx_prv. Non-root containers need those group IDs to access SGX HW. +# Group names often don't exist inside containers, so we detect HOST numeric GIDs for +# sgx/sgx_prv and use them in compose `group_add` or `docker run --group-add`. + +set -euo pipefail + +sgx__get_gid() { + local grp="$1" + local line gid + line="$(getent group "$grp" 2>/dev/null || true)" + [[ -z "$line" ]] && return 1 + gid="$(echo "$line" | cut -d: -f3)" + [[ -n "$gid" ]] || return 1 + echo "$gid" +} + +sgx_detect_groups() { + SGX_GID="" + SGX_PRV_GID="" + + SGX_GID="$(sgx__get_gid sgx || true)" + SGX_PRV_GID="$(sgx__get_gid sgx_prv || true)" + + export SGX_GID SGX_PRV_GID +} + +sgx_group_add_yaml() { + local out="" + [[ -n "${SGX_GID:-}" ]] || [[ -n "${SGX_PRV_GID:-}" ]] || return 0 + + out=" group_add:" + [[ -n "${SGX_GID:-}" ]] && out="${out} + - \"${SGX_GID}\" # sgx" + [[ -n "${SGX_PRV_GID:-}" ]] && out="${out} + - \"${SGX_PRV_GID}\" # sgx_prv" + + echo "$out" +} + +sgx_group_add_docker_args() { + local args=() + [[ -n "${SGX_GID:-}" ]] && args+=(--group-add "${SGX_GID}") + [[ -n "${SGX_PRV_GID:-}" ]] && args+=(--group-add "${SGX_PRV_GID}") + printf '%s ' "${args[@]:-}" +} diff --git a/examples/sgx_container/sgx_default_qcnl.conf b/examples/sgx_container/sgx_default_qcnl.conf new file mode 100644 index 00000000..b5e609f0 --- /dev/null +++ b/examples/sgx_container/sgx_default_qcnl.conf @@ -0,0 +1,4 @@ +{ + "pccs_url": "https://pccs:8081/sgx/certification/v4/", + "use_secure_cert": true +} diff --git a/examples/sgx_container/workflow.json b/examples/sgx_container/workflow.json new file mode 100644 index 00000000..e44ab6c4 --- /dev/null +++ b/examples/sgx_container/workflow.json @@ -0,0 +1,20 @@ +{ + "functions": [ + { + "name": "sgx", + "class_specification": { + "id": "sgx", + "function_type": "CONTAINER", + "version": "0.1", + "code": "container:edgeless-sgx-function-rust:dev", + "outputs": [] + }, + "output_mapping": {}, + "annotations": { + "init-payload": "nothing interesting" + } + } + ], + "resources": [], + "annotations": {} +}