Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:

launcher-nontee-check:
name: "MPC Launcher non-tee check"
runs-on: warp-ubuntu-2404-x64-2x
runs-on: warp-ubuntu-2404-x64-8x
timeout-minutes: 60
permissions:
contents: read
Expand All @@ -67,7 +67,7 @@ jobs:

docker-launcher-build-and-verify:
name: "Build MPC Launcher Docker image and verify"
runs-on: warp-ubuntu-2404-x64-2x
runs-on: warp-ubuntu-2404-x64-8x
timeout-minutes: 60
permissions:
contents: read
Expand All @@ -78,10 +78,25 @@ jobs:
with:
persist-credentials: false

- name: Install skopeo
- name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04)
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y skopeo
sudo apt-get install -y skopeo liblzma-dev podman

- name: Install repro-env
run: |
wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env'
echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c -
sudo install -m755 repro-env -t /usr/bin

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "warpbuild"

- name: Build launcher docker image and verify its hash
shell: bash
Expand Down Expand Up @@ -467,13 +482,13 @@ jobs:
run: |
python3 -m venv tee_launcher/venv
source tee_launcher/venv/bin/activate
cd tee_launcher
cd tee_launcher
pip install -r requirements.txt

- name: Run pytest
run: |
source tee_launcher/venv/bin/activate
cd tee_launcher
cd tee_launcher
PYTHONPATH=. pytest -vsx

fast-ci-checks:
Expand Down
27 changes: 21 additions & 6 deletions .github/workflows/docker_build_launcher.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ on:
jobs:
build-and-push-images:
name: "Build and push Docker launcher image with commit hash"
runs-on: warp-ubuntu-2404-x64-2x
runs-on: warp-ubuntu-2404-x64-8x
permissions:
contents: read

Expand All @@ -23,17 +23,32 @@ jobs:
with:
persist-credentials: false

- name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04)
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y skopeo liblzma-dev podman

- name: Install repro-env
run: |
wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env'
echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c -
sudo install -m755 repro-env -t /usr/bin

- name: Cache Rust dependencies
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "warpbuild"

- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Install skopeo
run: |
sudo apt-get update
sudo apt-get install -y skopeo

- name: Build and push launcher image
run: |
export LAUNCHER_IMAGE_NAME=mpc-launcher
Expand Down
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ members = [
"crates/primitives",
"crates/tee-authority",
"crates/tee-context",
"crates/tee-launcher",
"crates/test-migration-contract",
"crates/test-parallel-contract",
"crates/test-port-allocator",
Expand Down
18 changes: 9 additions & 9 deletions crates/attestation/assets/tcb_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46",
"rtmr1": "b598fde9491427341bc4683b75d10d3e36770af3a36a6954d8b6b7b22aa66358f13e1f172e51b7d6e6710d99a8d8532f",
"rtmr2": "c812d42bfff1c75382e91a37c867ab117b97eb5e8d6797488928ea38e5fd38b5ed2f87d9613d392507f1c3af94657c93",
"rtmr3": "1df9222353d7c680e4692fd4e1c929cf55c3f02e0e5437c09da0ed4ec11473319cc7fba1404c3eef193aad3afbaee28b",
"rtmr3": "bfd1345bc9ecef80f380f6d619eb52893290312427237081b9765470109c597dffc98d4578588fbab210f2c97413c167",
"os_image_hash": "7d47512fda31dc5a7318f72ae1869a3c76323981eea21fc30cafd0f79668642c",
"compose_hash": "05860c32c210b09b0a3957039f926bb3efafb00e11e6ce99cd7d823488fca8ef",
"compose_hash": "6c6037dc97767a0f37f4f21521a1c28a22922ff5a4478967948a94b6c7a7b591",
"device_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1774339569\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:ro\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}",
"app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1774515652\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:f0d8146ae705dad182f7e9601e6e97215be4cf94ce80b38fddb2df654020be49\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:rw\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}",
"event_log": [
{
"imr": 0,
Expand Down Expand Up @@ -159,16 +159,16 @@
{
"imr": 3,
"event_type": 134217729,
"digest": "c24d38d25cce2d2f4def3765a97c348363f485d5c429c2c799d1596e1608a412a7edd654aeb494cecf3e37093b84bfe8",
"digest": "cdbd13eb98f8b2f83729abbd9a20cddd9053b9971802dda713c121babdb7d3dcb633f7506d8f99440f3f5d600afdf076",
"event": "app-id",
"event_payload": "05860c32c210b09b0a3957039f926bb3efafb00e"
"event_payload": "6c6037dc97767a0f37f4f21521a1c28a22922ff5"
},
{
"imr": 3,
"event_type": 134217729,
"digest": "e9a96dda81a262016807e428c4f8791681ae609c77df817f0a88fb63348174b91e2a9d09654dba5c569aa9b4623d82c8",
"digest": "e76932b68c12a11c217e235b5f16ce04eb70f0714501610986165f423b900974a05c0c8cc2d31aa6f7e9c986d945a168",
"event": "compose-hash",
"event_payload": "05860c32c210b09b0a3957039f926bb3efafb00e11e6ce99cd7d823488fca8ef"
"event_payload": "6c6037dc97767a0f37f4f21521a1c28a22922ff5a4478967948a94b6c7a7b591"
},
{
"imr": 3,
Expand Down Expand Up @@ -208,9 +208,9 @@
{
"imr": 3,
"event_type": 134217729,
"digest": "505922fbbd1de0732579ace623add381c1ef328ec64f73940ac12d3f9081163426126ba7c0a530241143bd3b8954a2d5",
"digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678",
"event": "mpc-image-digest",
"event_payload": "e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0"
"event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- /var/run/dstack.sock:/var/run/dstack.sock
- /tapp:/tapp:ro
- shared-volume:/mnt/shared:ro
- shared-volume:/mnt/shared:rw

security_opt:
- no-new-privileges:true
Expand Down
1 change: 1 addition & 0 deletions crates/mpc-attestation/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ impl Attestation {
.get_single_event(MPC_IMAGE_HASH_EVENT)?
.event_payload;

// TODO(#2478): decode raw bytes
let mpc_image_hash_bytes: Vec<u8> = hex::decode(mpc_image_hash_payload)
.map_err(|err| {
VerificationError::Custom(format!(
Expand Down
42 changes: 42 additions & 0 deletions crates/tee-launcher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "tee-launcher"
readme = "README.md"
version = { workspace = true }
license = { workspace = true }
edition = { workspace = true }

[[bin]]
name = "tee-launcher"
path = "src/main.rs"

[features]
external-services-tests = []

[dependencies]
backon = { workspace = true }
clap = { workspace = true }
dstack-sdk = { workspace = true }
launcher-interface = { workspace = true }
near-mpc-bounded-collections = { workspace = true }
# Pin reqwest 0.12 with bundled webpki-roots for reproducible builds.
# The workspace uses reqwest 0.13 which defaults to rustls-platform-verifier
# (loads CA certs from the system), but the launcher Docker image is a minimal
# container without system CA certs. Using rustls-tls bundles Mozilla's root
# certs into the binary, making TLS work without any system dependencies.
reqwest = { version = "0.12.28", default-features = false, features = ["rustls-tls", "json"] }
serde = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true, features = ["serde"] }

[dev-dependencies]
assert_matches = { workspace = true }
httpmock = { workspace = true }

[lints]
workspace = true
110 changes: 110 additions & 0 deletions crates/tee-launcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# TEE Launcher (Rust)

Secure launcher for initializing and attesting a Docker-based MPC node inside a TEE-enabled environment (e.g., Intel TDX via dstack).

Replaces the previous Python launcher (`tee_launcher/launcher.py`).

## What it does

1. Loads a TOML configuration file from `/tapp/user_config`
2. Selects an approved MPC image hash (from on-disk approved list, override, or default)
3. Validates the image by resolving it through the Docker registry and pulling by digest
4. In TEE mode: extends RTMR3 by emitting the image digest to dstack
5. Writes the MPC node config to a shared volume
6. Launches the MPC container via `docker compose up -d`

## CLI Arguments

All arguments are read from environment variables (set via docker-compose `environment`):

| Variable | Required | Description |
|----------|----------|-------------|
| `PLATFORM` | Yes | `TEE` or `NONTEE` |
| `DOCKER_CONTENT_TRUST` | Yes | Must be `1` |
| `DEFAULT_IMAGE_DIGEST` | Yes | Fallback `sha256:...` digest when the approved-hashes file is absent |

## Configuration (TOML)

The launcher reads its configuration from `/tapp/user_config` as a TOML file. This is a change from the previous Python launcher which used a `.env`-style file.

```toml
[launcher_config]
image_tags = ["latest"]
image_name = "nearone/mpc-node"
registry = "registry.hub.docker.com"
rpc_request_timeout_secs = 10
rpc_request_interval_secs = 1
rpc_max_attempts = 20
# Optional: force selection of a specific digest (must be in approved list)
# mpc_hash_override = "sha256:abcd..."
port_mappings = [
{ host = 11780, container = 11780 },
{ host = 2200, container = 2200 },
]

# Opaque MPC node configuration.
# The launcher does not interpret these fields — they are re-serialized
# to TOML and mounted into the container at /mnt/shared/mpc-config.toml
# for the MPC binary to consume via `start-with-config-file`.
[mpc_config]
# ... any fields the MPC node expects
```

### `[launcher_config]`

| Field | Default | Description |
|-------|---------|-------------|
| `image_tags` | `["latest"]` | Comma-separated Docker image tags to search |
| `image_name` | `nearone/mpc-node` | Docker image name |
| `registry` | `registry.hub.docker.com` | Docker registry hostname |
| `rpc_request_timeout_secs` | `10` | Per-request timeout for registry API calls |
| `rpc_request_interval_secs` | `1` | Initial retry interval for registry API calls |
| `rpc_max_attempts` | `20` | Maximum registry API retry attempts |
| `mpc_hash_override` | (none) | Optional: force a specific `sha256:` digest (must appear in approved list) |

| `port_mappings` | `[]` | Port mappings forwarded to the MPC container (`{ host, container }` pairs) |

### `[mpc_config]`

Arbitrary TOML table passed through to the MPC node. The launcher writes this verbatim to `/mnt/shared/mpc-config.toml`, which the container reads on startup.

## Image Hash Selection

Priority order:
1. If the approved hashes file (`/mnt/shared/image-digest.bin`) exists and `mpc_hash_override` is set: use the override (must be in the approved list)
2. If the approved hashes file exists: use the newest approved hash (first in list)
3. If the file is absent: fall back to `DEFAULT_IMAGE_DIGEST`

## File Locations

| Path | Description |
|------|-------------|
| `/tapp/user_config` | TOML configuration file |
| `/mnt/shared/image-digest.bin` | JSON file with approved image hashes (written by the MPC node) |
| `/mnt/shared/mpc-config.toml` | MPC node config (written by the launcher) |
| `/var/run/dstack.sock` | dstack unix socket (TEE mode only) |

## Key Differences from the Python Launcher

| Aspect | Python (`launcher.py`) | Rust (`tee-launcher`) |
|--------|----------------------|----------------------|
| Config format | `.env` key-value file | TOML |
| MPC node config | Environment variables passed to container | TOML file mounted into container |
| Container launch | `docker run` with flags | `docker compose up -d` with rendered template |
| RTMR3 extension | `curl` to unix socket | `dstack-sdk` native client |

## Building

```bash
cargo build -p tee-launcher --release
```

## Testing

```bash
# Unit tests
cargo nextest run -p tee-launcher

# Integration tests (requires network access and Docker Hub)
cargo nextest run -p tee-launcher --features integration-test
```
21 changes: 21 additions & 0 deletions crates/tee-launcher/mpc-node-docker-compose.tee.template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
mpc-node:
image: "{{IMAGE_NAME}}@{{IMAGE}}"
container_name: "{{CONTAINER_NAME}}"
security_opt:
- no-new-privileges:true
ports: {{PORTS}}
environment:
- "DSTACK_ENDPOINT={{DSTACK_UNIX_SOCKET}}"
volumes:
- /tapp:/tapp:ro
- shared-volume:/mnt/shared
- mpc-data:/data
- "{{DSTACK_UNIX_SOCKET}}:{{DSTACK_UNIX_SOCKET}}"
command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_SHARED_PATH}}"]

volumes:
shared-volume:
name: shared-volume
mpc-data:
name: mpc-data
Loading
Loading