From f16a2912b752b7d240c96e35179fecd3e8487b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Mon, 23 Mar 2026 13:53:06 +0100 Subject: [PATCH 1/9] feat: Port node launcher to Rust Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 54 +- .github/workflows/docker_build_launcher.yml | 27 +- Cargo.lock | 23 + Cargo.toml | 9 +- .../launcher_docker_compose.yaml.template | 2 +- crates/contract/src/lib.rs | 2 +- crates/mpc-attestation/src/attestation.rs | 1 + crates/tee-launcher/Cargo.toml | 37 + crates/tee-launcher/README.md | 110 ++ .../mpc-node-docker-compose.tee.template.yml | 21 + .../mpc-node-docker-compose.template.yml | 18 + crates/tee-launcher/src/constants.rs | 8 + crates/tee-launcher/src/docker_types.rs | 150 ++ crates/tee-launcher/src/error.rs | 88 ++ crates/tee-launcher/src/main.rs | 1278 +++++++++++++++++ crates/tee-launcher/src/types.rs | 288 ++++ crates/test-utils/assets/README.md | 5 +- crates/test-utils/assets/app_compose.json | 4 +- crates/test-utils/assets/collateral.json | 12 +- .../assets/launcher_image_compose.yaml | 6 +- crates/test-utils/assets/mpc_image_digest.txt | 2 +- .../assets/near_account_public_key.pub | 2 +- .../test-utils/assets/near_p2p_public_key.pub | 2 +- crates/test-utils/assets/public_data.json | 87 +- crates/test-utils/assets/quote.json | 2 +- crates/test-utils/assets/tcb_info.json | 18 +- crates/test-utils/src/attestation.rs | 4 +- deployment/Dockerfile-launcher | 15 +- deployment/build-images.sh | 7 +- .../cvm-deployment}/configs/kms.env | 0 .../cvm-deployment}/configs/sgx.env | 0 .../cvm-deployment}/default.env | 0 .../cvm-deployment}/deploy-launcher-guide.md | 0 .../cvm-deployment}/deploy-launcher.sh | 0 .../launcher_docker_compose.yaml | 6 +- .../launcher_docker_compose_nontee.yaml | 8 +- deployment/cvm-deployment/user-config.toml | 66 + deployment/localnet/tee/frodo.conf | 19 - deployment/localnet/tee/frodo.toml | 61 + deployment/localnet/tee/sam.conf | 19 - deployment/localnet/tee/sam.toml | 61 + deployment/testnet/frodo.conf | 19 - deployment/testnet/frodo.toml | 64 + deployment/testnet/sam.conf | 19 - deployment/testnet/sam.toml | 64 + {tee_launcher => docs}/UPDATING_LAUNCHER.md | 0 docs/localnet/mpc-config.template.toml | 1 + docs/localnet/tee-localnet.md | 16 +- .../using-the-launcher-in-nontee-setup.md | 0 flake.nix | 16 +- localnet/tee/scripts/single-node-readme.md | 23 +- .../build-and-verify-launcher-docker-image.sh | 13 +- scripts/check-kebab-case-files.sh | 1 + scripts/check-mpc-node-docker-starts.sh | 16 +- tee_launcher/__init__.py | 0 tee_launcher/launcher-test-image/Dockerfile | 3 - tee_launcher/launcher.md | 102 -- tee_launcher/launcher.py | 873 ----------- tee_launcher/requirements.txt | 2 - tee_launcher/test_launcher.py | 32 - tee_launcher/test_launcher_config.py | 844 ----------- tee_launcher/user-config.conf | 18 - 62 files changed, 2514 insertions(+), 2134 deletions(-) create mode 100644 crates/tee-launcher/Cargo.toml create mode 100644 crates/tee-launcher/README.md create mode 100644 crates/tee-launcher/mpc-node-docker-compose.tee.template.yml create mode 100644 crates/tee-launcher/mpc-node-docker-compose.template.yml create mode 100644 crates/tee-launcher/src/constants.rs create mode 100644 crates/tee-launcher/src/docker_types.rs create mode 100644 crates/tee-launcher/src/error.rs create mode 100644 crates/tee-launcher/src/main.rs create mode 100644 crates/tee-launcher/src/types.rs rename {tee_launcher => deployment/cvm-deployment}/configs/kms.env (100%) rename {tee_launcher => deployment/cvm-deployment}/configs/sgx.env (100%) rename {tee_launcher => deployment/cvm-deployment}/default.env (100%) rename {tee_launcher => deployment/cvm-deployment}/deploy-launcher-guide.md (100%) rename {tee_launcher => deployment/cvm-deployment}/deploy-launcher.sh (100%) rename {tee_launcher => deployment/cvm-deployment}/launcher_docker_compose.yaml (63%) rename {tee_launcher => deployment/cvm-deployment}/launcher_docker_compose_nontee.yaml (57%) create mode 100644 deployment/cvm-deployment/user-config.toml delete mode 100644 deployment/localnet/tee/frodo.conf create mode 100644 deployment/localnet/tee/frodo.toml delete mode 100644 deployment/localnet/tee/sam.conf create mode 100644 deployment/localnet/tee/sam.toml delete mode 100644 deployment/testnet/frodo.conf create mode 100644 deployment/testnet/frodo.toml delete mode 100644 deployment/testnet/sam.conf create mode 100644 deployment/testnet/sam.toml rename {tee_launcher => docs}/UPDATING_LAUNCHER.md (100%) rename {tee_launcher => docs}/using-the-launcher-in-nontee-setup.md (100%) delete mode 100644 tee_launcher/__init__.py delete mode 100644 tee_launcher/launcher-test-image/Dockerfile delete mode 100644 tee_launcher/launcher.md delete mode 100644 tee_launcher/launcher.py delete mode 100644 tee_launcher/requirements.txt delete mode 100644 tee_launcher/test_launcher.py delete mode 100644 tee_launcher/test_launcher_config.py delete mode 100644 tee_launcher/user-config.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a180e6590..af272284c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 @@ -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 @@ -445,37 +460,6 @@ jobs: target/wasm32-unknown-unknown/release-contract/test_parallel_contract.wasm libs/nearcore/target/release/neard - tee-launcher-tests: - name: "TEE Launcher: pytests" - runs-on: warp-ubuntu-2404-x64-2x - timeout-minutes: 60 - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - persist-credentials: false - - - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.11" - - - name: Setup virtualenv - run: | - python3 -m venv tee_launcher/venv - source tee_launcher/venv/bin/activate - cd tee_launcher - pip install -r requirements.txt - - - name: Run pytest - run: | - source tee_launcher/venv/bin/activate - cd tee_launcher - PYTHONPATH=. pytest -vsx - fast-ci-checks: name: "Fast CI checks" runs-on: warp-ubuntu-2404-x64-8x diff --git a/.github/workflows/docker_build_launcher.yml b/.github/workflows/docker_build_launcher.yml index 2cab91641..3b8e80da8 100644 --- a/.github/workflows/docker_build_launcher.yml +++ b/.github/workflows/docker_build_launcher.yml @@ -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 @@ -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 diff --git a/Cargo.lock b/Cargo.lock index f8d772e98..11a2a9eb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10683,6 +10683,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "tee-launcher" +version = "3.7.0" +dependencies = [ + "assert_matches", + "backon", + "clap", + "dstack-sdk", + "httpmock", + "launcher-interface", + "near-mpc-bounded-collections", + "reqwest 0.12.28", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", + "toml 1.0.6+spec-1.1.0", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "tempfile" version = "3.26.0" diff --git a/Cargo.toml b/Cargo.toml index a1a6e2962..8fce20a30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "crates/primitives", "crates/tee-authority", "crates/tee-context", + "crates/tee-launcher", "crates/test-migration-contract", "crates/test-parallel-contract", "crates/test-utils", @@ -169,7 +170,13 @@ reddsa = { git = "https://github.com/near/reddsa", rev = "c7cd92a55f7399d8d7f8c0 ] } regex = "1.12.3" # TODO(#2053): upgrading this leads to errors -reqwest = { version = "0.12.28", features = ["multipart", "json"] } +reqwest = { version = "0.12.28", default-features = false, features = [ + "charset", + "http2", + "rustls-tls", + "multipart", + "json", +] } rmp-serde = "1.3.1" rstest = { version = "0.26.1" } rustls = { version = "0.23.37", default-features = false, features = ["std"] } diff --git a/crates/contract/assets/launcher_docker_compose.yaml.template b/crates/contract/assets/launcher_docker_compose.yaml.template index 612afb397..bcf258c4f 100644 --- a/crates/contract/assets/launcher_docker_compose.yaml.template +++ b/crates/contract/assets/launcher_docker_compose.yaml.template @@ -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 diff --git a/crates/contract/src/lib.rs b/crates/contract/src/lib.rs index 0fc427ae2..57c04f076 100644 --- a/crates/contract/src/lib.rs +++ b/crates/contract/src/lib.rs @@ -4561,7 +4561,7 @@ mod tests { block_timestamp_ns: u64, ) { let launcher_hash_bytes: [u8; 32] = - hex::decode("e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701") + hex::decode("02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316") .unwrap() .try_into() .unwrap(); diff --git a/crates/mpc-attestation/src/attestation.rs b/crates/mpc-attestation/src/attestation.rs index c469ecd00..74ec7f916 100644 --- a/crates/mpc-attestation/src/attestation.rs +++ b/crates/mpc-attestation/src/attestation.rs @@ -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 = hex::decode(mpc_image_hash_payload) .map_err(|err| { VerificationError::Custom(format!( diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml new file mode 100644 index 000000000..23ee283af --- /dev/null +++ b/crates/tee-launcher/Cargo.toml @@ -0,0 +1,37 @@ +[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 } +reqwest = { workspace = true } +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 diff --git a/crates/tee-launcher/README.md b/crates/tee-launcher/README.md new file mode 100644 index 000000000..425d1fd0d --- /dev/null +++ b/crates/tee-launcher/README.md @@ -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 +``` diff --git a/crates/tee-launcher/mpc-node-docker-compose.tee.template.yml b/crates/tee-launcher/mpc-node-docker-compose.tee.template.yml new file mode 100644 index 000000000..ef3c06fb4 --- /dev/null +++ b/crates/tee-launcher/mpc-node-docker-compose.tee.template.yml @@ -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 diff --git a/crates/tee-launcher/mpc-node-docker-compose.template.yml b/crates/tee-launcher/mpc-node-docker-compose.template.yml new file mode 100644 index 000000000..b6e3e5979 --- /dev/null +++ b/crates/tee-launcher/mpc-node-docker-compose.template.yml @@ -0,0 +1,18 @@ +services: + mpc-node: + image: "{{IMAGE_NAME}}@{{IMAGE}}" + container_name: "{{CONTAINER_NAME}}" + security_opt: + - no-new-privileges:true + ports: {{PORTS}} + volumes: + - /tapp:/tapp:ro + - shared-volume:/mnt/shared + - mpc-data:/data + command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_SHARED_PATH}}"] + +volumes: + shared-volume: + name: shared-volume + mpc-data: + name: mpc-data diff --git a/crates/tee-launcher/src/constants.rs b/crates/tee-launcher/src/constants.rs new file mode 100644 index 000000000..6a6b7623b --- /dev/null +++ b/crates/tee-launcher/src/constants.rs @@ -0,0 +1,8 @@ +pub(crate) const MPC_CONTAINER_NAME: &str = "mpc-node"; +pub(crate) const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; +pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; +pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; + +/// Path on the shared volume where the launcher writes the MPC config and the +/// MPC container reads it. Both containers mount `shared-volume` at `/mnt/shared`. +pub(crate) const MPC_CONFIG_SHARED_PATH: &str = "/mnt/shared/mpc-config.toml"; diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs new file mode 100644 index 000000000..ac9321b3e --- /dev/null +++ b/crates/tee-launcher/src/docker_types.rs @@ -0,0 +1,150 @@ +use launcher_interface::types::DockerSha256Digest; +use serde::{Deserialize, Serialize}; + +/// Partial response +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct DockerTokenResponse { + pub(crate) token: String, +} + +/// Response from `GET /v2/{name}/manifests/{reference}`. +/// +/// The `mediaType` field determines the variant: +/// - OCI image index → multi-platform manifest with a list of platform entries +/// - Docker V2 / OCI manifest → single-platform manifest with a config digest +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "mediaType")] +pub(crate) enum ManifestResponse { + /// Multi-platform manifest (OCI image index). + #[serde(rename = "application/vnd.oci.image.index.v1+json")] + ImageIndex { manifests: Vec }, + + /// Single-platform Docker V2 manifest. + #[serde(rename = "application/vnd.docker.distribution.manifest.v2+json")] + DockerV2 { config: ManifestConfig }, + + /// Single-platform OCI manifest. + #[serde(rename = "application/vnd.oci.image.manifest.v1+json")] + OciManifest { config: ManifestConfig }, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct ManifestEntry { + pub(crate) digest: String, + pub(crate) platform: ManifestPlatform, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +pub(crate) struct ManifestPlatform { + pub(crate) architecture: String, + pub(crate) os: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) struct ManifestConfig { + pub(crate) digest: DockerSha256Digest, +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + + fn sample_digest_str() -> String { + format!("sha256:{}", "ab".repeat(32)) + } + + #[test] + fn image_index_deserializes() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "digest": "sha256:abc123", + "platform": { "architecture": "amd64", "os": "linux" } + }, + { + "digest": "sha256:def456", + "platform": { "architecture": "arm64", "os": "linux" } + } + ] + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(ManifestResponse::ImageIndex { manifests }) => { + assert_eq!(manifests.len(), 2); + assert_eq!(manifests[0].platform, ManifestPlatform { + architecture: "amd64".into(), + os: "linux".into(), + }); + }); + } + + #[test] + fn docker_v2_manifest_deserializes() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": sample_digest_str() } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(ManifestResponse::DockerV2 { config }) => { + assert_eq!(config.digest.to_string(), sample_digest_str()); + }); + } + + #[test] + fn oci_manifest_deserializes() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { "digest": sample_digest_str() } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(ManifestResponse::OciManifest { config }) => { + assert_eq!(config.digest.to_string(), sample_digest_str()); + }); + } + + #[test] + fn unknown_media_type_is_rejected() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.unknown.format", + "config": { "digest": sample_digest_str() } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn docker_token_response_deserializes() { + // given + let json = serde_json::json!({ "token": "abc.def.ghi" }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(resp) => { + assert_eq!(resp.token, "abc.def.ghi"); + }); + } +} diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs new file mode 100644 index 000000000..0b3283e71 --- /dev/null +++ b/crates/tee-launcher/src/error.rs @@ -0,0 +1,88 @@ +use launcher_interface::types::DockerSha256Digest; +use thiserror::Error; + +#[derive(Error, Debug)] +pub(crate) enum LauncherError { + #[error("EmitEvent failed while extending RTMR3: {0}")] + DstackEmitEventFailed(String), + + #[error("MPC_HASH_OVERRIDE invalid: {0}")] + InvalidHashOverride(String), + + #[error("Image hash not found among tags")] + ImageHashNotFoundAmongTags, + + #[error("Failed to get auth token from registry: {0}")] + RegistryAuthFailed(String), + + #[error("docker run failed for validated hash")] + DockerRunFailed { + image_hash: DockerSha256Digest, + inner: std::io::Error, + }, + + #[error("docker run failed for validated hash")] + DockerRunFailedExitStatus { + image_hash: DockerSha256Digest, + output: String, + }, + + #[error("Failed to read {path}: {source}")] + FileRead { + path: String, + source: std::io::Error, + }, + + #[error("Failed to write {path}: {source}")] + FileWrite { + path: String, + source: std::io::Error, + }, + + #[error("Failed to create temp file: {0}")] + TempFileCreate(std::io::Error), + + #[error("Failed to parse {path}: {source}")] + JsonParse { + path: String, + source: serde_json::Error, + }, + + #[error("Failed to parse {path}: {source}")] + TomlParse { + path: String, + source: toml::de::Error, + }, + + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("Registry response parse error: {0}")] + RegistryResponseParse(String), + + #[error("Invalid manifest URL: {0}")] + InvalidManifestUrl(String), + + #[error("User config contains reserved key [{0}] — remove it from mpc_node_config")] + ReservedConfigKey(String), + + #[error("The selected image failed digest validation: {0}")] + ImageDigestValidationFailed(#[from] ImageDigestValidationFailed), +} + +#[derive(Error, Debug)] +pub(crate) enum ImageDigestValidationFailed { + #[error("manifest digest lookup failed: {0}")] + ManifestDigestLookupFailed(String), + #[error("docker pull failed for {0}")] + DockerPullFailed(String), + #[error("docker inspect failed for {0}")] + DockerInspectFailed(String), + #[error( + "pulled image has mismatching image ID. pulled: {pulled_image_id}, expected: {expected_image_id}" + )] + PulledImageHasMismatchedDigest { + expected_image_id: DockerSha256Digest, + pulled_image_id: DockerSha256Digest, + }, +} diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs new file mode 100644 index 000000000..dff322660 --- /dev/null +++ b/crates/tee-launcher/src/main.rs @@ -0,0 +1,1278 @@ +use std::io::Write; +use std::process::Command; +use std::{collections::VecDeque, time::Duration}; + +use backon::{ExponentialBuilder, Retryable}; +use clap::Parser; +use launcher_interface::types::{ + ApprovedHashes, DockerSha256Digest, TeeAuthorityConfig, TeeConfig, +}; +use launcher_interface::{DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, MPC_IMAGE_HASH_EVENT}; + +use constants::*; +use docker_types::*; +use error::*; +use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue}; + +use types::*; +use url::Url; + +mod constants; +mod docker_types; +mod error; +mod types; + +const COMPOSE_TEMPLATE: &str = include_str!("../mpc-node-docker-compose.template.yml"); +const COMPOSE_TEE_TEMPLATE: &str = include_str!("../mpc-node-docker-compose.tee.template.yml"); + +const DOCKER_AUTH_ACCEPT_HEADER_VALUE: HeaderValue = + HeaderValue::from_static("application/vnd.docker.distribution.manifest.v2+json"); + +const DOCKER_CONTENT_DIGEST_HEADER: &str = "Docker-Content-Digest"; + +const AMD64: &str = "amd64"; +const LINUX: &str = "linux"; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + if let Err(e) = run().await { + tracing::error!("Error: {e}"); + std::process::exit(1); + } +} + +async fn run() -> Result<(), LauncherError> { + tracing::info!("start"); + + let args = CliArgs::parse(); + + tracing::info!(platform = ?args.platform, "starting launcher"); + + // Load dstack user config (TOML) + let config_contents = std::fs::read_to_string(DSTACK_USER_CONFIG_FILE).map_err(|source| { + LauncherError::FileRead { + path: DSTACK_USER_CONFIG_FILE.to_string(), + source, + } + })?; + + let config: Config = + toml::from_str(&config_contents).map_err(|source| LauncherError::TomlParse { + path: DSTACK_USER_CONFIG_FILE.to_string(), + source, + })?; + + let approved_hashes_file = std::fs::OpenOptions::new() + .read(true) + .write(false) + .open(IMAGE_DIGEST_FILE) + .map_err(|source| LauncherError::FileRead { + path: IMAGE_DIGEST_FILE.to_string(), + source, + }); + + let approved_hashes_on_disk: Option = match approved_hashes_file { + Err(err) => { + tracing::warn!( + ?err, + default_image_digest = ?args.default_image_digest, + "approved hashes file does not exist on disk, falling back to default digest" + ); + None + } + Ok(file) => { + let parsed: ApprovedHashes = + serde_json::from_reader(file).map_err(|source| LauncherError::JsonParse { + path: IMAGE_DIGEST_FILE.to_string(), + source, + })?; + Some(parsed) + } + }; + + let image_hash = select_image_hash( + approved_hashes_on_disk.as_ref(), + &args.default_image_digest, + config.launcher_config.mpc_hash_override.as_ref(), + )?; + + let manifest_digest = validate_image_hash(&config.launcher_config, image_hash.clone()).await?; + + let should_extend_rtmr_3 = args.platform == Platform::Tee; + + if should_extend_rtmr_3 { + let dstack_client = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); + + // EmitEvent with the image digest + dstack_client + .emit_event( + MPC_IMAGE_HASH_EVENT.to_string(), + image_hash.as_ref().to_vec(), + ) + .await + .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; + } + + let mpc_binary_config_path = std::path::Path::new(MPC_CONFIG_SHARED_PATH); + + let tee_authority_config = match args.platform { + Platform::Tee => TeeAuthorityConfig::Dstack { + dstack_endpoint: DSTACK_UNIX_SOCKET.to_string(), + quote_upload_url: DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL.to_string(), + }, + Platform::NonTee => TeeAuthorityConfig::Local, + }; + + let tee_config = TeeConfig { + authority: tee_authority_config, + image_hash: image_hash.clone(), + latest_allowed_hash_file_path: IMAGE_DIGEST_FILE + .parse() + .expect("image digest file has a valid path"), + }; + + let mpc_node_config = intercept_node_config(config.mpc_node_config, &tee_config)?; + + let mpc_config_toml = + toml::to_string(&mpc_node_config).expect("re-serializing a toml::Table always succeeds"); + + std::fs::write(mpc_binary_config_path, mpc_config_toml.as_bytes()).map_err(|source| { + LauncherError::FileWrite { + path: mpc_binary_config_path.display().to_string(), + source, + } + })?; + + launch_mpc_container( + args.platform, + &manifest_digest, + &config.launcher_config.image_name, + &config.launcher_config.port_mappings, + )?; + + Ok(()) +} + +/// Inject launcher-controlled config section (`tee`) into the user-provided +/// MPC node config table. Returns an error if the user config already +/// contains the reserved key. +fn intercept_node_config( + mut node_config: toml::Table, + tee_config: &TeeConfig, +) -> Result { + insert_reserved( + &mut node_config, + "tee", + toml::Value::try_from(tee_config).expect("tee config serializes to TOML"), + )?; + Ok(node_config) +} + +/// Inject launcher-controlled config section (`tee`) into the user-provided +/// MPC node config table. Returns an error if the user config already +/// contains the reserved key. +/// Insert `value` under `key` in `table`, returning an error if the key +/// already exists. +fn insert_reserved( + table: &mut toml::Table, + key: &str, + value: toml::Value, +) -> Result<(), LauncherError> { + match table.entry(key) { + toml::map::Entry::Vacant(vacant) => { + vacant.insert(value); + Ok(()) + } + toml::map::Entry::Occupied(_) => Err(LauncherError::ReservedConfigKey(key.to_string())), + } +} + +/// Select which image hash to use, given the approved hashes file (if present), +/// a fallback default digest, and an optional user override. +/// +/// Selection rules: +/// - If the approved hashes file is absent → use `default_digest` +/// - If `override_hash` is set and appears in the approved list → use it +/// - If `override_hash` is set but NOT in the approved list → error +/// - Otherwise → use the newest approved hash (first in the list) +fn select_image_hash( + approved_hashes: Option<&ApprovedHashes>, + default_digest: &DockerSha256Digest, + override_hash: Option<&DockerSha256Digest>, +) -> Result { + let Some(approved) = approved_hashes else { + tracing::info!("no approved hashes file, using default digest"); + return Ok(default_digest.clone()); + }; + + if let Some(override_image) = override_hash { + tracing::info!(?override_image, "override mpc image hash provided"); + if !approved.approved_hashes.contains(override_image) { + return Err(LauncherError::InvalidHashOverride(format!( + "MPC_HASH_OVERRIDE={override_image} does not match any approved hash", + ))); + } + return Ok(override_image.clone()); + } + + let selected = approved.newest_approved_hash().clone(); + tracing::info!(?selected, "selected newest approved hash"); + Ok(selected) +} + +/// Provides the URLs needed to interact with a container registry. +trait RegistryInfo { + fn token_url(&self) -> String; + fn manifest_url(&self, tag: &str) -> Result; +} + +/// Production registry info for Docker Hub. +struct DockerRegistry { + registry_base_url: String, + image_name: String, +} + +impl DockerRegistry { + fn new(config: &LauncherConfig) -> Self { + Self { + registry_base_url: format!("https://{}", config.registry), + image_name: config.image_name.clone(), + } + } +} + +impl RegistryInfo for DockerRegistry { + // TODO(#2479): if we use a different registry, we need a different auth-endpoint + fn token_url(&self) -> String { + format!( + "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", + self.image_name, + ) + } + + fn manifest_url(&self, tag: &str) -> Result { + let url_string = format!( + "{}/v2/{}/manifests/{tag}", + self.registry_base_url, self.image_name + ); + + url_string + .parse() + .map_err(|_| LauncherError::InvalidManifestUrl(url_string)) + } +} + +async fn get_manifest_digest( + registry: &dyn RegistryInfo, + config: &LauncherConfig, + expected_image_digest: &DockerSha256Digest, +) -> Result { + let mut tags: VecDeque = config.image_tags.iter().cloned().collect(); + + let reqwest_client = reqwest::Client::new(); + + // We need an authorization token to fetch manifests. + let token_url = registry.token_url(); + + let token_request_response = reqwest_client + .get(token_url) + .send() + .await + .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; + + let status = token_request_response.status(); + if !status.is_success() { + return Err(LauncherError::RegistryAuthFailed(format!( + "token request returned non-success status: {status}" + ))); + } + + let token_response: DockerTokenResponse = token_request_response + .json() + .await + .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; + + while let Some(tag) = tags.pop_front() { + let manifest_url = registry.manifest_url(&tag)?; + + let authorization_value: HeaderValue = format!("Bearer {}", token_response.token) + .parse() + .expect("bearer token received from docker auth is a valid header value"); + + let headers = HeaderMap::from_iter([ + (ACCEPT, DOCKER_AUTH_ACCEPT_HEADER_VALUE), + (AUTHORIZATION, authorization_value), + ]); + + let request_timeout = Duration::from_secs(config.rpc_request_timeout_secs); + let backoff = ExponentialBuilder::default() + .with_min_delay(Duration::from_secs(config.rpc_request_interval_secs)) + .with_factor(1.5) + .with_max_delay(Duration::from_secs(60)) + .with_max_times(config.rpc_max_attempts as usize); + + let request_future = || async { + reqwest_client + .get(manifest_url.clone()) + .headers(headers.clone()) + .timeout(request_timeout) + .send() + .await? + .error_for_status() + }; + + let request_with_retry_future = request_future + .retry(backoff) + .when(|_: &reqwest::Error| true) + .notify(|err, dur| { + tracing::warn!( + ?manifest_url, + ?dur, + ?err, + "failed to fetch manifest, retrying" + ); + }); + + let Ok(resp) = request_with_retry_future.await else { + tracing::warn!( + ?manifest_url, + "exceeded max RPC attempts. \ + Will continue in the hopes of finding the matching image hash among remaining tags" + ); + continue; + }; + + let response_headers = resp.headers().clone(); + let manifest: ManifestResponse = resp + .json() + .await + .map_err(|e| LauncherError::RegistryResponseParse(e.to_string()))?; + + match manifest { + ManifestResponse::ImageIndex { manifests } => { + let platform_digests: Vec<_> = manifests + .iter() + .filter(|m| m.platform.architecture == AMD64 && m.platform.os == LINUX) + .map(|m| m.digest.as_str()) + .collect(); + tracing::info!( + ?tag, + ?platform_digests, + "received multi-platform image index, queuing amd64/linux manifests" + ); + manifests + .into_iter() + .filter(|manifest| { + manifest.platform.architecture == AMD64 && manifest.platform.os == LINUX + }) + .for_each(|manifest| tags.push_back(manifest.digest)); + } + ManifestResponse::DockerV2 { config } | ManifestResponse::OciManifest { config } => { + if config.digest != *expected_image_digest { + tracing::warn!( + ?tag, + actual_config_digest = %config.digest, + expected_config_digest = %expected_image_digest, + "config digest mismatch, skipping tag" + ); + continue; + } + + let Some(content_digest) = response_headers + .get(DOCKER_CONTENT_DIGEST_HEADER) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + else { + tracing::warn!( + ?tag, + "manifest matched but Docker-Content-Digest header missing, skipping" + ); + continue; + }; + + tracing::info!( + ?tag, + %content_digest, + "config digest matched, resolved manifest digest" + ); + return content_digest.parse().map_err(|_| { + LauncherError::RegistryResponseParse(format!( + "failed to parse manifest digest: {}", + content_digest + )) + }); + } + } + } + + tracing::error!( + ?expected_image_digest, + tags = ?config.image_tags, + "no tag produced a manifest with matching config digest" + ); + Err(LauncherError::ImageHashNotFoundAmongTags) +} + +/// Returns if the given image digest is valid (pull + manifest + digest match). +/// Does NOT extend RTMR3 and does NOT run the container. +async fn validate_image_hash( + launcher_config: &LauncherConfig, + image_hash: DockerSha256Digest, +) -> Result { + let registry = DockerRegistry::new(launcher_config); + let manifest_digest = get_manifest_digest(®istry, launcher_config, &image_hash) + .await + .map_err(|e| ImageDigestValidationFailed::ManifestDigestLookupFailed(e.to_string()))?; + let image_name = &launcher_config.image_name; + + let name_and_digest = format!("{image_name}@{manifest_digest}"); + + // Pull + let pull = Command::new("docker") + .args(["pull", &name_and_digest]) + .output() + .map_err(|e| ImageDigestValidationFailed::DockerPullFailed(e.to_string()))?; + + let pull_failed = !pull.status.success(); + if pull_failed { + return Err(ImageDigestValidationFailed::DockerPullFailed( + "docker pull terminated with unsuccessful status".to_string(), + )); + } + + // Verify that the pulled image ID matches the expected config digest. + // `docker inspect .ID` returns the image ID, which equals the config digest + // (i.e. the sha256 of the image config blob). + let inspect = Command::new("docker") + .args([ + "image", + "inspect", + "--format", + "{{index .ID}}", + &name_and_digest, + ]) + .output() + .map_err(|e| ImageDigestValidationFailed::DockerInspectFailed(e.to_string()))?; + + let docker_inspect_failed = !inspect.status.success(); + if docker_inspect_failed { + return Err(ImageDigestValidationFailed::DockerInspectFailed( + "docker inspect terminated with unsuccessful status".to_string(), + )); + } + + let pulled_image_id: DockerSha256Digest = String::from_utf8_lossy(&inspect.stdout) + .trim() + .to_string() + .parse() + .expect("is valid digest"); + + if pulled_image_id != image_hash { + return Err( + ImageDigestValidationFailed::PulledImageHasMismatchedDigest { + pulled_image_id, + expected_image_id: image_hash, + }, + ); + } + + Ok(manifest_digest) +} + +fn render_compose_file( + platform: Platform, + port_mappings: &[PortMapping], + image_name: &str, + manifest_digest: &DockerSha256Digest, +) -> Result { + let template = match platform { + Platform::Tee => COMPOSE_TEE_TEMPLATE, + Platform::NonTee => COMPOSE_TEMPLATE, + }; + + let ports: Vec = port_mappings + .iter() + .map(PortMapping::docker_compose_value) + .collect(); + let ports_json = serde_json::to_string(&ports).expect("port list is serializable"); + + let rendered = template + .replace("{{IMAGE_NAME}}", image_name) + .replace("{{IMAGE}}", &manifest_digest.to_string()) + .replace("{{CONTAINER_NAME}}", MPC_CONTAINER_NAME) + .replace("{{MPC_CONFIG_SHARED_PATH}}", MPC_CONFIG_SHARED_PATH) + .replace("{{DSTACK_UNIX_SOCKET}}", DSTACK_UNIX_SOCKET) + .replace("{{PORTS}}", &ports_json); + + tracing::info!(compose = %rendered, "rendered docker-compose file"); + + let mut file = tempfile::NamedTempFile::new().map_err(LauncherError::TempFileCreate)?; + file.write_all(rendered.as_bytes()) + .map_err(|source| LauncherError::FileWrite { + path: file.path().display().to_string(), + source, + })?; + + Ok(file) +} + +fn launch_mpc_container( + platform: Platform, + manifest_digest: &DockerSha256Digest, + image_name: &str, + port_mappings: &[PortMapping], +) -> Result<(), LauncherError> { + tracing::info!(?manifest_digest, "launching MPC node"); + + let compose_file = render_compose_file(platform, port_mappings, image_name, manifest_digest)?; + let compose_path = compose_file.path().display().to_string(); + + // Remove any existing container from a previous run (by name, independent of compose file) + let _ = Command::new("docker") + .args(["rm", "-f", MPC_CONTAINER_NAME]) + .output(); + + let run_output = Command::new("docker") + .args(["compose", "-f", &compose_path, "up", "-d"]) + .output() + .map_err(|inner| LauncherError::DockerRunFailed { + image_hash: manifest_digest.clone(), + inner, + })?; + + if !run_output.status.success() { + let stderr = String::from_utf8_lossy(&run_output.stderr); + let stdout = String::from_utf8_lossy(&run_output.stdout); + tracing::error!(%stderr, %stdout, "docker compose up failed"); + return Err(LauncherError::DockerRunFailedExitStatus { + image_hash: manifest_digest.clone(), + output: stderr.into_owned(), + }); + } + + tracing::info!("MPC launched successfully."); + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU16; + + use crate::{ + RegistryInfo, constants::*, error::LauncherError, get_manifest_digest, + intercept_node_config, render_compose_file, select_image_hash, types::*, + }; + + use assert_matches::assert_matches; + use httpmock::prelude::*; + use launcher_interface::types::{ + ApprovedHashes, DockerSha256Digest, TeeAuthorityConfig, TeeConfig, + }; + use near_mpc_bounded_collections::NonEmptyVec; + + const SAMPLE_IMAGE_NAME: &str = "nearone/mpc-node"; + + fn render( + platform: Platform, + port_mappings: &[PortMapping], + digest: &DockerSha256Digest, + ) -> String { + let file = render_compose_file(platform, port_mappings, SAMPLE_IMAGE_NAME, digest).unwrap(); + std::fs::read_to_string(file.path()).unwrap() + } + + fn digest(hex_char: char) -> DockerSha256Digest { + format!( + "sha256:{}", + std::iter::repeat_n(hex_char, 64).collect::() + ) + .parse() + .unwrap() + } + + fn sample_digest() -> DockerSha256Digest { + digest('a') + } + + fn approved_file(hashes: Vec) -> ApprovedHashes { + ApprovedHashes { + approved_hashes: NonEmptyVec::from_vec(hashes).unwrap(), + } + } + + struct MockRegistry { + base_url: String, + image_name: String, + } + + impl RegistryInfo for MockRegistry { + fn token_url(&self) -> String { + format!("{}/token", self.base_url) + } + + fn manifest_url(&self, tag: &str) -> Result { + let raw = format!("{}/v2/{}/manifests/{tag}", self.base_url, self.image_name); + raw.parse() + .map_err(|_| crate::error::LauncherError::InvalidManifestUrl(raw)) + } + } + + fn mock_launcher_config(tag: &str) -> LauncherConfig { + LauncherConfig { + image_tags: near_mpc_bounded_collections::NonEmptyVec::from_vec(vec![tag.into()]) + .unwrap(), + image_name: "test/image".into(), + registry: "unused".into(), + rpc_request_timeout_secs: 5, + rpc_request_interval_secs: 1, + rpc_max_attempts: 1, + mpc_hash_override: None, + port_mappings: vec![], + } + } + + fn mock_registry(server: &MockServer) -> MockRegistry { + MockRegistry { + base_url: server.base_url(), + image_name: "test/image".into(), + } + } + + fn empty_port_mappings() -> Vec { + vec![] + } + + fn port_mappings_with_port() -> Vec { + vec![PortMapping { + host: NonZeroU16::new(11780).unwrap(), + container: NonZeroU16::new(11780).unwrap(), + }] + } + + #[test] + fn tee_mode_includes_dstack_env_and_volume() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::Tee, &port_mappings, &digest); + + // then + assert!(rendered.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); + assert!(rendered.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); + } + + #[test] + fn nontee_mode_excludes_dstack() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then + assert!(!rendered.contains("DSTACK_ENDPOINT")); + assert!(!rendered.contains(DSTACK_UNIX_SOCKET)); + } + + #[test] + fn includes_security_opts_and_required_volumes() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then + assert!(rendered.contains("no-new-privileges:true")); + assert!(rendered.contains("/tapp:/tapp:ro")); + assert!(rendered.contains("shared-volume:/mnt/shared")); + assert!(rendered.contains("mpc-data:/data")); + assert!(rendered.contains(&format!("container_name: \"{MPC_CONTAINER_NAME}\""))); + } + + #[test] + fn mounts_config_file_read_only() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then — config is on the shared volume, referenced in the command + assert!(rendered.contains(MPC_CONFIG_SHARED_PATH)); + } + + #[test] + fn includes_start_with_config_file_command() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then + assert!(rendered.contains("/app/mpc-node")); + assert!(rendered.contains(MPC_CONFIG_SHARED_PATH)); + } + + #[test] + fn image_is_set() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then + assert!(rendered.contains(&format!("image: \"{SAMPLE_IMAGE_NAME}@{digest}\""))); + } + + #[test] + fn includes_ports() { + // given + let port_mappings = port_mappings_with_port(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then + assert!(rendered.contains("11780:11780")); + } + + #[test] + fn no_env_section_in_nontee_mode() { + // given + let port_mappings = empty_port_mappings(); + let digest = sample_digest(); + + // when + let rendered = render(Platform::NonTee, &port_mappings, &digest); + + // then + assert!(!rendered.contains("environment:")); + } + + fn sample_tee_config() -> TeeConfig { + TeeConfig { + authority: TeeAuthorityConfig::Dstack { + dstack_endpoint: "/var/run/dstack.sock".to_string(), + quote_upload_url: "https://example.com/quote".to_string(), + }, + image_hash: sample_digest(), + latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), + } + } + + #[test] + fn intercept_config_injects_tee_config() { + // given + let config: toml::Table = toml::from_str(r#"home_dir = "/data""#).unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); + + // then + assert!(result.contains_key("tee")); + assert_eq!(result["home_dir"].as_str(), Some("/data")); + } + + #[test] + fn intercept_config_rejects_user_provided_tee_key() { + // given + let config: toml::Table = toml::from_str( + r#"[tee] +type = "Local" +"#, + ) + .unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config()); + + // then + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "tee"); + }); + } + + #[test] + fn intercept_config_empty_table_gets_tee_key() { + // given + let config = toml::Table::new(); + + // when + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); + + // then + assert!(result.contains_key("tee")); + assert_eq!(result.len(), 1); + } + + #[test] + fn intercept_config_preserves_all_existing_keys() { + // given + let config: toml::Table = toml::from_str( + r#" +home_dir = "/data" +port = 8080 +[nested] +key = "value" +"#, + ) + .unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); + + // then + assert_eq!(result["home_dir"].as_str(), Some("/data")); + assert_eq!(result["port"].as_integer(), Some(8080)); + assert_eq!(result["nested"]["key"].as_str(), Some("value")); + assert!(result.contains_key("tee")); + } + + #[test] + fn intercept_config_dstack_tee_config_serializes_correctly() { + // given + let config = toml::Table::new(); + let tee = TeeConfig { + authority: TeeAuthorityConfig::Dstack { + dstack_endpoint: "/my/socket".to_string(), + quote_upload_url: "https://example.com".to_string(), + }, + image_hash: sample_digest(), + latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), + }; + + // when + let result = intercept_node_config(config, &tee).unwrap(); + + // then + let tee_table = result["tee"].as_table().unwrap(); + let authority = tee_table["authority"].as_table().unwrap(); + assert_eq!(authority["dstack_endpoint"].as_str(), Some("/my/socket")); + assert_eq!( + authority["quote_upload_url"].as_str(), + Some("https://example.com") + ); + } + + #[test] + fn intercept_config_local_tee_config_serializes_correctly() { + // given + let config = toml::Table::new(); + let tee = TeeConfig { + authority: TeeAuthorityConfig::Local, + image_hash: sample_digest(), + latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), + }; + + // when + let result = intercept_node_config(config, &tee).unwrap(); + + // then — Local variant is a unit variant; just verify the key exists + assert!(result.contains_key("tee")); + // re-serialize the whole thing to verify it round-trips + let toml_str = toml::to_string(&result).unwrap(); + assert!(toml_str.contains("tee")); + } + + #[test] + fn intercept_config_image_config_contains_expected_fields() { + // given + let config = toml::Table::new(); + let tee = TeeConfig { + authority: TeeAuthorityConfig::Dstack { + dstack_endpoint: "/var/run/dstack.sock".to_string(), + quote_upload_url: "https://example.com/quote".to_string(), + }, + image_hash: digest('b'), + latest_allowed_hash_file_path: "/some/path".into(), + }; + + // when + let result = intercept_node_config(config, &tee).unwrap(); + + // then + let tee_table = result["tee"].as_table().unwrap(); + assert!(tee_table["image_hash"].as_str().unwrap().contains("bbbb")); + assert_eq!( + tee_table["latest_allowed_hash_file_path"].as_str(), + Some("/some/path") + ); + } + + #[test] + fn intercept_config_output_re_serializes_to_valid_toml() { + // given + let config: toml::Table = toml::from_str(r#"home_dir = "/data""#).unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); + let toml_str = toml::to_string(&result).unwrap(); + + // then — the output can be parsed back + let reparsed: toml::Table = toml::from_str(&toml_str).unwrap(); + assert!(reparsed.contains_key("tee")); + assert_eq!(reparsed["home_dir"].as_str(), Some("/data")); + } + + #[test] + fn intercept_config_tee_as_non_table_value_is_rejected() { + // given — tee exists but as a string, not a table + let config: toml::Table = toml::from_str(r#"tee = "sneaky""#).unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config()); + + // then — any occupied entry is rejected regardless of value type + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "tee"); + }); + } + + // --- select_image_hash --- + + #[test] + fn select_hash_override_present_and_in_approved_list() { + // given + let override_digest = digest('b'); + let approved = approved_file(vec![digest('c'), override_digest.clone(), digest('d')]); + + // when + let result = select_image_hash(Some(&approved), &digest('f'), Some(&override_digest)); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, override_digest); + }); + } + + #[test] + fn select_hash_override_not_in_approved_list() { + // given + let override_digest = digest('b'); + let approved = approved_file(vec![digest('c'), digest('d')]); + + // when + let result = select_image_hash(Some(&approved), &digest('f'), Some(&override_digest)); + + // then + assert_matches!(result, Err(LauncherError::InvalidHashOverride(_))); + } + + #[test] + fn select_hash_no_override_picks_newest() { + // given - first entry is "newest" + let newest = digest('a'); + let approved = approved_file(vec![newest.clone(), digest('b'), digest('c')]); + + // when + let result = select_image_hash(Some(&approved), &digest('f'), None); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, newest); + }); + } + + #[test] + fn select_hash_missing_file_falls_back_to_default() { + // given + let default = digest('d'); + + // when + let result = select_image_hash(None, &default, None); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, default); + }); + } + + #[test] + fn select_hash_missing_file_ignores_override() { + // given - override is set but file is missing, so default wins + let default = digest('d'); + let override_digest = digest('b'); + + // when + let result = select_image_hash(None, &default, Some(&override_digest)); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, default); + }); + } + + // --- approved_hashes JSON key alignment --- + + #[test] + fn approved_hashes_json_key_is_approved_hashes() { + // given - the JSON field name must match between launcher and MPC node + let file = approved_file(vec![sample_digest()]); + + // when + let json = serde_json::to_value(&file).unwrap(); + + // then + assert!(json.get("approved_hashes").is_some()); + } + + #[tokio::test] + async fn get_manifest_digest_resolves_docker_v2() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + let manifest_digest = digest('b'); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + let manifest_body = serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": expected_image_digest.to_string() } + }); + + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/v1.0"); + then.status(200) + .header("Docker-Content-Digest", manifest_digest.to_string()) + .json_body(manifest_body); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then + assert_matches!(result, Ok(d) => { + assert_eq!(d, manifest_digest); + }); + } + + #[tokio::test] + async fn get_manifest_digest_follows_image_index_to_amd64_manifest() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + let manifest_digest = digest('c'); + let amd64_ref = "sha256:amd64ref"; + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + // First request: image index pointing to amd64 manifest + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/latest"); + then.status(200).json_body(serde_json::json!({ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "digest": amd64_ref, + "platform": { "architecture": "amd64", "os": "linux" } + }, + { + "digest": "sha256:armref", + "platform": { "architecture": "arm64", "os": "linux" } + } + ] + })); + }); + + // Second request: the resolved amd64 manifest + server.mock(|when, then| { + when.method(GET) + .path(format!("/v2/test/image/manifests/{amd64_ref}")); + then.status(200) + .header("Docker-Content-Digest", manifest_digest.to_string()) + .json_body(serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": expected_image_digest.to_string() } + })); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("latest"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then + assert_matches!(result, Ok(d) => { + assert_eq!(d, manifest_digest); + }); + } + + #[tokio::test] + async fn get_manifest_digest_skips_mismatched_config_digest() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + let wrong_digest = digest('f'); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/v1.0"); + then.status(200) + .header("Docker-Content-Digest", "sha256:doesntmatter") + .json_body(serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": wrong_digest.to_string() } + })); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then + assert_matches!(result, Err(LauncherError::ImageHashNotFoundAmongTags)); + } + + #[tokio::test] + async fn get_manifest_digest_errors_on_auth_failure() { + // given + let server = MockServer::start(); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(403); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &sample_digest()).await; + + // then + assert_matches!(result, Err(LauncherError::RegistryAuthFailed(_))); + } + + #[tokio::test] + async fn get_manifest_digest_missing_content_digest_header_skips_tag() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + // Manifest matches but no Docker-Content-Digest header + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/v1.0"); + then.status(200).json_body(serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": expected_image_digest.to_string() } + })); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then - tag is skipped, no more tags → error + assert_matches!(result, Err(LauncherError::ImageHashNotFoundAmongTags)); + } +} + +/// Tests requiring network access and Docker Hub. +#[cfg(all(test, feature = "external-services-tests"))] +mod integration_tests { + use super::*; + #[cfg(target_os = "linux")] + use assert_matches::assert_matches; + + // # Dockerfile + // FROM alpine@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 + // CMD ["true"] + // TODO: Look into reusing this image, as its small and will be faster on CI + + const TEST_DIGEST: &str = + "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; + const TEST_TAG: &str = "83b52da4e2270c688cdd30da04f6b9d3565f25bb"; + const TEST_IMAGE_NAME: &str = "nearone/testing"; + const TEST_REGISTRY: &str = "registry.hub.docker.com"; + + fn test_launcher_config() -> LauncherConfig { + LauncherConfig { + image_tags: near_mpc_bounded_collections::NonEmptyVec::from_vec(vec![TEST_TAG.into()]) + .unwrap(), + image_name: TEST_IMAGE_NAME.into(), + registry: TEST_REGISTRY.into(), + rpc_request_timeout_secs: 10, + rpc_request_interval_secs: 1, + rpc_max_attempts: 20, + mpc_hash_override: None, + port_mappings: vec![], + } + } + + #[tokio::test] + async fn get_manifest_digest_resolves_known_image() { + // given + let config = test_launcher_config(); + let expected_digest: DockerSha256Digest = TEST_DIGEST.parse().unwrap(); + + // when + let registry = DockerRegistry::new(&config); + let result = get_manifest_digest(®istry, &config, &expected_digest).await; + + // then + assert!(result.is_ok(), "get_manifest_digest failed: {result:?}"); + } + + // `validate_image_hash` compares the output of `docker inspect .ID` against + // the expected config digest. On native Linux, `.ID` returns the config digest + // (sha256 of the image config blob), but on macOS, Docker Desktop's containerd + // image store returns the manifest digest instead, causing a spurious mismatch. + #[cfg(target_os = "linux")] + #[tokio::test] + async fn validate_image_hash_succeeds_for_known_image() { + // given + let config = test_launcher_config(); + let expected_digest: DockerSha256Digest = TEST_DIGEST.parse().unwrap(); + + // when + let result = validate_image_hash(&config, expected_digest).await; + + // then + assert_matches!(result, Ok(_)); + } +} diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs new file mode 100644 index 000000000..0f122f4b7 --- /dev/null +++ b/crates/tee-launcher/src/types.rs @@ -0,0 +1,288 @@ +use std::net::Ipv4Addr; +use std::num::NonZeroU16; + +use launcher_interface::types::DockerSha256Digest; +use url::Host; + +use clap::{Parser, ValueEnum}; +use near_mpc_bounded_collections::NonEmptyVec; +use serde::{Deserialize, Serialize}; + +/// CLI arguments parsed from environment variables via clap. +#[derive(Parser, Debug)] +#[command(name = "tee-launcher")] +pub(crate) struct CliArgs { + /// Platform mode: TEE or NONTEE + #[arg(long, env = "PLATFORM")] + pub(crate) platform: Platform, + + #[arg(long, env = "DOCKER_CONTENT_TRUST")] + // ensure that `docker_content_trust` is enabled. + docker_content_trust: DockerContentTrust, + + /// Fallback image digest when the approved-hashes file is absent + #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] + pub(crate) default_image_digest: DockerSha256Digest, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum DockerContentTrust { + #[value(name = "1")] + Enabled, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub(crate) enum Platform { + #[value(name = "TEE")] + Tee, + #[value(name = "NONTEE")] + NonTee, +} + +/// Typed representation of the dstack user config file (`/tapp/user_config`). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Config { + pub(crate) launcher_config: LauncherConfig, + /// Opaque MPC node configuration table. + /// The launcher does not interpret these fields — they are re-serialized + /// to a TOML string, written to a file on disk, and mounted into the + /// container for the MPC binary to consume via `start-with-config-file`. + pub(crate) mpc_node_config: toml::Table, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct LauncherConfig { + /// Docker image tags to search (from `MPC_IMAGE_TAGS`, comma-separated). + pub(crate) image_tags: NonEmptyVec, + /// Docker image name (from `MPC_IMAGE_NAME`). + pub(crate) image_name: String, + /// Docker registry (from `MPC_REGISTRY`). + pub(crate) registry: String, + /// Per-request timeout for registry RPC calls (from `RPC_REQUEST_TIMEOUT_SECS`). + pub(crate) rpc_request_timeout_secs: u64, + /// Delay between registry RPC retries (from `RPC_REQUEST_INTERVAL_SECS`). + pub(crate) rpc_request_interval_secs: u64, + /// Maximum registry RPC attempts (from `RPC_MAX_ATTEMPTS`). + pub(crate) rpc_max_attempts: u32, + /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). + pub(crate) mpc_hash_override: Option, + pub(crate) port_mappings: Vec, +} + +/// A `--add-host` entry: `hostname:IPv4`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct HostEntry { + pub(crate) hostname: Host, + pub(crate) ip: Ipv4Addr, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct PortMapping { + pub(crate) host: NonZeroU16, + pub(crate) container: NonZeroU16, +} + +impl PortMapping { + /// Returns e.g. `"11780:11780"` for use in docker-compose port lists. + pub(crate) fn docker_compose_value(&self) -> String { + format!("{}:{}", self.host, self.container) + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use std::net::Ipv4Addr; + use std::num::NonZeroU16; + + use super::*; + + // --- HostEntry deserialization --- + + #[test] + fn host_entry_valid_deserialization() { + // given + let json = serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "192.168.1.1"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(entry) => { + assert_eq!(entry.ip, Ipv4Addr::new(192, 168, 1, 1)); + }); + } + + #[test] + fn host_entry_rejects_invalid_ip() { + // given + let json = serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "not-an-ip"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn host_entry_rejects_plain_string_as_hostname() { + // given - url::Host requires tagged variant, plain string is rejected + let json = serde_json::json!({"hostname": "node.local", "ip": "192.168.1.1"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn host_entry_rejects_injection_string_as_hostname() { + // given + let json = serde_json::json!({"hostname": "--env LD_PRELOAD=hack.so", "ip": "192.168.1.1"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + // --- PortMapping deserialization --- + + #[test] + fn port_mapping_valid_deserialization() { + // given + let json = serde_json::json!({"host": 11780, "container": 11780}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(_)); + } + + #[test] + fn port_mapping_rejects_zero_port() { + // given + let json = serde_json::json!({"host": 0, "container": 11780}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn port_mapping_rejects_out_of_range_port() { + // given + let json = serde_json::json!({"host": 65536, "container": 11780}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + // --- docker_compose_value output format --- + + #[test] + fn port_mapping_docker_compose_value() { + // given + let mapping = PortMapping { + host: NonZeroU16::new(11780).unwrap(), + container: NonZeroU16::new(11780).unwrap(), + }; + + // when + let value = mapping.docker_compose_value(); + + // then + assert_eq!(value, "11780:11780"); + } + + // --- Config full deserialization (TOML) --- + + #[test] + fn config_deserializes_valid_toml() { + // given + let toml_str = r#" +[launcher_config] +image_tags = ["tag1"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +port_mappings = [{ host = 11780, container = 11780 }] + +[mpc_node_config] +home_dir = "/data" +some_opaque_field = true +"#; + + // when + let result = toml::from_str::(toml_str); + + // then + assert_matches!(result, Ok(config) => { + assert_eq!(config.launcher_config.image_name, "nearone/mpc-node"); + assert_eq!(config.mpc_node_config["home_dir"].as_str(), Some("/data")); + assert_eq!(config.mpc_node_config["some_opaque_field"].as_bool(), Some(true)); + }); + } + + #[test] + fn config_mpc_config_round_trips_to_toml_string() { + // given + let toml_str = r#" +[launcher_config] +image_tags = ["tag1"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +port_mappings = [{ host = 11780, container = 11780 }] + +[mpc_node_config] +home_dir = "/data" +arbitrary_key = "arbitrary_value" +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + + // when — re-serialize the opaque table (what the launcher writes to disk) + let serialized = toml::to_string(&config.mpc_node_config).unwrap(); + + // then + assert!(serialized.contains("home_dir")); + assert!(serialized.contains("arbitrary_key")); + } + + #[test] + fn config_rejects_missing_required_field() { + // given - mpc_config is missing + let toml_str = r#" +[launcher_config] +image_tags = ["tag1"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +port_mappings = [] +"#; + + // when + let result = toml::from_str::(toml_str); + + // then + assert_matches!(result, Err(_)); + } +} diff --git a/crates/test-utils/assets/README.md b/crates/test-utils/assets/README.md index 15c781905..c0445a918 100644 --- a/crates/test-utils/assets/README.md +++ b/crates/test-utils/assets/README.md @@ -1,7 +1,6 @@ # Updating Test Assets -Updating test assets is needed when updating launcher code (or when updating other measured components). See [UPDATING_LAUNCHER.md](../../../tee_launcher/UPDATING_LAUNCHER.md) - +Updating test assets is needed when updating launcher code (or when updating other measured components). See [UPDATING_LAUNCHER.md](../../../docs/UPDATING_LAUNCHER.md) To update the test asset files, fetch `/public_data` from the MPC node’s public HTTP endpoint and save the response to a JSON file. @@ -12,11 +11,9 @@ Example: curl http://:/public_data -o public_data.json ``` - See [single-node-readme.md](../../../localnet/tee/scripts/single-node-readme.md) for automation script that will launch a TEE MPC node, collect the attestation, and save the public data into /tmp/%user/public_data.json - ## Steps 1. Change into the `crates/test_utils/assets` directory: diff --git a/crates/test-utils/assets/app_compose.json b/crates/test-utils/assets/app_compose.json index a1c06b183..7119afab6 100644 --- a/crates/test-utils/assets/app_compose.json +++ b/crates/test-utils/assets/app_compose.json @@ -1,8 +1,8 @@ { "manifest_version": 2, - "name": "mpc-localnet-one-node-1771748631", + "name": "mpc-local-node0-testnet-tee", "runner": "docker-compose", - "docker_compose_file": "version: '3.8'\n\nservices:\n launcher:\n image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701\n\n container_name: launcher\n\n environment:\n - PLATFORM=TEE\n - DOCKER_CONTENT_TRUST=1\n - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45\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", + "docker_compose_file": "version: '3.8'\n\nservices:\n launcher:\n image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316\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", "kms_enabled": false, "gateway_enabled": false, "local_key_provider_enabled": true, diff --git a/crates/test-utils/assets/collateral.json b/crates/test-utils/assets/collateral.json index a4e0b9e3f..3a22de564 100644 --- a/crates/test-utils/assets/collateral.json +++ b/crates/test-utils/assets/collateral.json @@ -1,11 +1,11 @@ { "pck_crl_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC\nMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD\nb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw\nCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg\nBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs\nIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex\nCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO\n2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl\neTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS\nBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d\nzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue\nnA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "root_ca_crl": "308201203081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3235303332303131323135375a170d3236303430333131323135375aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020347003044022030c9fce1438da0a94e4fffdd46c9650e393be6e5a7862d4e4e73527932d04af302206539efe3f734c3d7df20d9dfc4630e1c7ff0439a0f8ece101f15b5eaff9b4f33", - "pck_crl": "30820d1730820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232313133343330335a170d3236303332333133343330335a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303232313133343330335a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303232313133343330335a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303232313133343330335a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303232313133343330335a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303232313133343330335a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303232313133343330335a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303232313133343330335a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303232313133343330335a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303232313133343330335a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303232313133343330335a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303232313133343330335a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303232313133343330335a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303232313133343330335a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303232313133343330335a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303232313133343330335a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303232313133343330335a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303232313133343330335a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303232313133343330335a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303232313133343330335a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303232313133343330335a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303232313133343330335a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303232313133343330335a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303232313133343330335a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303232313133343330335a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303232313133343330335a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303232313133343330335a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303232313133343330335a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303232313133343330335a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303232313133343330335a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303232313133343330335a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303232313133343330335a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303232313133343330335a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303232313133343330335a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303232313133343330335a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303232313133343330335a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303232313133343330335a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303232313133343330335a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303232313133343330335a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303232313133343330335a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303232313133343330335a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303232313133343330335a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d040302034800304502203bd1f94fcc12868b9760bcf134ce34b844713b667fcb9e4d2207fc0cd81566de022100ef021d2ca3ff8bd81861b3a3722f955703b69f5133cfe5fea3d4a1098922f103", + "root_ca_crl": "308201223081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232363133303430305a170d3237303232363133303430305aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020349003046022100c252ed59c795ba2b11496a4a99758bb8cbc380a1ebbb0865be69f2c4b38bb6400221009a7d8b03602a9ee2d62322d759166d6933d24d9dfa01ab3fde4520691d715bd7", + "pck_crl": "30820d1830820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303331383136303130355a170d3236303431373136303130355a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303331383136303130355a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303331383136303130355a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303331383136303130355a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303331383136303130355a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303331383136303130355a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303331383136303130355a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303331383136303130355a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303331383136303130355a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303331383136303130355a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303331383136303130355a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303331383136303130355a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303331383136303130355a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303331383136303130355a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303331383136303130355a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303331383136303130355a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303331383136303130355a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303331383136303130355a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303331383136303130355a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303331383136303130355a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303331383136303130355a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303331383136303130355a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303331383136303130355a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303331383136303130355a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303331383136303130355a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303331383136303130355a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303331383136303130355a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303331383136303130355a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303331383136303130355a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303331383136303130355a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303331383136303130355a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303331383136303130355a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303331383136303130355a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303331383136303130355a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303331383136303130355a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303331383136303130355a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303331383136303130355a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303331383136303130355a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303331383136303130355a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303331383136303130355a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303331383136303130355a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303331383136303130355a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d0403020349003046022100c71b1e53814b3437773403491a1e10a53043880e98868ee957bfa044031c3bf3022100d12d92717d516bd91b8a0c275fce3dc886f4cbc8a2300275fcf01a78691eeefd", "tcb_info_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-02-21T13:31:57Z\",\"nextUpdate\":\"2026-03-23T13:31:57Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", - "tcb_info_signature": "6cb0f2b2d37350c8d25f478cac1b5a341cc1d6deb8379f0aa5c8708ebf00fbd7fb39b146ebd086593f7b86f37c8cb82a0e3df178857625ef95d64f636167c5eb", + "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-03-18T15:43:27Z\",\"nextUpdate\":\"2026-04-17T15:43:27Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", + "tcb_info_signature": "77bd013b1fbb1162604d1b76e2ead05315b61963d15c34e6c8dfbec009930dcaa55e2026eb3befad7df463210a85c392d9b77caf72c76e4ec03f02c71855a95c", "qe_identity_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-02-21T13:43:47Z\",\"nextUpdate\":\"2026-03-23T13:43:47Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", - "qe_identity_signature": "8d31db300d8fbd61c3525f177a5963ef729139e358b9b572822f7a72812d3eff83fc06a97b0cc9d921a3a12ad63f547266aff1629b9bae4ec31d69305bdb6c52" + "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-03-18T15:16:33Z\",\"nextUpdate\":\"2026-04-17T15:16:33Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", + "qe_identity_signature": "a0c628117e0d40c9b2682dff38cd21283c70bc319546e3a05c530458c8a532f3bc67d5c66196dd012f9fe8490da0648b9ec0bbeb43ed48662a969871567abec4" } diff --git a/crates/test-utils/assets/launcher_image_compose.yaml b/crates/test-utils/assets/launcher_image_compose.yaml index 314e066f2..25a19acff 100644 --- a/crates/test-utils/assets/launcher_image_compose.yaml +++ b/crates/test-utils/assets/launcher_image_compose.yaml @@ -2,20 +2,20 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701 + image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316 container_name: launcher environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45 + - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 volumes: - /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 diff --git a/crates/test-utils/assets/mpc_image_digest.txt b/crates/test-utils/assets/mpc_image_digest.txt index 9b36d86aa..ea6e0fda2 100644 --- a/crates/test-utils/assets/mpc_image_digest.txt +++ b/crates/test-utils/assets/mpc_image_digest.txt @@ -1 +1 @@ -6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45 \ No newline at end of file +6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 \ No newline at end of file diff --git a/crates/test-utils/assets/near_account_public_key.pub b/crates/test-utils/assets/near_account_public_key.pub index 48fca4efd..2f1c3c5c4 100644 --- a/crates/test-utils/assets/near_account_public_key.pub +++ b/crates/test-utils/assets/near_account_public_key.pub @@ -1 +1 @@ -ed25519:7DeYpZ9UDY9t6hgw9zakSu2VrseuVpwhaTwLfWUNppqF \ No newline at end of file +ed25519:3QLLYQfJcDv5UD9Spo8dR32XrWiZdTZrA93KCJ7DMcN3 \ No newline at end of file diff --git a/crates/test-utils/assets/near_p2p_public_key.pub b/crates/test-utils/assets/near_p2p_public_key.pub index 9c9f00845..cacb92286 100644 --- a/crates/test-utils/assets/near_p2p_public_key.pub +++ b/crates/test-utils/assets/near_p2p_public_key.pub @@ -1 +1 @@ -ed25519:os7wBGxbPavhE9F56eSU9G2TqDT3YFdLsZqeSijqAT4 \ No newline at end of file +ed25519:9vx7F6MxKXcvT4rpxrtTdowAUA2gDvNssHymSWan8oAt \ No newline at end of file diff --git a/crates/test-utils/assets/public_data.json b/crates/test-utils/assets/public_data.json index 16c8d6d65..0709587f2 100644 --- a/crates/test-utils/assets/public_data.json +++ b/crates/test-utils/assets/public_data.json @@ -1,82 +1,33 @@ { - "near_signer_public_key": "ed25519:7DeYpZ9UDY9t6hgw9zakSu2VrseuVpwhaTwLfWUNppqF", - "near_p2p_public_key": "ed25519:os7wBGxbPavhE9F56eSU9G2TqDT3YFdLsZqeSijqAT4", + "near_signer_public_key": "ed25519:3QLLYQfJcDv5UD9Spo8dR32XrWiZdTZrA93KCJ7DMcN3", + "near_p2p_public_key": "ed25519:9vx7F6MxKXcvT4rpxrtTdowAUA2gDvNssHymSWan8oAt", "near_responder_public_keys": [ - "ed25519:Ko4VqMsrDPapf3M3isuu4nyycU4ZJdhAVxF46SAaiyU", - "ed25519:J3qZzxjRCQeQnQwr4P9E18Au69CNabBYBX5stc9BijaL", - "ed25519:A3LveKbnJUtQES94HU6sapE61hPC7pFErWXmBTEWqRJ6", - "ed25519:EZJ8k8VVa7XDRkXpYDJ1muJU4Zt3W8emcuFAQennTSPF", - "ed25519:BJKEe7FvsEYbAqYoAnuZcCroYnoN7byUC35kPnB6Kt8a", - "ed25519:B1e1eCns6FipzA1EpqYsGEmW5FshJ1VG9p38wjoLXoU5", - "ed25519:DSbs39dUxCkr76joLopC19wX22Z7mkTtyWqFfqfjBHuc", - "ed25519:De8qFXwVzdMkrxjsvQuzdAQrukjdykvigsBABjQK6h2m", - "ed25519:7RJA77oDDvTApLQCS6eDJwZiZzZzYvrNQKgdQvWTBWRR", - "ed25519:DMAYtSo9DfpQuZQCmwYkfbbDiteU4bvvTnFYREfuGbpR", - "ed25519:289rt9nZTEVzUrA9N4txXWSUCMfthoT6WEWgSiKUXx2D", - "ed25519:Dtf6K5Bnv6BYEw5vuA2jgfPm1B6XfJGcRcMCWnH6RAuX", - "ed25519:hSmYb4czfXMxdAUVhiAUTRfJJh8vMvyUzS5QcXEUGyz", - "ed25519:41rhaYLfd7QtYKogU49a2hWETfHkFYUfiTfzsB6iF5an", - "ed25519:63HvyThe2VeZJgsFDKUxBboxmgyzrNhyovPvVacso8HC", - "ed25519:Do2HSNDBS1k9RAr3KHNyTuXa68QciP4PNy8oNyNKng1", - "ed25519:7p7ptby14Wo5k6iCwwNZjJcsSLCJx39pc38riabktri1", - "ed25519:72FNME7ZZA7C7YtaoYfGSDAweP3GcR5BEC2xKWAqJ1r7", - "ed25519:BwbS1W9PygQLKpnrARAQDFDUeuepLUXSY5D7fVn1ByiA", - "ed25519:Aa6evgmAi2JSkskYrvD1cMTNSeBAFR8MQDbrw4J1q9hK", - "ed25519:7HWJCvJvRyW6Evi4jhBBKi5g1PWVDDgFVv2wFYngmwi4", - "ed25519:FRr8TDyHEVcGiTDBT7hasRCJpQiMe2J4beh9f3R2o15c", - "ed25519:G8iGeBgatiMB8QPzqBwSPdyPpYUd8EQ87vjLJQFFdD2Q", - "ed25519:AcFS33c45SiyhGjq7vAFNY9L9Fa5eLfcp12pWMNK6jyk", - "ed25519:7qmeGN3zVxYrZZEC52AkRKakm3L77NiVKd1NPPkCdqAr", - "ed25519:3drp9wPWoKgCFWER9pvy6psyqfgEYmfsFmCgoTaYP1mz", - "ed25519:2zx3A1noqdWQFxX8JczMUfm1cya434qDLaCUbaP7Xexo", - "ed25519:HSuYfvb98M7wpaVjFhP2aoW7Kv1VpDxU6NcWRd7taKTS", - "ed25519:GEW3FMMkGtesgqS5CiCw2WzYkt7tNZFPc37FEZTFDZDq", - "ed25519:8XwzZaRNxJDrmqwkz3Q6V7x91nVjhM4H3q1d69pDVzxT", - "ed25519:5Y8JwpvzoDDgRxdc9MpVUjC6BZ5AWNAgVNkXQCK6nvEB", - "ed25519:HmPSb6ZMyHdSjGW8Jzwuwp2KMJ6y4oBgbdGD4SgYviK2", - "ed25519:DMSKnq9htoLTFYrx3MVKbHLTpsJqPD5eMmF4FsuUWQxZ", - "ed25519:Hg2EfJSGMyxY9utafnFXiZBhzLHiqzaGBhfgxpJEcEFX", - "ed25519:7Q4afPMBCNATdibmnRWYtBmpLwr7ybDRcFfpTa8hxkNK", - "ed25519:3NvXx3CPsYJNMg4r9J5GbFfU5hf8xaKnYjd18nLWSF3t", - "ed25519:FaVhEYnD1VsYows1FqqwQFzWw9svirb6R7is8ZA2fYKT", - "ed25519:89FdAs8nEkn74SRsRuNierfkj4LfWtRsKBryLskuaBVE", - "ed25519:FucAj3Fv9X7Ri7TrrpoGQm2mk4x1Zx45p4rFe3ovUREY", - "ed25519:2KSeFPShZbRXcdxWBpU4gEfhQBcgyJfvPWB8TSMNhnzh", - "ed25519:nVqZNPk5nTN1MMJdSY97v1qQYnqGRsvKuf9iQB4PYs9", - "ed25519:5ZNs2mqURhwaF9HCrxx6uKR7GAR4Gp8hXEskm3LSLodU", - "ed25519:CTQSM5AVVRGjrSJ1duoofYPnJbqJNiAwmVyqAtSvsRUS", - "ed25519:5T3wY4pB84QfAeUFwPGGYwGVN542nWXxDEJU4oYJAVL3", - "ed25519:BikJcvyjmUDM4q1tfzU5rmocxZgZvyd7TwRyktPSbmAD", - "ed25519:6WbYqApHTHC33uPkJg88i7YHkKBRABEYq2b8snHcFXCC", - "ed25519:BhpGVA3SnXdLEJxWY1y74G4NA2jJM4SCxbsuv2Hmg8Rn", - "ed25519:Dgu2PJfqSkf6w4DuB8UsJZS8RfPiaDwBDWAfpNj3sVVr", - "ed25519:WVjzNCCt5ogksujdh8XcrgkJu97uPZsD5wE1A7WvW4g", - "ed25519:G89VLPw46CxgdV99DexVt4zviTqy1WD1G6Y2XPZxuc7o" + "ed25519:DG5cLS9eQBta3JzGAtsc7CJYviC7ktvUhWds8XCzsfdG" ], "tee_participant_info": { "Dstack": { - "quote": [4, 0, 2, 0, 129, 0, 0, 0, 0, 0, 0, 0, 147, 154, 114, 51, 247, 156, 76, 169, 148, 10, 13, 179, 149, 127, 6, 7, 61, 153, 138, 108, 16, 87, 107, 253, 246, 246, 237, 142, 155, 133, 233, 50, 0, 0, 0, 0, 11, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 240, 99, 40, 14, 148, 251, 5, 31, 93, 215, 177, 252, 89, 206, 154, 172, 66, 187, 150, 29, 248, 212, 75, 112, 156, 155, 15, 248, 122, 123, 77, 246, 72, 101, 123, 166, 209, 24, 149, 137, 254, 171, 29, 90, 60, 154, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 231, 2, 6, 0, 0, 0, 0, 0, 240, 109, 253, 166, 220, 225, 207, 144, 77, 78, 43, 171, 29, 195, 112, 99, 76, 249, 92, 239, 162, 206, 178, 222, 46, 238, 18, 124, 147, 130, 105, 128, 144, 215, 164, 161, 62, 20, 197, 54, 236, 108, 156, 60, 143, 168, 112, 119, 1, 110, 105, 242, 11, 52, 142, 61, 104, 116, 87, 244, 229, 124, 63, 254, 60, 251, 113, 56, 246, 146, 168, 139, 218, 191, 234, 199, 146, 253, 217, 141, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 115, 190, 47, 112, 190, 239, 183, 11, 72, 166, 16, 158, 237, 71, 21, 215, 39, 13, 70, 131, 179, 191, 53, 111, 162, 95, 175, 191, 26, 167, 110, 57, 233, 18, 126, 110, 104, 140, 205, 169, 139, 218, 177, 212, 212, 127, 70, 167, 181, 35, 39, 141, 79, 145, 78, 232, 223, 14, 200, 12, 209, 195, 212, 152, 203, 241, 21, 43, 12, 94, 175, 101, 186, 217, 66, 80, 114, 135, 74, 63, 207, 137, 30, 139, 1, 113, 61, 61, 153, 55, 227, 224, 210, 108, 21, 219, 244, 146, 76, 7, 245, 6, 111, 61, 198, 133, 152, 68, 24, 67, 68, 48, 106, 163, 38, 56, 23, 21, 61, 202, 238, 133, 175, 151, 210, 62, 12, 11, 150, 239, 224, 115, 29, 136, 101, 168, 116, 126, 81, 185, 227, 81, 172, 201, 31, 132, 227, 182, 188, 148, 117, 137, 112, 2, 51, 248, 229, 0, 217, 195, 123, 45, 121, 115, 131, 137, 239, 97, 209, 209, 129, 132, 245, 11, 172, 178, 11, 152, 186, 41, 211, 45, 250, 26, 147, 59, 17, 246, 215, 102, 85, 0, 1, 195, 223, 53, 227, 107, 60, 23, 144, 43, 16, 55, 239, 206, 246, 210, 246, 13, 185, 162, 204, 96, 156, 224, 219, 17, 240, 182, 48, 192, 132, 93, 226, 121, 108, 93, 157, 145, 44, 64, 158, 145, 229, 42, 58, 76, 114, 127, 244, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 204, 16, 0, 0, 10, 124, 154, 105, 114, 192, 236, 4, 179, 208, 82, 53, 162, 157, 20, 21, 152, 133, 71, 85, 85, 21, 59, 229, 138, 60, 128, 119, 31, 41, 74, 151, 97, 88, 211, 84, 31, 214, 163, 180, 43, 240, 136, 29, 116, 231, 138, 255, 148, 105, 65, 17, 10, 5, 221, 116, 226, 119, 8, 76, 218, 67, 115, 16, 142, 188, 151, 224, 189, 139, 21, 116, 87, 28, 157, 151, 225, 226, 250, 218, 147, 80, 231, 144, 252, 223, 62, 103, 176, 31, 51, 101, 181, 44, 82, 180, 72, 148, 151, 10, 88, 144, 81, 87, 230, 135, 174, 102, 165, 143, 241, 229, 60, 148, 151, 208, 187, 151, 100, 64, 82, 171, 52, 1, 40, 143, 31, 160, 6, 0, 70, 16, 0, 0, 4, 4, 25, 27, 4, 255, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 231, 0, 0, 0, 0, 0, 0, 0, 229, 163, 167, 181, 216, 48, 194, 149, 59, 152, 83, 76, 108, 89, 163, 163, 79, 220, 52, 233, 51, 247, 245, 137, 143, 10, 133, 207, 8, 132, 107, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 158, 42, 124, 111, 148, 143, 23, 71, 78, 52, 167, 252, 67, 237, 3, 15, 124, 21, 99, 241, 186, 189, 223, 99, 64, 200, 46, 14, 84, 168, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215, 98, 66, 167, 203, 62, 30, 139, 2, 195, 192, 57, 51, 40, 206, 207, 77, 159, 207, 61, 144, 77, 52, 130, 175, 199, 184, 139, 101, 54, 6, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 243, 159, 196, 112, 17, 166, 99, 200, 182, 156, 136, 179, 201, 15, 15, 47, 98, 23, 37, 24, 68, 126, 153, 62, 34, 18, 139, 203, 16, 236, 185, 233, 46, 116, 134, 201, 84, 179, 122, 113, 148, 250, 173, 226, 9, 104, 70, 208, 7, 5, 223, 11, 239, 88, 182, 207, 12, 115, 185, 54, 184, 154, 234, 32, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 5, 0, 94, 14, 0, 0, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 69, 56, 84, 67, 67, 66, 74, 97, 103, 65, 119, 73, 66, 65, 103, 73, 85, 102, 50, 83, 98, 121, 119, 107, 77, 86, 84, 74, 75, 85, 53, 55, 47, 74, 119, 66, 112, 56, 69, 100, 104, 80, 48, 52, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 99, 68, 69, 105, 77, 67, 65, 71, 65, 49, 85, 69, 65, 119, 119, 90, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 66, 68, 83, 121, 66, 81, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 66, 68, 81, 84, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 67, 103, 119, 82, 10, 83, 87, 53, 48, 90, 87, 119, 103, 81, 50, 57, 121, 99, 71, 57, 121, 89, 88, 82, 112, 98, 50, 52, 120, 70, 68, 65, 83, 66, 103, 78, 86, 66, 65, 99, 77, 67, 49, 78, 104, 98, 110, 82, 104, 73, 69, 78, 115, 89, 88, 74, 104, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 73, 10, 68, 65, 74, 68, 81, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 66, 104, 77, 67, 86, 86, 77, 119, 72, 104, 99, 78, 77, 106, 85, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 87, 104, 99, 78, 77, 122, 73, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 10, 87, 106, 66, 119, 77, 83, 73, 119, 73, 65, 89, 68, 86, 81, 81, 68, 68, 66, 108, 74, 98, 110, 82, 108, 98, 67, 66, 84, 82, 49, 103, 103, 85, 69, 78, 76, 73, 69, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 10, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 10, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 66, 90, 77, 66, 77, 71, 66, 121, 113, 71, 83, 77, 52, 57, 65, 103, 69, 71, 67, 67, 113, 71, 83, 77, 52, 57, 65, 119, 69, 72, 65, 48, 73, 65, 66, 71, 112, 118, 10, 48, 89, 117, 89, 114, 113, 65, 117, 83, 75, 66, 122, 75, 108, 117, 98, 54, 109, 76, 43, 114, 118, 102, 68, 53, 65, 106, 89, 79, 51, 81, 78, 103, 102, 87, 122, 116, 103, 52, 101, 109, 49, 69, 71, 66, 86, 107, 71, 108, 87, 118, 100, 117, 66, 48, 88, 81, 83, 69, 47, 115, 120, 71, 68, 10, 109, 83, 118, 75, 111, 57, 116, 51, 67, 114, 79, 80, 67, 52, 83, 85, 54, 88, 54, 106, 103, 103, 77, 77, 77, 73, 73, 68, 67, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 83, 86, 98, 49, 51, 78, 118, 82, 118, 104, 54, 85, 66, 74, 121, 100, 84, 48, 10, 77, 56, 52, 66, 86, 119, 118, 101, 86, 68, 66, 114, 66, 103, 78, 86, 72, 82, 56, 69, 90, 68, 66, 105, 77, 71, 67, 103, 88, 113, 66, 99, 104, 108, 112, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 70, 119, 97, 83, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 51, 78, 110, 101, 67, 57, 106, 90, 88, 74, 48, 97, 87, 90, 112, 89, 50, 70, 48, 97, 87, 57, 117, 76, 51, 89, 48, 76, 51, 66, 106, 97, 50, 78, 121, 98, 68, 57, 106, 89, 84, 49, 119, 10, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 90, 108, 98, 109, 78, 118, 90, 71, 108, 117, 90, 122, 49, 107, 90, 88, 73, 119, 72, 81, 89, 68, 86, 82, 48, 79, 66, 66, 89, 69, 70, 71, 51, 110, 54, 83, 43, 75, 120, 78, 54, 116, 43, 72, 73, 56, 71, 112, 57, 54, 80, 107, 117, 90, 10, 105, 87, 115, 90, 77, 65, 52, 71, 65, 49, 85, 100, 68, 119, 69, 66, 47, 119, 81, 69, 65, 119, 73, 71, 119, 68, 65, 77, 66, 103, 78, 86, 72, 82, 77, 66, 65, 102, 56, 69, 65, 106, 65, 65, 77, 73, 73, 67, 79, 81, 89, 74, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 10, 66, 73, 73, 67, 75, 106, 67, 67, 65, 105, 89, 119, 72, 103, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 65, 81, 81, 81, 48, 103, 106, 102, 115, 81, 65, 106, 82, 113, 52, 98, 116, 79, 56, 113, 80, 65, 86, 83, 107, 106, 67, 67, 65, 87, 77, 71, 67, 105, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 119, 103, 103, 70, 84, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 66, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 67, 65, 103, 69, 69, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 68, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 69, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 70, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 71, 65, 103, 69, 66, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 72, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 73, 65, 103, 69, 70, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 74, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 75, 65, 103, 69, 65, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 76, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 77, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 78, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 79, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 80, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 81, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 82, 65, 103, 69, 76, 77, 66, 56, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 83, 66, 66, 65, 69, 10, 66, 65, 73, 67, 66, 65, 69, 65, 66, 81, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 77, 66, 65, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 77, 69, 65, 103, 65, 65, 77, 66, 81, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 81, 69, 10, 66, 114, 68, 65, 98, 119, 65, 65, 65, 68, 65, 80, 66, 103, 111, 113, 104, 107, 105, 71, 43, 69, 48, 66, 68, 81, 69, 70, 67, 103, 69, 66, 77, 66, 52, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 89, 69, 69, 68, 97, 57, 104, 116, 65, 56, 65, 74, 47, 90, 10, 50, 70, 109, 97, 76, 53, 74, 113, 47, 75, 69, 119, 82, 65, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 66, 122, 65, 50, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 66, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 67, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 68, 65, 81, 72, 47, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 65, 48, 107, 65, 77, 69, 89, 67, 10, 73, 81, 67, 70, 71, 49, 89, 65, 98, 51, 101, 88, 70, 116, 101, 56, 53, 51, 67, 108, 86, 66, 110, 104, 108, 67, 102, 68, 121, 99, 53, 55, 50, 90, 88, 69, 113, 97, 120, 52, 85, 99, 99, 83, 97, 119, 73, 104, 65, 79, 110, 48, 86, 78, 75, 84, 90, 109, 65, 120, 85, 70, 52, 110, 10, 119, 82, 107, 83, 70, 104, 52, 113, 70, 74, 51, 97, 85, 108, 122, 70, 111, 80, 81, 84, 51, 120, 73, 102, 55, 107, 70, 68, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 108, 106, 67, 67, 65, 106, 50, 103, 65, 119, 73, 66, 65, 103, 73, 86, 65, 74, 86, 118, 88, 99, 50, 57, 71, 43, 72, 112, 81, 69, 110, 74, 49, 80, 81, 122, 122, 103, 70, 88, 67, 57, 53, 85, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 10, 77, 71, 103, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 77, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 83, 98, 50, 57, 48, 73, 69, 78, 66, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 10, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 10, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 65, 101, 70, 119, 48, 120, 79, 68, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 70, 119, 48, 122, 77, 122, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 77, 72, 65, 120, 73, 106, 65, 103, 10, 66, 103, 78, 86, 66, 65, 77, 77, 71, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 81, 81, 48, 115, 103, 85, 71, 120, 104, 100, 71, 90, 118, 99, 109, 48, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 10, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 10, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 78, 83, 66, 47, 55, 116, 50, 49, 108, 88, 83, 79, 10, 50, 67, 117, 122, 112, 120, 119, 55, 52, 101, 74, 66, 55, 50, 69, 121, 68, 71, 103, 87, 53, 114, 88, 67, 116, 120, 50, 116, 86, 84, 76, 113, 54, 104, 75, 107, 54, 122, 43, 85, 105, 82, 90, 67, 110, 113, 82, 55, 112, 115, 79, 118, 103, 113, 70, 101, 83, 120, 108, 109, 84, 108, 74, 108, 10, 101, 84, 109, 105, 50, 87, 89, 122, 51, 113, 79, 66, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 10, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 108, 87, 57, 100, 10, 122, 98, 48, 98, 52, 101, 108, 65, 83, 99, 110, 85, 57, 68, 80, 79, 65, 86, 99, 76, 51, 108, 81, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 10, 65, 102, 56, 67, 65, 81, 65, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 82, 119, 65, 119, 82, 65, 73, 103, 88, 115, 86, 107, 105, 48, 119, 43, 105, 54, 86, 89, 71, 87, 51, 85, 70, 47, 50, 50, 117, 97, 88, 101, 48, 89, 74, 68, 106, 49, 85, 101, 10, 110, 65, 43, 84, 106, 68, 49, 97, 105, 53, 99, 67, 73, 67, 89, 98, 49, 83, 65, 109, 68, 53, 120, 107, 102, 84, 86, 112, 118, 111, 52, 85, 111, 121, 105, 83, 89, 120, 114, 68, 87, 76, 109, 85, 82, 52, 67, 73, 57, 78, 75, 121, 102, 80, 78, 43, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 106, 122, 67, 67, 65, 106, 83, 103, 65, 119, 73, 66, 65, 103, 73, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 97, 68, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 10, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 10, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 66, 52, 88, 68, 84, 69, 52, 77, 68, 85, 121, 77, 84, 69, 119, 78, 68, 85, 120, 77, 70, 111, 88, 68, 84, 81, 53, 77, 84, 73, 122, 77, 84, 73, 122, 78, 84, 107, 49, 79, 86, 111, 119, 97, 68, 69, 97, 77, 66, 103, 71, 10, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 10, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 10, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 67, 54, 110, 69, 119, 77, 68, 73, 89, 90, 79, 106, 47, 105, 80, 87, 115, 67, 122, 97, 69, 75, 105, 55, 10, 49, 79, 105, 79, 83, 76, 82, 70, 104, 87, 71, 106, 98, 110, 66, 86, 74, 102, 86, 110, 107, 89, 52, 117, 51, 73, 106, 107, 68, 89, 89, 76, 48, 77, 120, 79, 52, 109, 113, 115, 121, 89, 106, 108, 66, 97, 108, 84, 86, 89, 120, 70, 80, 50, 115, 74, 66, 75, 53, 122, 108, 75, 79, 66, 10, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 10, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 10, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 10, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 65, 102, 56, 67, 65, 81, 69, 119, 67, 103, 89, 73, 10, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 83, 81, 65, 119, 82, 103, 73, 104, 65, 79, 87, 47, 53, 81, 107, 82, 43, 83, 57, 67, 105, 83, 68, 99, 78, 111, 111, 119, 76, 117, 80, 82, 76, 115, 87, 71, 102, 47, 89, 105, 55, 71, 83, 88, 57, 52, 66, 103, 119, 84, 119, 103, 10, 65, 105, 69, 65, 52, 74, 48, 108, 114, 72, 111, 77, 115, 43, 88, 111, 53, 111, 47, 115, 88, 54, 79, 57, 81, 87, 120, 72, 82, 65, 118, 90, 85, 71, 79, 100, 82, 81, 55, 99, 118, 113, 82, 88, 97, 113, 73, 61, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "quote": [4, 0, 2, 0, 129, 0, 0, 0, 0, 0, 0, 0, 147, 154, 114, 51, 247, 156, 76, 169, 148, 10, 13, 179, 149, 127, 6, 7, 61, 153, 138, 108, 16, 87, 107, 253, 246, 246, 237, 142, 155, 133, 233, 50, 0, 0, 0, 0, 11, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 240, 99, 40, 14, 148, 251, 5, 31, 93, 215, 177, 252, 89, 206, 154, 172, 66, 187, 150, 29, 248, 212, 75, 112, 156, 155, 15, 248, 122, 123, 77, 246, 72, 101, 123, 166, 209, 24, 149, 137, 254, 171, 29, 90, 60, 154, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 231, 2, 6, 0, 0, 0, 0, 0, 240, 109, 253, 166, 220, 225, 207, 144, 77, 78, 43, 171, 29, 195, 112, 99, 76, 249, 92, 239, 162, 206, 178, 222, 46, 238, 18, 124, 147, 130, 105, 128, 144, 215, 164, 161, 62, 20, 197, 54, 236, 108, 156, 60, 143, 168, 112, 119, 1, 83, 236, 221, 225, 0, 154, 247, 12, 125, 232, 120, 198, 48, 97, 92, 131, 215, 76, 192, 223, 245, 128, 116, 216, 4, 194, 52, 38, 59, 232, 158, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 115, 190, 47, 112, 190, 239, 183, 11, 72, 166, 16, 158, 237, 71, 21, 215, 39, 13, 70, 131, 179, 191, 53, 111, 162, 95, 175, 191, 26, 167, 110, 57, 233, 18, 126, 110, 104, 140, 205, 169, 139, 218, 177, 212, 212, 127, 70, 167, 181, 35, 39, 141, 79, 145, 78, 232, 223, 14, 200, 12, 209, 195, 212, 152, 203, 241, 21, 43, 12, 94, 175, 101, 186, 217, 66, 80, 114, 135, 74, 63, 207, 137, 30, 139, 1, 113, 61, 61, 153, 55, 227, 224, 210, 108, 21, 219, 244, 146, 76, 7, 245, 6, 111, 61, 198, 133, 152, 68, 24, 67, 68, 48, 106, 163, 38, 56, 23, 21, 61, 202, 238, 133, 175, 151, 210, 62, 12, 11, 150, 239, 224, 115, 29, 136, 101, 168, 116, 126, 81, 185, 227, 81, 172, 218, 10, 68, 24, 210, 32, 87, 249, 132, 115, 76, 139, 18, 47, 196, 30, 6, 46, 191, 7, 36, 203, 162, 85, 86, 86, 236, 144, 73, 229, 115, 173, 70, 197, 32, 31, 176, 14, 71, 252, 120, 207, 34, 229, 187, 182, 191, 54, 0, 1, 230, 96, 24, 18, 213, 192, 100, 70, 58, 213, 78, 59, 53, 17, 217, 163, 54, 112, 232, 23, 160, 220, 235, 179, 37, 49, 79, 190, 82, 213, 203, 190, 225, 212, 81, 68, 83, 229, 207, 194, 138, 138, 24, 32, 234, 212, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 204, 16, 0, 0, 72, 83, 124, 99, 30, 47, 37, 1, 239, 147, 205, 111, 190, 223, 244, 63, 24, 79, 122, 117, 181, 21, 208, 192, 202, 124, 19, 125, 86, 114, 223, 16, 236, 95, 243, 28, 111, 185, 165, 202, 147, 55, 87, 91, 132, 48, 117, 139, 38, 49, 119, 42, 244, 99, 183, 237, 161, 13, 98, 224, 148, 222, 245, 106, 142, 188, 151, 224, 189, 139, 21, 116, 87, 28, 157, 151, 225, 226, 250, 218, 147, 80, 231, 144, 252, 223, 62, 103, 176, 31, 51, 101, 181, 44, 82, 180, 72, 148, 151, 10, 88, 144, 81, 87, 230, 135, 174, 102, 165, 143, 241, 229, 60, 148, 151, 208, 187, 151, 100, 64, 82, 171, 52, 1, 40, 143, 31, 160, 6, 0, 70, 16, 0, 0, 4, 4, 25, 27, 4, 255, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 231, 0, 0, 0, 0, 0, 0, 0, 229, 163, 167, 181, 216, 48, 194, 149, 59, 152, 83, 76, 108, 89, 163, 163, 79, 220, 52, 233, 51, 247, 245, 137, 143, 10, 133, 207, 8, 132, 107, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 158, 42, 124, 111, 148, 143, 23, 71, 78, 52, 167, 252, 67, 237, 3, 15, 124, 21, 99, 241, 186, 189, 223, 99, 64, 200, 46, 14, 84, 168, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215, 98, 66, 167, 203, 62, 30, 139, 2, 195, 192, 57, 51, 40, 206, 207, 77, 159, 207, 61, 144, 77, 52, 130, 175, 199, 184, 139, 101, 54, 6, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 43, 224, 72, 222, 235, 58, 201, 147, 41, 73, 102, 159, 167, 17, 139, 183, 255, 92, 219, 174, 234, 152, 2, 188, 66, 241, 7, 248, 149, 104, 181, 57, 202, 96, 91, 236, 128, 245, 224, 94, 102, 1, 108, 69, 117, 32, 200, 63, 232, 4, 154, 218, 62, 108, 62, 37, 222, 11, 49, 235, 195, 58, 169, 32, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 5, 0, 94, 14, 0, 0, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 69, 56, 84, 67, 67, 66, 74, 97, 103, 65, 119, 73, 66, 65, 103, 73, 85, 102, 50, 83, 98, 121, 119, 107, 77, 86, 84, 74, 75, 85, 53, 55, 47, 74, 119, 66, 112, 56, 69, 100, 104, 80, 48, 52, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 99, 68, 69, 105, 77, 67, 65, 71, 65, 49, 85, 69, 65, 119, 119, 90, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 66, 68, 83, 121, 66, 81, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 66, 68, 81, 84, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 67, 103, 119, 82, 10, 83, 87, 53, 48, 90, 87, 119, 103, 81, 50, 57, 121, 99, 71, 57, 121, 89, 88, 82, 112, 98, 50, 52, 120, 70, 68, 65, 83, 66, 103, 78, 86, 66, 65, 99, 77, 67, 49, 78, 104, 98, 110, 82, 104, 73, 69, 78, 115, 89, 88, 74, 104, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 73, 10, 68, 65, 74, 68, 81, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 66, 104, 77, 67, 86, 86, 77, 119, 72, 104, 99, 78, 77, 106, 85, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 87, 104, 99, 78, 77, 122, 73, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 10, 87, 106, 66, 119, 77, 83, 73, 119, 73, 65, 89, 68, 86, 81, 81, 68, 68, 66, 108, 74, 98, 110, 82, 108, 98, 67, 66, 84, 82, 49, 103, 103, 85, 69, 78, 76, 73, 69, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 10, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 10, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 66, 90, 77, 66, 77, 71, 66, 121, 113, 71, 83, 77, 52, 57, 65, 103, 69, 71, 67, 67, 113, 71, 83, 77, 52, 57, 65, 119, 69, 72, 65, 48, 73, 65, 66, 71, 112, 118, 10, 48, 89, 117, 89, 114, 113, 65, 117, 83, 75, 66, 122, 75, 108, 117, 98, 54, 109, 76, 43, 114, 118, 102, 68, 53, 65, 106, 89, 79, 51, 81, 78, 103, 102, 87, 122, 116, 103, 52, 101, 109, 49, 69, 71, 66, 86, 107, 71, 108, 87, 118, 100, 117, 66, 48, 88, 81, 83, 69, 47, 115, 120, 71, 68, 10, 109, 83, 118, 75, 111, 57, 116, 51, 67, 114, 79, 80, 67, 52, 83, 85, 54, 88, 54, 106, 103, 103, 77, 77, 77, 73, 73, 68, 67, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 83, 86, 98, 49, 51, 78, 118, 82, 118, 104, 54, 85, 66, 74, 121, 100, 84, 48, 10, 77, 56, 52, 66, 86, 119, 118, 101, 86, 68, 66, 114, 66, 103, 78, 86, 72, 82, 56, 69, 90, 68, 66, 105, 77, 71, 67, 103, 88, 113, 66, 99, 104, 108, 112, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 70, 119, 97, 83, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 51, 78, 110, 101, 67, 57, 106, 90, 88, 74, 48, 97, 87, 90, 112, 89, 50, 70, 48, 97, 87, 57, 117, 76, 51, 89, 48, 76, 51, 66, 106, 97, 50, 78, 121, 98, 68, 57, 106, 89, 84, 49, 119, 10, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 90, 108, 98, 109, 78, 118, 90, 71, 108, 117, 90, 122, 49, 107, 90, 88, 73, 119, 72, 81, 89, 68, 86, 82, 48, 79, 66, 66, 89, 69, 70, 71, 51, 110, 54, 83, 43, 75, 120, 78, 54, 116, 43, 72, 73, 56, 71, 112, 57, 54, 80, 107, 117, 90, 10, 105, 87, 115, 90, 77, 65, 52, 71, 65, 49, 85, 100, 68, 119, 69, 66, 47, 119, 81, 69, 65, 119, 73, 71, 119, 68, 65, 77, 66, 103, 78, 86, 72, 82, 77, 66, 65, 102, 56, 69, 65, 106, 65, 65, 77, 73, 73, 67, 79, 81, 89, 74, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 10, 66, 73, 73, 67, 75, 106, 67, 67, 65, 105, 89, 119, 72, 103, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 65, 81, 81, 81, 48, 103, 106, 102, 115, 81, 65, 106, 82, 113, 52, 98, 116, 79, 56, 113, 80, 65, 86, 83, 107, 106, 67, 67, 65, 87, 77, 71, 67, 105, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 119, 103, 103, 70, 84, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 66, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 67, 65, 103, 69, 69, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 68, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 69, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 70, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 71, 65, 103, 69, 66, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 72, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 73, 65, 103, 69, 70, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 74, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 75, 65, 103, 69, 65, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 76, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 77, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 78, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 79, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 80, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 81, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 82, 65, 103, 69, 76, 77, 66, 56, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 83, 66, 66, 65, 69, 10, 66, 65, 73, 67, 66, 65, 69, 65, 66, 81, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 77, 66, 65, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 77, 69, 65, 103, 65, 65, 77, 66, 81, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 81, 69, 10, 66, 114, 68, 65, 98, 119, 65, 65, 65, 68, 65, 80, 66, 103, 111, 113, 104, 107, 105, 71, 43, 69, 48, 66, 68, 81, 69, 70, 67, 103, 69, 66, 77, 66, 52, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 89, 69, 69, 68, 97, 57, 104, 116, 65, 56, 65, 74, 47, 90, 10, 50, 70, 109, 97, 76, 53, 74, 113, 47, 75, 69, 119, 82, 65, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 66, 122, 65, 50, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 66, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 67, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 68, 65, 81, 72, 47, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 65, 48, 107, 65, 77, 69, 89, 67, 10, 73, 81, 67, 70, 71, 49, 89, 65, 98, 51, 101, 88, 70, 116, 101, 56, 53, 51, 67, 108, 86, 66, 110, 104, 108, 67, 102, 68, 121, 99, 53, 55, 50, 90, 88, 69, 113, 97, 120, 52, 85, 99, 99, 83, 97, 119, 73, 104, 65, 79, 110, 48, 86, 78, 75, 84, 90, 109, 65, 120, 85, 70, 52, 110, 10, 119, 82, 107, 83, 70, 104, 52, 113, 70, 74, 51, 97, 85, 108, 122, 70, 111, 80, 81, 84, 51, 120, 73, 102, 55, 107, 70, 68, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 108, 106, 67, 67, 65, 106, 50, 103, 65, 119, 73, 66, 65, 103, 73, 86, 65, 74, 86, 118, 88, 99, 50, 57, 71, 43, 72, 112, 81, 69, 110, 74, 49, 80, 81, 122, 122, 103, 70, 88, 67, 57, 53, 85, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 10, 77, 71, 103, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 77, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 83, 98, 50, 57, 48, 73, 69, 78, 66, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 10, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 10, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 65, 101, 70, 119, 48, 120, 79, 68, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 70, 119, 48, 122, 77, 122, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 77, 72, 65, 120, 73, 106, 65, 103, 10, 66, 103, 78, 86, 66, 65, 77, 77, 71, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 81, 81, 48, 115, 103, 85, 71, 120, 104, 100, 71, 90, 118, 99, 109, 48, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 10, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 10, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 78, 83, 66, 47, 55, 116, 50, 49, 108, 88, 83, 79, 10, 50, 67, 117, 122, 112, 120, 119, 55, 52, 101, 74, 66, 55, 50, 69, 121, 68, 71, 103, 87, 53, 114, 88, 67, 116, 120, 50, 116, 86, 84, 76, 113, 54, 104, 75, 107, 54, 122, 43, 85, 105, 82, 90, 67, 110, 113, 82, 55, 112, 115, 79, 118, 103, 113, 70, 101, 83, 120, 108, 109, 84, 108, 74, 108, 10, 101, 84, 109, 105, 50, 87, 89, 122, 51, 113, 79, 66, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 10, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 108, 87, 57, 100, 10, 122, 98, 48, 98, 52, 101, 108, 65, 83, 99, 110, 85, 57, 68, 80, 79, 65, 86, 99, 76, 51, 108, 81, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 10, 65, 102, 56, 67, 65, 81, 65, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 82, 119, 65, 119, 82, 65, 73, 103, 88, 115, 86, 107, 105, 48, 119, 43, 105, 54, 86, 89, 71, 87, 51, 85, 70, 47, 50, 50, 117, 97, 88, 101, 48, 89, 74, 68, 106, 49, 85, 101, 10, 110, 65, 43, 84, 106, 68, 49, 97, 105, 53, 99, 67, 73, 67, 89, 98, 49, 83, 65, 109, 68, 53, 120, 107, 102, 84, 86, 112, 118, 111, 52, 85, 111, 121, 105, 83, 89, 120, 114, 68, 87, 76, 109, 85, 82, 52, 67, 73, 57, 78, 75, 121, 102, 80, 78, 43, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 106, 122, 67, 67, 65, 106, 83, 103, 65, 119, 73, 66, 65, 103, 73, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 97, 68, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 10, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 10, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 66, 52, 88, 68, 84, 69, 52, 77, 68, 85, 121, 77, 84, 69, 119, 78, 68, 85, 120, 77, 70, 111, 88, 68, 84, 81, 53, 77, 84, 73, 122, 77, 84, 73, 122, 78, 84, 107, 49, 79, 86, 111, 119, 97, 68, 69, 97, 77, 66, 103, 71, 10, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 10, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 10, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 67, 54, 110, 69, 119, 77, 68, 73, 89, 90, 79, 106, 47, 105, 80, 87, 115, 67, 122, 97, 69, 75, 105, 55, 10, 49, 79, 105, 79, 83, 76, 82, 70, 104, 87, 71, 106, 98, 110, 66, 86, 74, 102, 86, 110, 107, 89, 52, 117, 51, 73, 106, 107, 68, 89, 89, 76, 48, 77, 120, 79, 52, 109, 113, 115, 121, 89, 106, 108, 66, 97, 108, 84, 86, 89, 120, 70, 80, 50, 115, 74, 66, 75, 53, 122, 108, 75, 79, 66, 10, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 10, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 10, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 10, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 65, 102, 56, 67, 65, 81, 69, 119, 67, 103, 89, 73, 10, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 83, 81, 65, 119, 82, 103, 73, 104, 65, 79, 87, 47, 53, 81, 107, 82, 43, 83, 57, 67, 105, 83, 68, 99, 78, 111, 111, 119, 76, 117, 80, 82, 76, 115, 87, 71, 102, 47, 89, 105, 55, 71, 83, 88, 57, 52, 66, 103, 119, 84, 119, 103, 10, 65, 105, 69, 65, 52, 74, 48, 108, 114, 72, 111, 77, 115, 43, 88, 111, 53, 111, 47, 115, 88, 54, 79, 57, 81, 87, 120, 72, 82, 65, 118, 90, 85, 71, 79, 100, 82, 81, 55, 99, 118, 113, 82, 88, 97, 113, 73, 61, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "collateral": { "pck_crl_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC\nMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD\nb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw\nCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg\nBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs\nIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex\nCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO\n2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl\neTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS\nBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d\nzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue\nnA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "root_ca_crl": "308201203081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3235303332303131323135375a170d3236303430333131323135375aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020347003044022030c9fce1438da0a94e4fffdd46c9650e393be6e5a7862d4e4e73527932d04af302206539efe3f734c3d7df20d9dfc4630e1c7ff0439a0f8ece101f15b5eaff9b4f33", - "pck_crl": "30820d1730820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232313133343330335a170d3236303332333133343330335a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303232313133343330335a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303232313133343330335a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303232313133343330335a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303232313133343330335a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303232313133343330335a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303232313133343330335a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303232313133343330335a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303232313133343330335a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303232313133343330335a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303232313133343330335a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303232313133343330335a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303232313133343330335a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303232313133343330335a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303232313133343330335a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303232313133343330335a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303232313133343330335a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303232313133343330335a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303232313133343330335a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303232313133343330335a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303232313133343330335a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303232313133343330335a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303232313133343330335a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303232313133343330335a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303232313133343330335a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303232313133343330335a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303232313133343330335a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303232313133343330335a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303232313133343330335a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303232313133343330335a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303232313133343330335a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303232313133343330335a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303232313133343330335a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303232313133343330335a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303232313133343330335a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303232313133343330335a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303232313133343330335a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303232313133343330335a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303232313133343330335a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303232313133343330335a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303232313133343330335a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303232313133343330335a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d040302034800304502203bd1f94fcc12868b9760bcf134ce34b844713b667fcb9e4d2207fc0cd81566de022100ef021d2ca3ff8bd81861b3a3722f955703b69f5133cfe5fea3d4a1098922f103", + "root_ca_crl": "308201223081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232363133303430305a170d3237303232363133303430305aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020349003046022100c252ed59c795ba2b11496a4a99758bb8cbc380a1ebbb0865be69f2c4b38bb6400221009a7d8b03602a9ee2d62322d759166d6933d24d9dfa01ab3fde4520691d715bd7", + "pck_crl": "30820d1830820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303331383136303130355a170d3236303431373136303130355a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303331383136303130355a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303331383136303130355a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303331383136303130355a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303331383136303130355a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303331383136303130355a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303331383136303130355a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303331383136303130355a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303331383136303130355a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303331383136303130355a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303331383136303130355a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303331383136303130355a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303331383136303130355a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303331383136303130355a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303331383136303130355a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303331383136303130355a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303331383136303130355a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303331383136303130355a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303331383136303130355a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303331383136303130355a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303331383136303130355a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303331383136303130355a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303331383136303130355a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303331383136303130355a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303331383136303130355a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303331383136303130355a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303331383136303130355a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303331383136303130355a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303331383136303130355a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303331383136303130355a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303331383136303130355a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303331383136303130355a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303331383136303130355a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303331383136303130355a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303331383136303130355a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303331383136303130355a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303331383136303130355a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303331383136303130355a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303331383136303130355a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303331383136303130355a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303331383136303130355a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303331383136303130355a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d0403020349003046022100c71b1e53814b3437773403491a1e10a53043880e98868ee957bfa044031c3bf3022100d12d92717d516bd91b8a0c275fce3dc886f4cbc8a2300275fcf01a78691eeefd", "tcb_info_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-02-21T13:31:57Z\",\"nextUpdate\":\"2026-03-23T13:31:57Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", - "tcb_info_signature": "6cb0f2b2d37350c8d25f478cac1b5a341cc1d6deb8379f0aa5c8708ebf00fbd7fb39b146ebd086593f7b86f37c8cb82a0e3df178857625ef95d64f636167c5eb", + "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-03-18T15:43:27Z\",\"nextUpdate\":\"2026-04-17T15:43:27Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", + "tcb_info_signature": "77bd013b1fbb1162604d1b76e2ead05315b61963d15c34e6c8dfbec009930dcaa55e2026eb3befad7df463210a85c392d9b77caf72c76e4ec03f02c71855a95c", "qe_identity_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-02-21T13:43:47Z\",\"nextUpdate\":\"2026-03-23T13:43:47Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", - "qe_identity_signature": "8d31db300d8fbd61c3525f177a5963ef729139e358b9b572822f7a72812d3eff83fc06a97b0cc9d921a3a12ad63f547266aff1629b9bae4ec31d69305bdb6c52" + "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-03-18T15:16:33Z\",\"nextUpdate\":\"2026-04-17T15:16:33Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", + "qe_identity_signature": "a0c628117e0d40c9b2682dff38cd21283c70bc319546e3a05c530458c8a532f3bc67d5c66196dd012f9fe8490da0648b9ec0bbeb43ed48662a969871567abec4" }, "tcb_info": { "mrtd": "f06dfda6dce1cf904d4e2bab1dc370634cf95cefa2ceb2de2eee127c9382698090d7a4a13e14c536ec6c9c3c8fa87077", "rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46", "rtmr1": "a7b523278d4f914ee8df0ec80cd1c3d498cbf1152b0c5eaf65bad9425072874a3fcf891e8b01713d3d9937e3e0d26c15", "rtmr2": "dbf4924c07f5066f3dc6859844184344306aa3263817153dcaee85af97d23e0c0b96efe0731d8865a8747e51b9e351ac", - "rtmr3": "c91f84e3b6bc947589700233f8e500d9c37b2d79738389ef61d1d18184f50bacb20b98ba29d32dfa1a933b11f6d76655", + "rtmr3": "da0a4418d22057f984734c8b122fc41e062ebf0724cba2555656ec9049e573ad46c5201fb00e47fc78cf22e5bbb6bf36", "os_image_hash": "", - "compose_hash": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48", + "compose_hash": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14", "device_id": "7a82191bd4dedb9d716e3aa422963cf1009f36e3068404a0322feca1ce517dc9", - "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1771748631\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45\\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-local-node0-testnet-tee\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316\\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, @@ -228,16 +179,16 @@ { "imr": 3, "event_type": 134217729, - "digest": "749f2448e65b5beae9ee03fcbcf0debb84bddb024c5d7859d98c8d25a5361630b13653e0c0feba4b172a83c33d967ebc", + "digest": "40c2b270b217916105b5d8502aeaa68f451b8e23d4c9d284a027b790168fc2d7db5d91ea0948a1aaf0618d7b2f982112", "event": "app-id", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f6" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0df" }, { "imr": 3, "event_type": 134217729, - "digest": "d322210028255abc72dc77012047a9d497cc0252d0f07f86d98290675ca66abfb9afc78785c6038357426af4e7dfd26f", + "digest": "2e7f3eddf530256b99ca3b3dea6c7ff3bbdcb251a4228d4f54d04b05277d6a46ec04bdf08aac771f2579e07271f22192", "event": "compose-hash", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14" }, { "imr": 3, @@ -270,9 +221,9 @@ { "imr": 3, "event_type": 134217729, - "digest": "a098bd7250ced8cc8340120c5452dcab7297a994c102b5c40c8e1c0ba2ba53d3b3c4342298d34e5587f753d750a9faad", + "digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678", "event": "mpc-image-digest", - "event_payload": "6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45" + "event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980" } ] } diff --git a/crates/test-utils/assets/quote.json b/crates/test-utils/assets/quote.json index 887cc7852..1886ba77e 100644 --- a/crates/test-utils/assets/quote.json +++ b/crates/test-utils/assets/quote.json @@ -1 +1 @@ -[4,0,2,0,129,0,0,0,0,0,0,0,147,154,114,51,247,156,76,169,148,10,13,179,149,127,6,7,61,153,138,108,16,87,107,253,246,246,237,142,155,133,233,50,0,0,0,0,11,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,123,240,99,40,14,148,251,5,31,93,215,177,252,89,206,154,172,66,187,150,29,248,212,75,112,156,155,15,248,122,123,77,246,72,101,123,166,209,24,149,137,254,171,29,90,60,154,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,231,2,6,0,0,0,0,0,240,109,253,166,220,225,207,144,77,78,43,171,29,195,112,99,76,249,92,239,162,206,178,222,46,238,18,124,147,130,105,128,144,215,164,161,62,20,197,54,236,108,156,60,143,168,112,119,1,110,105,242,11,52,142,61,104,116,87,244,229,124,63,254,60,251,113,56,246,146,168,139,218,191,234,199,146,253,217,141,72,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,230,115,190,47,112,190,239,183,11,72,166,16,158,237,71,21,215,39,13,70,131,179,191,53,111,162,95,175,191,26,167,110,57,233,18,126,110,104,140,205,169,139,218,177,212,212,127,70,167,181,35,39,141,79,145,78,232,223,14,200,12,209,195,212,152,203,241,21,43,12,94,175,101,186,217,66,80,114,135,74,63,207,137,30,139,1,113,61,61,153,55,227,224,210,108,21,219,244,146,76,7,245,6,111,61,198,133,152,68,24,67,68,48,106,163,38,56,23,21,61,202,238,133,175,151,210,62,12,11,150,239,224,115,29,136,101,168,116,126,81,185,227,81,172,201,31,132,227,182,188,148,117,137,112,2,51,248,229,0,217,195,123,45,121,115,131,137,239,97,209,209,129,132,245,11,172,178,11,152,186,41,211,45,250,26,147,59,17,246,215,102,85,0,1,195,223,53,227,107,60,23,144,43,16,55,239,206,246,210,246,13,185,162,204,96,156,224,219,17,240,182,48,192,132,93,226,121,108,93,157,145,44,64,158,145,229,42,58,76,114,127,244,0,0,0,0,0,0,0,0,0,0,0,0,0,0,204,16,0,0,10,124,154,105,114,192,236,4,179,208,82,53,162,157,20,21,152,133,71,85,85,21,59,229,138,60,128,119,31,41,74,151,97,88,211,84,31,214,163,180,43,240,136,29,116,231,138,255,148,105,65,17,10,5,221,116,226,119,8,76,218,67,115,16,142,188,151,224,189,139,21,116,87,28,157,151,225,226,250,218,147,80,231,144,252,223,62,103,176,31,51,101,181,44,82,180,72,148,151,10,88,144,81,87,230,135,174,102,165,143,241,229,60,148,151,208,187,151,100,64,82,171,52,1,40,143,31,160,6,0,70,16,0,0,4,4,25,27,4,255,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,0,0,0,0,0,0,0,231,0,0,0,0,0,0,0,229,163,167,181,216,48,194,149,59,152,83,76,108,89,163,163,79,220,52,233,51,247,245,137,143,10,133,207,8,132,107,202,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,158,42,124,111,148,143,23,71,78,52,167,252,67,237,3,15,124,21,99,241,186,189,223,99,64,200,46,14,84,168,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,98,66,167,203,62,30,139,2,195,192,57,51,40,206,207,77,159,207,61,144,77,52,130,175,199,184,139,101,54,6,152,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180,243,159,196,112,17,166,99,200,182,156,136,179,201,15,15,47,98,23,37,24,68,126,153,62,34,18,139,203,16,236,185,233,46,116,134,201,84,179,122,113,148,250,173,226,9,104,70,208,7,5,223,11,239,88,182,207,12,115,185,54,184,154,234,32,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,5,0,94,14,0,0,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,69,56,84,67,67,66,74,97,103,65,119,73,66,65,103,73,85,102,50,83,98,121,119,107,77,86,84,74,75,85,53,55,47,74,119,66,112,56,69,100,104,80,48,52,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,99,68,69,105,77,67,65,71,65,49,85,69,65,119,119,90,83,87,53,48,90,87,119,103,85,48,100,89,73,70,66,68,83,121,66,81,98,71,70,48,90,109,57,121,98,83,66,68,81,84,69,97,77,66,103,71,65,49,85,69,67,103,119,82,10,83,87,53,48,90,87,119,103,81,50,57,121,99,71,57,121,89,88,82,112,98,50,52,120,70,68,65,83,66,103,78,86,66,65,99,77,67,49,78,104,98,110,82,104,73,69,78,115,89,88,74,104,77,81,115,119,67,81,89,68,86,81,81,73,10,68,65,74,68,81,84,69,76,77,65,107,71,65,49,85,69,66,104,77,67,86,86,77,119,72,104,99,78,77,106,85,120,77,84,65,50,77,68,99,122,78,122,77,48,87,104,99,78,77,122,73,120,77,84,65,50,77,68,99,122,78,122,77,48,10,87,106,66,119,77,83,73,119,73,65,89,68,86,81,81,68,68,66,108,74,98,110,82,108,98,67,66,84,82,49,103,103,85,69,78,76,73,69,78,108,99,110,82,112,90,109,108,106,89,88,82,108,77,82,111,119,71,65,89,68,86,81,81,75,10,68,66,70,74,98,110,82,108,98,67,66,68,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,10,66,65,103,77,65,107,78,66,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,86,85,122,66,90,77,66,77,71,66,121,113,71,83,77,52,57,65,103,69,71,67,67,113,71,83,77,52,57,65,119,69,72,65,48,73,65,66,71,112,118,10,48,89,117,89,114,113,65,117,83,75,66,122,75,108,117,98,54,109,76,43,114,118,102,68,53,65,106,89,79,51,81,78,103,102,87,122,116,103,52,101,109,49,69,71,66,86,107,71,108,87,118,100,117,66,48,88,81,83,69,47,115,120,71,68,10,109,83,118,75,111,57,116,51,67,114,79,80,67,52,83,85,54,88,54,106,103,103,77,77,77,73,73,68,67,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,83,86,98,49,51,78,118,82,118,104,54,85,66,74,121,100,84,48,10,77,56,52,66,86,119,118,101,86,68,66,114,66,103,78,86,72,82,56,69,90,68,66,105,77,71,67,103,88,113,66,99,104,108,112,111,100,72,82,119,99,122,111,118,76,50,70,119,97,83,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,51,78,110,101,67,57,106,90,88,74,48,97,87,90,112,89,50,70,48,97,87,57,117,76,51,89,48,76,51,66,106,97,50,78,121,98,68,57,106,89,84,49,119,10,98,71,70,48,90,109,57,121,98,83,90,108,98,109,78,118,90,71,108,117,90,122,49,107,90,88,73,119,72,81,89,68,86,82,48,79,66,66,89,69,70,71,51,110,54,83,43,75,120,78,54,116,43,72,73,56,71,112,57,54,80,107,117,90,10,105,87,115,90,77,65,52,71,65,49,85,100,68,119,69,66,47,119,81,69,65,119,73,71,119,68,65,77,66,103,78,86,72,82,77,66,65,102,56,69,65,106,65,65,77,73,73,67,79,81,89,74,75,111,90,73,104,118,104,78,65,81,48,66,10,66,73,73,67,75,106,67,67,65,105,89,119,72,103,89,75,75,111,90,73,104,118,104,78,65,81,48,66,65,81,81,81,48,103,106,102,115,81,65,106,82,113,52,98,116,79,56,113,80,65,86,83,107,106,67,67,65,87,77,71,67,105,113,71,10,83,73,98,52,84,81,69,78,65,81,73,119,103,103,70,84,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,66,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,67,65,103,69,69,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,68,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,69,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,70,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,71,65,103,69,66,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,72,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,73,65,103,69,70,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,74,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,75,65,103,69,65,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,76,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,77,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,78,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,79,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,80,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,81,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,82,65,103,69,76,77,66,56,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,83,66,66,65,69,10,66,65,73,67,66,65,69,65,66,81,65,65,65,65,65,65,65,65,65,65,77,66,65,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,77,69,65,103,65,65,77,66,81,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,81,69,10,66,114,68,65,98,119,65,65,65,68,65,80,66,103,111,113,104,107,105,71,43,69,48,66,68,81,69,70,67,103,69,66,77,66,52,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,89,69,69,68,97,57,104,116,65,56,65,74,47,90,10,50,70,109,97,76,53,74,113,47,75,69,119,82,65,89,75,75,111,90,73,104,118,104,78,65,81,48,66,66,122,65,50,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,66,65,81,72,47,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,99,67,65,81,72,47,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,68,65,81,72,47,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,65,48,107,65,77,69,89,67,10,73,81,67,70,71,49,89,65,98,51,101,88,70,116,101,56,53,51,67,108,86,66,110,104,108,67,102,68,121,99,53,55,50,90,88,69,113,97,120,52,85,99,99,83,97,119,73,104,65,79,110,48,86,78,75,84,90,109,65,120,85,70,52,110,10,119,82,107,83,70,104,52,113,70,74,51,97,85,108,122,70,111,80,81,84,51,120,73,102,55,107,70,68,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,108,106,67,67,65,106,50,103,65,119,73,66,65,103,73,86,65,74,86,118,88,99,50,57,71,43,72,112,81,69,110,74,49,80,81,122,122,103,70,88,67,57,53,85,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,10,77,71,103,120,71,106,65,89,66,103,78,86,66,65,77,77,69,85,108,117,100,71,86,115,73,70,78,72,87,67,66,83,98,50,57,48,73,69,78,66,77,82,111,119,71,65,89,68,86,81,81,75,68,66,70,74,98,110,82,108,98,67,66,68,10,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,66,65,103,77,65,107,78,66,77,81,115,119,10,67,81,89,68,86,81,81,71,69,119,74,86,85,122,65,101,70,119,48,120,79,68,65,49,77,106,69,120,77,68,85,119,77,84,66,97,70,119,48,122,77,122,65,49,77,106,69,120,77,68,85,119,77,84,66,97,77,72,65,120,73,106,65,103,10,66,103,78,86,66,65,77,77,71,85,108,117,100,71,86,115,73,70,78,72,87,67,66,81,81,48,115,103,85,71,120,104,100,71,90,118,99,109,48,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,10,73,69,78,118,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,10,67,122,65,74,66,103,78,86,66,65,89,84,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,78,83,66,47,55,116,50,49,108,88,83,79,10,50,67,117,122,112,120,119,55,52,101,74,66,55,50,69,121,68,71,103,87,53,114,88,67,116,120,50,116,86,84,76,113,54,104,75,107,54,122,43,85,105,82,90,67,110,113,82,55,112,115,79,118,103,113,70,101,83,120,108,109,84,108,74,108,10,101,84,109,105,50,87,89,122,51,113,79,66,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,10,66,103,78,86,72,82,56,69,83,122,66,74,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,108,87,57,100,10,122,98,48,98,52,101,108,65,83,99,110,85,57,68,80,79,65,86,99,76,51,108,81,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,10,65,102,56,67,65,81,65,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,68,82,119,65,119,82,65,73,103,88,115,86,107,105,48,119,43,105,54,86,89,71,87,51,85,70,47,50,50,117,97,88,101,48,89,74,68,106,49,85,101,10,110,65,43,84,106,68,49,97,105,53,99,67,73,67,89,98,49,83,65,109,68,53,120,107,102,84,86,112,118,111,52,85,111,121,105,83,89,120,114,68,87,76,109,85,82,52,67,73,57,78,75,121,102,80,78,43,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,106,122,67,67,65,106,83,103,65,119,73,66,65,103,73,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,85,114,57,81,71,122,107,110,66,113,119,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,97,68,69,97,77,66,103,71,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,10,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,10,66,103,78,86,66,65,89,84,65,108,86,84,77,66,52,88,68,84,69,52,77,68,85,121,77,84,69,119,78,68,85,120,77,70,111,88,68,84,81,53,77,84,73,122,77,84,73,122,78,84,107,49,79,86,111,119,97,68,69,97,77,66,103,71,10,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,99,110,66,118,99,109,70,48,10,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,66,103,78,86,66,65,89,84,10,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,67,54,110,69,119,77,68,73,89,90,79,106,47,105,80,87,115,67,122,97,69,75,105,55,10,49,79,105,79,83,76,82,70,104,87,71,106,98,110,66,86,74,102,86,110,107,89,52,117,51,73,106,107,68,89,89,76,48,77,120,79,52,109,113,115,121,89,106,108,66,97,108,84,86,89,120,70,80,50,115,74,66,75,53,122,108,75,79,66,10,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,66,103,78,86,72,82,56,69,83,122,66,74,10,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,100,109,108,106,90,88,77,117,97,87,53,48,10,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,10,85,114,57,81,71,122,107,110,66,113,119,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,65,102,56,67,65,81,69,119,67,103,89,73,10,75,111,90,73,122,106,48,69,65,119,73,68,83,81,65,119,82,103,73,104,65,79,87,47,53,81,107,82,43,83,57,67,105,83,68,99,78,111,111,119,76,117,80,82,76,115,87,71,102,47,89,105,55,71,83,88,57,52,66,103,119,84,119,103,10,65,105,69,65,52,74,48,108,114,72,111,77,115,43,88,111,53,111,47,115,88,54,79,57,81,87,120,72,82,65,118,90,85,71,79,100,82,81,55,99,118,113,82,88,97,113,73,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] +[4,0,2,0,129,0,0,0,0,0,0,0,147,154,114,51,247,156,76,169,148,10,13,179,149,127,6,7,61,153,138,108,16,87,107,253,246,246,237,142,155,133,233,50,0,0,0,0,11,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,123,240,99,40,14,148,251,5,31,93,215,177,252,89,206,154,172,66,187,150,29,248,212,75,112,156,155,15,248,122,123,77,246,72,101,123,166,209,24,149,137,254,171,29,90,60,154,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,231,2,6,0,0,0,0,0,240,109,253,166,220,225,207,144,77,78,43,171,29,195,112,99,76,249,92,239,162,206,178,222,46,238,18,124,147,130,105,128,144,215,164,161,62,20,197,54,236,108,156,60,143,168,112,119,1,83,236,221,225,0,154,247,12,125,232,120,198,48,97,92,131,215,76,192,223,245,128,116,216,4,194,52,38,59,232,158,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,230,115,190,47,112,190,239,183,11,72,166,16,158,237,71,21,215,39,13,70,131,179,191,53,111,162,95,175,191,26,167,110,57,233,18,126,110,104,140,205,169,139,218,177,212,212,127,70,167,181,35,39,141,79,145,78,232,223,14,200,12,209,195,212,152,203,241,21,43,12,94,175,101,186,217,66,80,114,135,74,63,207,137,30,139,1,113,61,61,153,55,227,224,210,108,21,219,244,146,76,7,245,6,111,61,198,133,152,68,24,67,68,48,106,163,38,56,23,21,61,202,238,133,175,151,210,62,12,11,150,239,224,115,29,136,101,168,116,126,81,185,227,81,172,218,10,68,24,210,32,87,249,132,115,76,139,18,47,196,30,6,46,191,7,36,203,162,85,86,86,236,144,73,229,115,173,70,197,32,31,176,14,71,252,120,207,34,229,187,182,191,54,0,1,230,96,24,18,213,192,100,70,58,213,78,59,53,17,217,163,54,112,232,23,160,220,235,179,37,49,79,190,82,213,203,190,225,212,81,68,83,229,207,194,138,138,24,32,234,212,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,204,16,0,0,72,83,124,99,30,47,37,1,239,147,205,111,190,223,244,63,24,79,122,117,181,21,208,192,202,124,19,125,86,114,223,16,236,95,243,28,111,185,165,202,147,55,87,91,132,48,117,139,38,49,119,42,244,99,183,237,161,13,98,224,148,222,245,106,142,188,151,224,189,139,21,116,87,28,157,151,225,226,250,218,147,80,231,144,252,223,62,103,176,31,51,101,181,44,82,180,72,148,151,10,88,144,81,87,230,135,174,102,165,143,241,229,60,148,151,208,187,151,100,64,82,171,52,1,40,143,31,160,6,0,70,16,0,0,4,4,25,27,4,255,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,0,0,0,0,0,0,0,231,0,0,0,0,0,0,0,229,163,167,181,216,48,194,149,59,152,83,76,108,89,163,163,79,220,52,233,51,247,245,137,143,10,133,207,8,132,107,202,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,158,42,124,111,148,143,23,71,78,52,167,252,67,237,3,15,124,21,99,241,186,189,223,99,64,200,46,14,84,168,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,98,66,167,203,62,30,139,2,195,192,57,51,40,206,207,77,159,207,61,144,77,52,130,175,199,184,139,101,54,6,152,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,150,43,224,72,222,235,58,201,147,41,73,102,159,167,17,139,183,255,92,219,174,234,152,2,188,66,241,7,248,149,104,181,57,202,96,91,236,128,245,224,94,102,1,108,69,117,32,200,63,232,4,154,218,62,108,62,37,222,11,49,235,195,58,169,32,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,5,0,94,14,0,0,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,69,56,84,67,67,66,74,97,103,65,119,73,66,65,103,73,85,102,50,83,98,121,119,107,77,86,84,74,75,85,53,55,47,74,119,66,112,56,69,100,104,80,48,52,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,99,68,69,105,77,67,65,71,65,49,85,69,65,119,119,90,83,87,53,48,90,87,119,103,85,48,100,89,73,70,66,68,83,121,66,81,98,71,70,48,90,109,57,121,98,83,66,68,81,84,69,97,77,66,103,71,65,49,85,69,67,103,119,82,10,83,87,53,48,90,87,119,103,81,50,57,121,99,71,57,121,89,88,82,112,98,50,52,120,70,68,65,83,66,103,78,86,66,65,99,77,67,49,78,104,98,110,82,104,73,69,78,115,89,88,74,104,77,81,115,119,67,81,89,68,86,81,81,73,10,68,65,74,68,81,84,69,76,77,65,107,71,65,49,85,69,66,104,77,67,86,86,77,119,72,104,99,78,77,106,85,120,77,84,65,50,77,68,99,122,78,122,77,48,87,104,99,78,77,122,73,120,77,84,65,50,77,68,99,122,78,122,77,48,10,87,106,66,119,77,83,73,119,73,65,89,68,86,81,81,68,68,66,108,74,98,110,82,108,98,67,66,84,82,49,103,103,85,69,78,76,73,69,78,108,99,110,82,112,90,109,108,106,89,88,82,108,77,82,111,119,71,65,89,68,86,81,81,75,10,68,66,70,74,98,110,82,108,98,67,66,68,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,10,66,65,103,77,65,107,78,66,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,86,85,122,66,90,77,66,77,71,66,121,113,71,83,77,52,57,65,103,69,71,67,67,113,71,83,77,52,57,65,119,69,72,65,48,73,65,66,71,112,118,10,48,89,117,89,114,113,65,117,83,75,66,122,75,108,117,98,54,109,76,43,114,118,102,68,53,65,106,89,79,51,81,78,103,102,87,122,116,103,52,101,109,49,69,71,66,86,107,71,108,87,118,100,117,66,48,88,81,83,69,47,115,120,71,68,10,109,83,118,75,111,57,116,51,67,114,79,80,67,52,83,85,54,88,54,106,103,103,77,77,77,73,73,68,67,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,83,86,98,49,51,78,118,82,118,104,54,85,66,74,121,100,84,48,10,77,56,52,66,86,119,118,101,86,68,66,114,66,103,78,86,72,82,56,69,90,68,66,105,77,71,67,103,88,113,66,99,104,108,112,111,100,72,82,119,99,122,111,118,76,50,70,119,97,83,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,51,78,110,101,67,57,106,90,88,74,48,97,87,90,112,89,50,70,48,97,87,57,117,76,51,89,48,76,51,66,106,97,50,78,121,98,68,57,106,89,84,49,119,10,98,71,70,48,90,109,57,121,98,83,90,108,98,109,78,118,90,71,108,117,90,122,49,107,90,88,73,119,72,81,89,68,86,82,48,79,66,66,89,69,70,71,51,110,54,83,43,75,120,78,54,116,43,72,73,56,71,112,57,54,80,107,117,90,10,105,87,115,90,77,65,52,71,65,49,85,100,68,119,69,66,47,119,81,69,65,119,73,71,119,68,65,77,66,103,78,86,72,82,77,66,65,102,56,69,65,106,65,65,77,73,73,67,79,81,89,74,75,111,90,73,104,118,104,78,65,81,48,66,10,66,73,73,67,75,106,67,67,65,105,89,119,72,103,89,75,75,111,90,73,104,118,104,78,65,81,48,66,65,81,81,81,48,103,106,102,115,81,65,106,82,113,52,98,116,79,56,113,80,65,86,83,107,106,67,67,65,87,77,71,67,105,113,71,10,83,73,98,52,84,81,69,78,65,81,73,119,103,103,70,84,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,66,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,67,65,103,69,69,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,68,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,69,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,70,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,71,65,103,69,66,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,72,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,73,65,103,69,70,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,74,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,75,65,103,69,65,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,76,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,77,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,78,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,79,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,80,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,81,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,82,65,103,69,76,77,66,56,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,83,66,66,65,69,10,66,65,73,67,66,65,69,65,66,81,65,65,65,65,65,65,65,65,65,65,77,66,65,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,77,69,65,103,65,65,77,66,81,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,81,69,10,66,114,68,65,98,119,65,65,65,68,65,80,66,103,111,113,104,107,105,71,43,69,48,66,68,81,69,70,67,103,69,66,77,66,52,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,89,69,69,68,97,57,104,116,65,56,65,74,47,90,10,50,70,109,97,76,53,74,113,47,75,69,119,82,65,89,75,75,111,90,73,104,118,104,78,65,81,48,66,66,122,65,50,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,66,65,81,72,47,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,99,67,65,81,72,47,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,68,65,81,72,47,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,65,48,107,65,77,69,89,67,10,73,81,67,70,71,49,89,65,98,51,101,88,70,116,101,56,53,51,67,108,86,66,110,104,108,67,102,68,121,99,53,55,50,90,88,69,113,97,120,52,85,99,99,83,97,119,73,104,65,79,110,48,86,78,75,84,90,109,65,120,85,70,52,110,10,119,82,107,83,70,104,52,113,70,74,51,97,85,108,122,70,111,80,81,84,51,120,73,102,55,107,70,68,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,108,106,67,67,65,106,50,103,65,119,73,66,65,103,73,86,65,74,86,118,88,99,50,57,71,43,72,112,81,69,110,74,49,80,81,122,122,103,70,88,67,57,53,85,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,10,77,71,103,120,71,106,65,89,66,103,78,86,66,65,77,77,69,85,108,117,100,71,86,115,73,70,78,72,87,67,66,83,98,50,57,48,73,69,78,66,77,82,111,119,71,65,89,68,86,81,81,75,68,66,70,74,98,110,82,108,98,67,66,68,10,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,66,65,103,77,65,107,78,66,77,81,115,119,10,67,81,89,68,86,81,81,71,69,119,74,86,85,122,65,101,70,119,48,120,79,68,65,49,77,106,69,120,77,68,85,119,77,84,66,97,70,119,48,122,77,122,65,49,77,106,69,120,77,68,85,119,77,84,66,97,77,72,65,120,73,106,65,103,10,66,103,78,86,66,65,77,77,71,85,108,117,100,71,86,115,73,70,78,72,87,67,66,81,81,48,115,103,85,71,120,104,100,71,90,118,99,109,48,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,10,73,69,78,118,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,10,67,122,65,74,66,103,78,86,66,65,89,84,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,78,83,66,47,55,116,50,49,108,88,83,79,10,50,67,117,122,112,120,119,55,52,101,74,66,55,50,69,121,68,71,103,87,53,114,88,67,116,120,50,116,86,84,76,113,54,104,75,107,54,122,43,85,105,82,90,67,110,113,82,55,112,115,79,118,103,113,70,101,83,120,108,109,84,108,74,108,10,101,84,109,105,50,87,89,122,51,113,79,66,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,10,66,103,78,86,72,82,56,69,83,122,66,74,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,108,87,57,100,10,122,98,48,98,52,101,108,65,83,99,110,85,57,68,80,79,65,86,99,76,51,108,81,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,10,65,102,56,67,65,81,65,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,68,82,119,65,119,82,65,73,103,88,115,86,107,105,48,119,43,105,54,86,89,71,87,51,85,70,47,50,50,117,97,88,101,48,89,74,68,106,49,85,101,10,110,65,43,84,106,68,49,97,105,53,99,67,73,67,89,98,49,83,65,109,68,53,120,107,102,84,86,112,118,111,52,85,111,121,105,83,89,120,114,68,87,76,109,85,82,52,67,73,57,78,75,121,102,80,78,43,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,106,122,67,67,65,106,83,103,65,119,73,66,65,103,73,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,85,114,57,81,71,122,107,110,66,113,119,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,97,68,69,97,77,66,103,71,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,10,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,10,66,103,78,86,66,65,89,84,65,108,86,84,77,66,52,88,68,84,69,52,77,68,85,121,77,84,69,119,78,68,85,120,77,70,111,88,68,84,81,53,77,84,73,122,77,84,73,122,78,84,107,49,79,86,111,119,97,68,69,97,77,66,103,71,10,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,99,110,66,118,99,109,70,48,10,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,66,103,78,86,66,65,89,84,10,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,67,54,110,69,119,77,68,73,89,90,79,106,47,105,80,87,115,67,122,97,69,75,105,55,10,49,79,105,79,83,76,82,70,104,87,71,106,98,110,66,86,74,102,86,110,107,89,52,117,51,73,106,107,68,89,89,76,48,77,120,79,52,109,113,115,121,89,106,108,66,97,108,84,86,89,120,70,80,50,115,74,66,75,53,122,108,75,79,66,10,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,66,103,78,86,72,82,56,69,83,122,66,74,10,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,100,109,108,106,90,88,77,117,97,87,53,48,10,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,10,85,114,57,81,71,122,107,110,66,113,119,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,65,102,56,67,65,81,69,119,67,103,89,73,10,75,111,90,73,122,106,48,69,65,119,73,68,83,81,65,119,82,103,73,104,65,79,87,47,53,81,107,82,43,83,57,67,105,83,68,99,78,111,111,119,76,117,80,82,76,115,87,71,102,47,89,105,55,71,83,88,57,52,66,103,119,84,119,103,10,65,105,69,65,52,74,48,108,114,72,111,77,115,43,88,111,53,111,47,115,88,54,79,57,81,87,120,72,82,65,118,90,85,71,79,100,82,81,55,99,118,113,82,88,97,113,73,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] diff --git a/crates/test-utils/assets/tcb_info.json b/crates/test-utils/assets/tcb_info.json index 772640d15..368169ea2 100644 --- a/crates/test-utils/assets/tcb_info.json +++ b/crates/test-utils/assets/tcb_info.json @@ -3,11 +3,11 @@ "rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46", "rtmr1": "a7b523278d4f914ee8df0ec80cd1c3d498cbf1152b0c5eaf65bad9425072874a3fcf891e8b01713d3d9937e3e0d26c15", "rtmr2": "dbf4924c07f5066f3dc6859844184344306aa3263817153dcaee85af97d23e0c0b96efe0731d8865a8747e51b9e351ac", - "rtmr3": "c91f84e3b6bc947589700233f8e500d9c37b2d79738389ef61d1d18184f50bacb20b98ba29d32dfa1a933b11f6d76655", + "rtmr3": "da0a4418d22057f984734c8b122fc41e062ebf0724cba2555656ec9049e573ad46c5201fb00e47fc78cf22e5bbb6bf36", "os_image_hash": "", - "compose_hash": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48", + "compose_hash": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14", "device_id": "7a82191bd4dedb9d716e3aa422963cf1009f36e3068404a0322feca1ce517dc9", - "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1771748631\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45\\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-local-node0-testnet-tee\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316\\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, @@ -159,16 +159,16 @@ { "imr": 3, "event_type": 134217729, - "digest": "749f2448e65b5beae9ee03fcbcf0debb84bddb024c5d7859d98c8d25a5361630b13653e0c0feba4b172a83c33d967ebc", + "digest": "40c2b270b217916105b5d8502aeaa68f451b8e23d4c9d284a027b790168fc2d7db5d91ea0948a1aaf0618d7b2f982112", "event": "app-id", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f6" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0df" }, { "imr": 3, "event_type": 134217729, - "digest": "d322210028255abc72dc77012047a9d497cc0252d0f07f86d98290675ca66abfb9afc78785c6038357426af4e7dfd26f", + "digest": "2e7f3eddf530256b99ca3b3dea6c7ff3bbdcb251a4228d4f54d04b05277d6a46ec04bdf08aac771f2579e07271f22192", "event": "compose-hash", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14" }, { "imr": 3, @@ -201,9 +201,9 @@ { "imr": 3, "event_type": 134217729, - "digest": "a098bd7250ced8cc8340120c5452dcab7297a994c102b5c40c8e1c0ba2ba53d3b3c4342298d34e5587f753d750a9faad", + "digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678", "event": "mpc-image-digest", - "event_payload": "6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45" + "event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980" } ] } diff --git a/crates/test-utils/src/attestation.rs b/crates/test-utils/src/attestation.rs index ea73601d4..e49f2d6ac 100644 --- a/crates/test-utils/src/attestation.rs +++ b/crates/test-utils/src/attestation.rs @@ -18,10 +18,10 @@ pub const TEST_MPC_IMAGE_DIGEST_HEX: &str = include_str!("../assets/mpc_image_di pub const TEST_LAUNCHER_IMAGE_COMPOSE_STRING: &str = include_str!("../assets/launcher_image_compose.yaml"); -/// Unix time as of 2026/02/18, represents a date where +/// Unix time as of 2026/03/20, represents a date where /// the measurements stored in ../assets are valid. When these measurements are /// modified, this value should be updated as well -pub const VALID_ATTESTATION_TIMESTAMP: u64 = 1771750692; +pub const VALID_ATTESTATION_TIMESTAMP: u64 = 1774018367; pub fn launcher_compose_digest() -> LauncherDockerComposeHash { let digest: [u8; 32] = Sha256::digest(TEST_LAUNCHER_IMAGE_COMPOSE_STRING).into(); diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 557e01c07..33a96871a 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -1,3 +1,9 @@ +FROM debian:bookworm-slim@sha256:acd98e6cfc42813a4db9ca54ed79b6f702830bfc2fa43a2c2e87517371d82edb AS download +ARG COMPOSE_VERSION=v2.37.0 +ARG COMPOSE_SHA256=e6e471b1e7bf0443592d3987dea6073f08db3e48ba0580199109aa7a44257e54 +ADD https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64 /docker-compose +RUN echo "${COMPOSE_SHA256} /docker-compose" | sha256sum -c - + FROM debian:bookworm-slim@sha256:acd98e6cfc42813a4db9ca54ed79b6f702830bfc2fa43a2c2e87517371d82edb ENV DEBIAN_FRONTEND=noninteractive @@ -8,11 +14,12 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io docker-compose curl jq python3 && \ + apt-get install -y --no-install-recommends docker.io && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache -COPY --chmod=0755 tee_launcher/launcher.py /scripts/ -ENV PATH="/scripts:${PATH}" +COPY --from=download --chmod=0755 /docker-compose /usr/local/lib/docker/cli-plugins/docker-compose + +COPY --chmod=0755 target/reproducible/tee-launcher /usr/local/bin/tee-launcher RUN mkdir -p /app-data && mkdir -p /mnt/shared -CMD ["python3", "/scripts/launcher.py"] +CMD ["tee-launcher"] diff --git a/deployment/build-images.sh b/deployment/build-images.sh index 933531a66..7e80e3407 100755 --- a/deployment/build-images.sh +++ b/deployment/build-images.sh @@ -2,7 +2,7 @@ # Script to reproducibly the docker images for the node and launcher # # Requirements: docker, docker-buildx, jq, git, find, touch -# Extra requirements if using --node: repro-env, podman +# Extra requirements if using --node or --launcher: repro-env, podman # # Usage: # ./deployment/build-images.sh [--node] [--launcher] [--push] @@ -61,7 +61,7 @@ require_cmds() { require_cmds docker jq git find touch -if $USE_NODE; then +if $USE_NODE || $USE_LAUNCHER; then require_cmds repro-env podman fi @@ -117,6 +117,8 @@ get_image_hash() { } if $USE_LAUNCHER; then + SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH repro-env build --env SOURCE_DATE_EPOCH -- cargo build -p tee-launcher --profile reproducible --locked + launcher_binary_hash=$(sha256sum target/reproducible/tee-launcher | cut -d' ' -f1) build_reproducible_image $LAUNCHER_IMAGE_NAME $DOCKERFILE_LAUNCHER launcher_image_hash=$(get_image_hash $LAUNCHER_IMAGE_NAME) fi @@ -182,5 +184,6 @@ if $USE_NODE_GCP; then echo "node gcp docker image hash: $node_gcp_image_hash" fi if $USE_LAUNCHER; then + echo "launcher binary hash: $launcher_binary_hash" echo "launcher docker image hash: $launcher_image_hash" fi diff --git a/tee_launcher/configs/kms.env b/deployment/cvm-deployment/configs/kms.env similarity index 100% rename from tee_launcher/configs/kms.env rename to deployment/cvm-deployment/configs/kms.env diff --git a/tee_launcher/configs/sgx.env b/deployment/cvm-deployment/configs/sgx.env similarity index 100% rename from tee_launcher/configs/sgx.env rename to deployment/cvm-deployment/configs/sgx.env diff --git a/tee_launcher/default.env b/deployment/cvm-deployment/default.env similarity index 100% rename from tee_launcher/default.env rename to deployment/cvm-deployment/default.env diff --git a/tee_launcher/deploy-launcher-guide.md b/deployment/cvm-deployment/deploy-launcher-guide.md similarity index 100% rename from tee_launcher/deploy-launcher-guide.md rename to deployment/cvm-deployment/deploy-launcher-guide.md diff --git a/tee_launcher/deploy-launcher.sh b/deployment/cvm-deployment/deploy-launcher.sh similarity index 100% rename from tee_launcher/deploy-launcher.sh rename to deployment/cvm-deployment/deploy-launcher.sh diff --git a/tee_launcher/launcher_docker_compose.yaml b/deployment/cvm-deployment/launcher_docker_compose.yaml similarity index 63% rename from tee_launcher/launcher_docker_compose.yaml rename to deployment/cvm-deployment/launcher_docker_compose.yaml index db24475aa..25a19acff 100644 --- a/tee_launcher/launcher_docker_compose.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose.yaml @@ -2,20 +2,20 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054 + image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316 container_name: launcher environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:07bba7c60565750f6d5fe6800cd73513dd2d0d02e6893184064e209ff37c25a2 + - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 volumes: - /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 diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml similarity index 57% rename from tee_launcher/launcher_docker_compose_nontee.yaml rename to deployment/cvm-deployment/launcher_docker_compose_nontee.yaml index 48b0bc4fc..8fd42c74e 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml @@ -1,17 +1,17 @@ services: launcher: - image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054 + image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316 container_name: "${LAUNCHER_IMAGE_NAME}" environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:9143bc98aaae3408c14cf4490d7b0e96a5a32d989ec865a0cf8dde391831a7a9 # 3.6.0 release + - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./user-config.conf:/tapp/user_config:ro - - shared-volume:/mnt/shared + - ./user-config.toml:/tapp/user_config:ro + - shared-volume:/mnt/shared:rw - mpc-data:/data security_opt: diff --git a/deployment/cvm-deployment/user-config.toml b/deployment/cvm-deployment/user-config.toml new file mode 100644 index 000000000..eb89dcc32 --- /dev/null +++ b/deployment/cvm-deployment/user-config.toml @@ -0,0 +1,66 @@ +[launcher_config] +image_tags = ["main-9515e18"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ + # debug/metrics endpoint + { host = 8080, container = 8080 }, + { host = 3030, container = 3030 }, + # MPC - P2P + { host = 80, container = 80 }, + # neard + { host = 24567, container = 24567 }, + # migration + { host = 8079, container = 8079 }, +] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "json" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "testnet" +boot_nodes = "" +download_genesis = true +download_config = "rpc" + +[mpc_node_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + +[mpc_node_config.node] +my_near_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" +near_responder_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8079" +cores = 4 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract-barak-launch1-4c5e2fe1fb42.5035bf56abb0.testnet" +finality = "optimistic" + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/deployment/localnet/tee/frodo.conf b/deployment/localnet/tee/frodo.conf deleted file mode 100644 index 8d75a8465..000000000 --- a/deployment/localnet/tee/frodo.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=main-260e88b -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=frodo.test.near -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=mpc-contract.test.near -MPC_ENV=mpc-localnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566 - -# Port forwarding -PORTS=8080:8080,24566:24566,13001:13001 \ No newline at end of file diff --git a/deployment/localnet/tee/frodo.toml b/deployment/localnet/tee/frodo.toml new file mode 100644 index 000000000..458354508 --- /dev/null +++ b/deployment/localnet/tee/frodo.toml @@ -0,0 +1,61 @@ +[launcher_config] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ + { host = 8080, container = 8080 }, + { host = 24566, container = 24566 }, + { host = 13001, container = 13001 }, +] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "plain" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "" +genesis_path = "/app/localnet-genesis.json" +download_genesis = false + +[mpc_node_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_node_config.node] +my_near_account_id = "frodo.test.near" +near_responder_account_id = "frodo.test.near" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract.test.near" +finality = "optimistic" + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/deployment/localnet/tee/sam.conf b/deployment/localnet/tee/sam.conf deleted file mode 100644 index 507084bf2..000000000 --- a/deployment/localnet/tee/sam.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=main-260e88b -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=sam.test.near -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=mpc-contract.test.near -MPC_ENV=mpc-localnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566 - -# Port forwarding -PORTS=8080:8080,24566:24566,13002:13002 \ No newline at end of file diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml new file mode 100644 index 000000000..a691a6414 --- /dev/null +++ b/deployment/localnet/tee/sam.toml @@ -0,0 +1,61 @@ +[launcher_config] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ + { host = 8080, container = 8080 }, + { host = 24566, container = 24566 }, + { host = 13002, container = 13002 }, +] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "plain" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "" +genesis_path = "/app/localnet-genesis.json" +download_genesis = false + +[mpc_node_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_node_config.node] +my_near_account_id = "sam.test.near" +near_responder_account_id = "sam.test.near" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract.test.near" +finality = "optimistic" + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/deployment/testnet/frodo.conf b/deployment/testnet/frodo.conf deleted file mode 100644 index a6705e1ce..000000000 --- a/deployment/testnet/frodo.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=barak-doc-update_localnet_guide-b12bc7d -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=$FRODO_ACCOUNT -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=$MPC_CONTRACT_ACCOUNT -MPC_ENV=testnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=$BOOTNODES - -# Port forwarding -PORTS=8080:8080,24567:24567,13001:13001,80:80 \ No newline at end of file diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml new file mode 100644 index 000000000..cb6bc96f2 --- /dev/null +++ b/deployment/testnet/frodo.toml @@ -0,0 +1,64 @@ +[launcher_config] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ + { host = 8080, container = 8080 }, + { host = 24567, container = 24567 }, + { host = 13001, container = 13001 }, + { host = 80, container = 80 }, +] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "json" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "testnet" +boot_nodes = "" +download_genesis = true +download_config = "rpc" + +[mpc_node_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_node_config.node] +my_near_account_id = "$FRODO_ACCOUNT" +near_responder_account_id = "$FRODO_ACCOUNT" +number_of_responder_keys = 50 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +pprof_bind_address = "0.0.0.0:34001" +cores = 12 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" +finality = "optimistic" +port_override = 80 + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 1000000 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 16 +desired_presignatures_to_buffer = 8192 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/deployment/testnet/sam.conf b/deployment/testnet/sam.conf deleted file mode 100644 index 4e63db03b..000000000 --- a/deployment/testnet/sam.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=barak-doc-update_localnet_guide-b12bc7d -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=$SAM_ACCOUNT -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=$MPC_CONTRACT_ACCOUNT -MPC_ENV=testnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=$BOOTNODES - -# Port forwarding -PORTS=8080:8080,24567:24567,13002:13002,80:80 \ No newline at end of file diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml new file mode 100644 index 000000000..40e9a00aa --- /dev/null +++ b/deployment/testnet/sam.toml @@ -0,0 +1,64 @@ +[launcher_config] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ + { host = 8080, container = 8080 }, + { host = 24567, container = 24567 }, + { host = 13002, container = 13002 }, + { host = 80, container = 80 }, +] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "json" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "testnet" +boot_nodes = "" +download_genesis = true +download_config = "rpc" + +[mpc_node_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_node_config.node] +my_near_account_id = "$SAM_ACCOUNT" +near_responder_account_id = "$SAM_ACCOUNT" +number_of_responder_keys = 50 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +pprof_bind_address = "0.0.0.0:34001" +cores = 12 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" +finality = "optimistic" +port_override = 80 + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 1000000 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 16 +desired_presignatures_to_buffer = 8192 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/tee_launcher/UPDATING_LAUNCHER.md b/docs/UPDATING_LAUNCHER.md similarity index 100% rename from tee_launcher/UPDATING_LAUNCHER.md rename to docs/UPDATING_LAUNCHER.md diff --git a/docs/localnet/mpc-config.template.toml b/docs/localnet/mpc-config.template.toml index 04772da72..e7377cb94 100644 --- a/docs/localnet/mpc-config.template.toml +++ b/docs/localnet/mpc-config.template.toml @@ -4,6 +4,7 @@ home_dir = "$HOME/.near/$MPC_NODE_ID" chain_id = "mpc-localnet" boot_nodes = "$NEAR_BOOT_NODES" genesis_path = "$HOME/.near/mpc-localnet/genesis.json" +download_genesis = false rpc_addr = "0.0.0.0:$RPC_PORT" network_addr = "0.0.0.0:$INDEXER_PORT" diff --git a/docs/localnet/tee-localnet.md b/docs/localnet/tee-localnet.md index 1e3d6a417..f4e85e238 100644 --- a/docs/localnet/tee-localnet.md +++ b/docs/localnet/tee-localnet.md @@ -61,7 +61,7 @@ If any of these ports are already in use on your system, you will need to update The most important ports are the Frodo/Sam TLS ports: 13001 / 13002. These appear in the following files: -- frodo/sam.conf +- frodo/sam.toml - frodo/sam.env - init_tee.json @@ -72,8 +72,8 @@ There are additional ports defined in frodo/sam.env, but you may change those to Those are the recommended configuration settings: you will need the following files: -* [docker-compose.yml](../../tee_launcher/launcher_docker_compose.yaml) -* [frodo.conf](../../deployment/localnet/tee/frodo.conf) / [sam.conf](../../deployment/localnet/tee/sam.conf) +* [docker-compose.yml](../../deployment/cvm-deployment/launcher_docker_compose.yaml) +* [frodo.toml](../../deployment/localnet/tee/frodo.toml) / [sam.toml](../../deployment/localnet/tee/sam.toml) * [frodo.env](../../deployment/localnet/tee/frodo.env)/ [sam.env](../../deployment/localnet/tee/sam.env) - if you use the deployment script @@ -105,9 +105,9 @@ Define the machine's external IP once export MACHINE_IP=$(curl -4 -s ifconfig.me) # or use known IP for the machine ``` -#### Environment File (`frodo/sam.conf`, `frodo/sam.env`) ) +#### Environment File (`frodo/sam.toml`, `frodo/sam.env`) ) -Update Sam/Frodo.conf fields: +Update Sam/Frodo.toml fields: ```env @@ -127,7 +127,7 @@ $Docker inspect nearone/mpc-node:main_3.0.3 | grep "Id" You can start the nodes **manually** as described in the Operator Guide, or you can start them using the `deploy-launcher.sh` script as shown below. -Once all paths and configuration files (`*.env` and `*.conf`) are prepared, you can launch each MPC node (Frodo and Sam) using the `deploy-launcher.sh` helper script. +Once all paths and configuration files (`*.env` and `*.toml`) are prepared, you can launch each MPC node (Frodo and Sam) using the `deploy-launcher.sh` helper script. #### 1. Move into the `tee_launcher` Directory @@ -153,11 +153,11 @@ export BASE_PATH="dstask base path" #### 4. Replace ${MACHINE_IP} inside the config files ```bash -envsubst '${MACHINE_IP}' < deployment/localnet/tee/frodo.conf > "/tmp/$USER/frodo.conf" +envsubst '${MACHINE_IP}' < deployment/localnet/tee/frodo.toml > "/tmp/$USER/frodo.toml" ``` ```bash -envsubst '${MACHINE_IP}' < deployment/localnet/tee/sam.conf > "/tmp/$USER/sam.conf" +envsubst '${MACHINE_IP}' < deployment/localnet/tee/sam.toml > "/tmp/$USER/sam.toml" ``` #### 5. Start the Frodo MPC Node diff --git a/tee_launcher/using-the-launcher-in-nontee-setup.md b/docs/using-the-launcher-in-nontee-setup.md similarity index 100% rename from tee_launcher/using-the-launcher-in-nontee-setup.md rename to docs/using-the-launcher-in-nontee-setup.md diff --git a/flake.nix b/flake.nix index da3d9bf7e..7f223d4c2 100644 --- a/flake.nix +++ b/flake.nix @@ -179,14 +179,14 @@ strictDeps = true; packages = - dockerTools ++ - llvmTools ++ - rustTools ++ - cargoTools ++ - pythonTools ++ - nearTools ++ - miscTools ++ - buildLibs; + dockerTools + ++ llvmTools + ++ rustTools + ++ cargoTools + ++ pythonTools + ++ nearTools + ++ miscTools + ++ buildLibs; env = envCommon // envDarwin; diff --git a/localnet/tee/scripts/single-node-readme.md b/localnet/tee/scripts/single-node-readme.md index 506712e66..bea9fcfa2 100644 --- a/localnet/tee/scripts/single-node-readme.md +++ b/localnet/tee/scripts/single-node-readme.md @@ -1,14 +1,16 @@ # Run a Single MPC Node on Localnet (dstack CVM) This script: + - Creates/reuses one NEAR account on localnet -- Deploys one MPC node into dstack CVM (node is not guaranteed to be fully functional) +- Deploys one MPC node into dstack CVM (node is not guaranteed to be fully functional) - Fetches `/public_data` and saves it to JSON It is used to generate real attestation data for testing only: -See [UPDATING_LAUNCHER.md](../../../tee_launcher/UPDATING_LAUNCHER.md) +See [UPDATING_LAUNCHER.md](../../../docs/UPDATING_LAUNCHER.md) ## Prerequisites + - Local NEAR network running: `NEAR_ENV=mpc-localnet neard --home ~/.near/mpc-localnet run` - `mpc-localnet` configured in `near` CLI - dstack running (`http://127.0.0.1:10000`) @@ -17,47 +19,58 @@ See [UPDATING_LAUNCHER.md](../../../tee_launcher/UPDATING_LAUNCHER.md) ## Setup variables ### Required + ```bash # dstack base path (the folder containing vmm or vmm-data folder) export BASE_PATH=/path/to/dstack # external machine IP (you can use: # ip -4 -o addr show scope global | awk '{print $4}' | cut -d/ -f1 | grep -Ev '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)' export MACHINE_IP= -# the mpc docker image tag. +# the mpc docker image tag. # 1. make sure it is available on docker hub. -# 2. make sure that DEFAULT_IMAGE_DIGEST=sha256: in mpc/tee_launcher/launcher_docker_compose.yaml corresponds to that tag, by calling -# `docker pull nearone/mpc-node:$MPC_IMAGE_TAGS` and then +# 2. make sure that DEFAULT_IMAGE_DIGEST=sha256: in mpc/tee_launcher/launcher_docker_compose.yaml corresponds to that tag, by calling +# `docker pull nearone/mpc-node:$MPC_IMAGE_TAGS` and then # `docker inspect --format='{{.Id}}' nearone/mpc-node:$MPC_IMAGE_TAGS` export MPC_IMAGE_TAGS=3.3.0 ``` ### dstack port + If dstack VMM is not on port 10000: + ```bash export VMM_RPC=http://127.0.0.1: ``` ### Optional + If you want to use specific NEAR accounts name instead of defaults: + ```bash export NODE_ACCOUNT=frodo.test.near export CONTRACT_ACCOUNT=mpc-contract.test.near ``` ## Run + From the MPC repo root: + ```bash bash ./localnet/tee/scripts/single-node.sh ``` ## Output + - The script prints the work directory and all assigned ports at startup - Public endpoint: `http://:/public_data` - Saved JSON: `/public_data.json` (path printed by the script) ## Cleanup + To remove the CVM after you're done: + ```bash BASE_PATH=/path/to/dstack bash ./localnet/tee/scripts/single-node.sh --cleanup ``` + The exact command is printed at the end of a successful run. diff --git a/scripts/build-and-verify-launcher-docker-image.sh b/scripts/build-and-verify-launcher-docker-image.sh index 1f802e934..fc54247d4 100755 --- a/scripts/build-and-verify-launcher-docker-image.sh +++ b/scripts/build-and-verify-launcher-docker-image.sh @@ -2,6 +2,9 @@ set -euo pipefail +LAUNCHER_COMPOSE="deployment/cvm-deployment/launcher_docker_compose.yaml" +LAUNCHER_TEMPLATE="crates/contract/assets/launcher_docker_compose.yaml.template" + ./deployment/build-images.sh --launcher # Step 1: Get the built launcher image's manifest hash @@ -14,8 +17,8 @@ built_launcher_hash=$(sha256sum $temp_dir/manifest.json | cut -d' ' -f1) echo "Built launcher image hash: $built_launcher_hash" # Step 2: Extract the launcher and MPC hashes from the deployment compose file -deployed_launcher_hash=$(grep -o 'nearone/mpc-launcher@sha256:.*' tee_launcher/launcher_docker_compose.yaml | grep -o '@sha256:.*' | cut -c 9-) -deployed_mpc_hash=$(grep 'DEFAULT_IMAGE_DIGEST=sha256:' tee_launcher/launcher_docker_compose.yaml | grep -o 'sha256:.*' | cut -c 8-) +deployed_launcher_hash=$(grep -o 'nearone/mpc-launcher@sha256:.*' $LAUNCHER_COMPOSE | grep -o '@sha256:.*' | cut -c 9-) +deployed_mpc_hash=$(grep 'DEFAULT_IMAGE_DIGEST=sha256:' $LAUNCHER_COMPOSE | grep -o 'sha256:.*' | cut -c 8-) # Step 3: Fill the contract template with the deployment compose hashes and compare # This verifies both: @@ -24,12 +27,12 @@ deployed_mpc_hash=$(grep 'DEFAULT_IMAGE_DIGEST=sha256:' tee_launcher/launcher_do filled_template=$(sed \ -e "s/{{LAUNCHER_IMAGE_HASH}}/${deployed_launcher_hash}/" \ -e "s/{{DEFAULT_IMAGE_DIGEST_HASH}}/${deployed_mpc_hash}/" \ - crates/contract/assets/launcher_docker_compose.yaml.template) + $LAUNCHER_TEMPLATE) -if ! diff <(echo "$filled_template") tee_launcher/launcher_docker_compose.yaml > /dev/null; then +if ! diff <(echo "$filled_template") $LAUNCHER_COMPOSE > /dev/null; then echo "Template structure verification failed" echo "The contract template (filled with deployment hashes) does not match the deployment compose file." - diff <(echo "$filled_template") tee_launcher/launcher_docker_compose.yaml || true + diff <(echo "$filled_template") $LAUNCHER_COMPOSE || true exit 1 fi echo "Template structure verified: contract template matches deployment compose" diff --git a/scripts/check-kebab-case-files.sh b/scripts/check-kebab-case-files.sh index eda77900a..d75ba6752 100755 --- a/scripts/check-kebab-case-files.sh +++ b/scripts/check-kebab-case-files.sh @@ -30,6 +30,7 @@ EXEMPT_FILES=( launcher_docker_compose.yaml launcher_docker_compose_nontee.yaml launcher_image_compose.yaml + UPDATING_LAUNCHER.md ) # Build a regex that matches any of the allowed non-kebab-case filenames diff --git a/scripts/check-mpc-node-docker-starts.sh b/scripts/check-mpc-node-docker-starts.sh index b212d4007..a711bdd68 100755 --- a/scripts/check-mpc-node-docker-starts.sh +++ b/scripts/check-mpc-node-docker-starts.sh @@ -2,6 +2,9 @@ set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + USE_LAUNCHER=false for arg in "$@"; do @@ -21,11 +24,11 @@ done : "${LAUNCHER_IMAGE_NAME:=mpc-launcher-nontee}" if $USE_LAUNCHER; then - cd tee_launcher + cd "$REPO_ROOT/deployment/cvm-deployment" export LAUNCHER_IMAGE_NAME docker compose -f launcher_docker_compose_nontee.yaml up -d sleep 10 - launcher_logs=$(docker logs --tail 10 "$LAUNCHER_IMAGE_NAME" 2>&1) + launcher_logs=$(docker logs "$LAUNCHER_IMAGE_NAME" 2>&1) if ! echo "$launcher_logs" | grep "MPC launched successfully."; then echo "MPC launcher image did not start properly" echo "$launcher_logs" @@ -36,6 +39,8 @@ else touch /tmp/image-digest.bin # Test container startup - fail if container can't start # Start container in background and check status after 60 seconds + # + # TODO: REMOVE ALL ENVS PASSED CONTAINER_ID=$(docker run -d \ -v /tmp/:/data \ -e MPC_HOME_DIR="/data" \ @@ -64,7 +69,12 @@ echo "Container started: $CONTAINER_ID" # Check if container is actually running sleep 60 if [ -z "$(docker ps --filter "id=$CONTAINER_ID" --format "{{.ID}}")" ]; then - docker logs --tail 100 "$CONTAINER_ID" 2>&1 + echo "=== Container inspect ===" + docker inspect "$CONTAINER_ID" --format '{{.State.Status}} exit={{.State.ExitCode}}' 2>&1 || true + echo "=== Container logs ===" + docker logs "$CONTAINER_ID" 2>&1 || true + echo "=== docker ps -a ===" + docker ps -a 2>&1 || true echo "❌ Container cannot initialize/start properly" exit 1 fi diff --git a/tee_launcher/__init__.py b/tee_launcher/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tee_launcher/launcher-test-image/Dockerfile b/tee_launcher/launcher-test-image/Dockerfile deleted file mode 100644 index b49bc4b74..000000000 --- a/tee_launcher/launcher-test-image/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -# Dockerfile -FROM alpine@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 -CMD ["true"] \ No newline at end of file diff --git a/tee_launcher/launcher.md b/tee_launcher/launcher.md deleted file mode 100644 index a21904a80..000000000 --- a/tee_launcher/launcher.md +++ /dev/null @@ -1,102 +0,0 @@ -# Deploy and Upgrade an MPC Node on dstack - -The launcher is a single Python script: [launcher.py](launcher.py) - -This is a secure launcher script for initializing and attesting a Docker-based MPC node. -It is designed to run inside a TEE-enabled environment (e.g., Intel TDX) to add and ensures the integrity and trustworthiness of the image before launching it. - - -## 🔐 Features - -- Pull an MPC docker image. -- Compares the MPC image digest against expected values -- Extends RTMR3 with the verified image digest -- prints remote attestation and quote generation information to log -- Starts the MPC node container with secure mount and network settings - -## Usage - -The launcher script is designed to run inside a confidential TDX VM managed by Dstack VMM. - -launcher-docker-compose.yaml — Docker Compose file used to start the launcher and supporting containers. -config.txt — File containing trusted environment variables used by the launcher and MPC node. -It should be uploaded to: /tapp/.host-shared/.user-config - -## 🧩 Environment Variables - -- `DOCKER_CONTENT_TRUST=1`: Must be enabled -- `DEFAULT_IMAGE_DIGEST`: The expected hash of the Docker image (e.g., `sha256:...`) - -## 📁 File Locations - -- `/tapp/user_config"`: Optional `.env` file for overriding defaults -- `/mnt/shared/image-digest`: Optional override of image digest (written by external components) -- `/var/run/dstack.sock`: Unix socket used to communicate with `dstack` - -## 🔧 Configuration (via user-config) - -## 🖼️ Image selection - -| Variable | Description | -|----------|-------------| -| `MPC_IMAGE_NAME` | Name of the MPC docker image (default: `nearone/mpc-node`) | -| `MPC_REGISTRY` | Registry hostname (default: `registry.hub.docker.com`) | -| `MPC_IMAGE_TAGS` | Comma-separated tags to try (default: `latest`) | -| `MPC_HASH_OVERRIDE` | Optional: force a slection of specific sha256 digest (must be in approved list) | -| `RPC_REQUEST_TIMEOUT_SECS` | Per-request timeout for dockerhub | `10` | -| `RPC_REQUEST_INTERVAL_SECS` | Initial retry interval (seconds) for dockerhub | `1.0` | -| `RPC_MAX_ATTEMPTS` | Max attempts before failure for dockerhub | `20` | - -The launcher supports the following environment variables via `/tapp/user_config`: - -Example values (for [user-config.conf](./user-config.conf)) - -```bash -LAUNCHER_IMAGE_NAME=nearone/mpc-node -LAUNCHER_IMAGE_TAGS=latest -LAUNCHER_REGISTRY=registry.hub.docker.com -MPC_HASH_OVERRIDE=sha256:xyz... -RPC_REQUEST_TIMEOUT_SECS =10 -RPC_REQUEST_INTERVAL_SECS =1 -RPC_MAX_ATTEMPTS =20 -``` - -## Reproducible builds -from: tee_launcher folder run: -docker build -t barakeinavnear/launcher:latest -f development/Dockerfile.launcher . - -- [Dockerfile-node](../deployment/Dockerfile-node) Dockerfile with all dependencies pinned to specific versions, e.g., other Dockerfile via sha256 digests and Linux distribution packages via explicit version strings -- [build-images.sh](../deployment/build-images.sh) drives the build process - -For example, I ran `../deployment/build-images.sh` on the git commit [ef3f1e7...](https://github.com/Near-One/mpc/commit/ef3f1e7f862d447de60e91d32dadf68696eb6a58). The resulting Docker image digest was - -``` -sha256:dcbd3b8c8ae35d2ba63b25d6b617ce8b7faabb0af96ffa2e35b08a50258ebfa4 -``` - -and the MPC binary digest was - -``` -5dd1a80f842d08753184334466e97d55e5caa3dbcd93af27097d11e926d7f823 -``` - -The respective commands to find either are - -``` -docker image inspect mpc-node-gcp:latest | jq '.[0].Id' -``` - -Note, the image digest used with `docker run` is the output of the `docker image inspect ...` command. - -``` -docker run --rm dcbd3b8c8ae35d2ba63b25d6b617ce8b7faabb0af96ffa2e35b08a50258ebfa4 cat /app/mpc-node | sha256sum -``` - -Opens: write a script utilizing `vmm-cli.py` from Dstack to deploy an mpc node - -- Artifacts to deploy a node - - Scripts to a) reproducibly build the mpc binary and b) reproducibly build a docker image containing the mpc binary -- Actual upgrade procedure - - Write new image hash to /mnt/shared/image-digest - - Shut down cvm - - Amend `LAUNCHER_IMAGE_TAGS` if necessary; can be done from host by editing ./meta-dstack/build/run/*/shared/.user-config diff --git a/tee_launcher/launcher.py b/tee_launcher/launcher.py deleted file mode 100644 index 4f75fb762..000000000 --- a/tee_launcher/launcher.py +++ /dev/null @@ -1,873 +0,0 @@ -from collections import deque -import logging -import os -import stat -from typing import Dict, Union -from enum import Enum -import requests -from subprocess import CompletedProcess, run, check_output -import sys -import time -import traceback -from dataclasses import dataclass -import re -import json -from typing import NamedTuple - - -from requests.models import Response - -MPC_CONTAINER_NAME = "mpc-node" - -# The volume where this file resides is shared between launcher and app. -# To avoid concurrent modifications, the launcher mounts the volume read-only! -# the contents of this file are generated by the node itself and fetched from the contract. -IMAGE_DIGEST_FILE = "/mnt/shared/image-digest.bin" - - -# docker-hub defaults -RPC_REQUEST_TIMEOUT_SECS = 10 -RPC_REQUEST_INTERVAL_SECS = 1.0 -RPC_MAX_ATTEMPTS = 20 - - -class RpcTimingConfig(NamedTuple): - rpc_request_timeout_secs: float - rpc_request_interval_secs: float - rpc_max_attempts: int - - -# -# Platform mode (MUST come from measured docker-compose env in TEE deployments) -# -ENV_VAR_PLATFORM = "PLATFORM" - - -# do not change the string values - they are used in docker-compose files. -class Platform(Enum): - TEE = "TEE" - NONTEE = "NONTEE" - - -# only considered if `IMAGE_DIGEST_FILE` does not exist. -ENV_VAR_DEFAULT_IMAGE_DIGEST = "DEFAULT_IMAGE_DIGEST" - -# optional - the time to wait between rpc requests, in seconds. Defaults to 1.0 seconds. -ENV_VAR_RPC_REQUEST_INTERVAL_SECS = "RPC_REQUEST_INTERVAL_SECS" -# optional - the maximum time to wait for an rpc response. Defaults to 10 seconds. -ENV_VAR_RPC_REQUEST_TIMEOUT_SECS = "RPC_REQUEST_TIMEOUT_SECS" -# optional - the maximum number of attempts for rpc requests until we raise an exception -ENV_VAR_RPC_MAX_ATTEMPTS = "RPC_MAX_ATTEMPTS" -# MUST be set to 1. -OS_ENV_DOCKER_CONTENT_TRUST = "DOCKER_CONTENT_TRUST" - - -# dstack user configuration flags -DSTACK_USER_CONFIG_FILE = "/tapp/user_config" - -# dstack user config. Read from `DSTACK_USER_CONFIG_FILE` -DSTACK_USER_CONFIG_MPC_IMAGE_TAGS = "MPC_IMAGE_TAGS" -DSTACK_USER_CONFIG_MPC_IMAGE_NAME = "MPC_IMAGE_NAME" -DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY = "MPC_REGISTRY" - -# optional - if set, overrides the approved hashes list. -# format: sha256:... -ENV_VAR_MPC_HASH_OVERRIDE = "MPC_HASH_OVERRIDE" - -# Default values for dstack user config file. -DEFAULT_MPC_IMAGE_NAME = "nearone/mpc-node" -DEFAULT_MPC_REGISTRY = "registry.hub.docker.com" -DEFAULT_MPC_IMAGE_TAG = "latest" - -# Environment variables that configure the launcher itself. -# These are read from the user config file but should NEVER be passed to the MPC container. -ALLOWED_LAUNCHER_ENV_VARS = { - DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, - DSTACK_USER_CONFIG_MPC_IMAGE_NAME, - DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, - ENV_VAR_MPC_HASH_OVERRIDE, - ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, - ENV_VAR_RPC_REQUEST_INTERVAL_SECS, - ENV_VAR_RPC_MAX_ATTEMPTS, -} - - -# the unix socket to communicate with dstack -DSTACK_UNIX_SOCKET = "/var/run/dstack.sock" - -SHA256_PREFIX = "sha256:" -SHA256_REGEX = re.compile(r"^sha256:[0-9a-f]{64}$") - -# JSON key used inside image-digest.bin -# IMPORTANT: Must stay aligned with the MPC node implementation in: -# crates/node/src/tee/allowed_image_hashes_watcher.rs -JSON_KEY_APPROVED_HASHES = "approved_hashes" - - -# -------------------------------------------------------------------------------------- -# Security policy for env passthrough -# -------------------------------------------------------------------------------------- - -# Allow all MPC_* keys, but keep validation strict. -MPC_ENV_KEY_RE = re.compile(r"^MPC_[A-Z0-9_]{1,64}$") - - -# Hard caps to prevent DoS via huge env payloads. -MAX_PASSTHROUGH_ENV_VARS = 64 -MAX_ENV_VALUE_LEN = 1024 -MAX_TOTAL_ENV_BYTES = 32 * 1024 # 32KB total across passed envs - -# Never pass raw private keys via launcher (any platform) -DENIED_CONTAINER_ENV_KEYS = { - "MPC_P2P_PRIVATE_KEY", - "MPC_ACCOUNT_SK", -} - -# Example of .user-config file format: -# -# MPC_ACCOUNT_ID=mpc-user-123 -# MPC_LOCAL_ADDRESS=127.0.0.1 -# MPC_SECRET_STORE_KEY=secret -# MPC_CONTRACT_ID=mpc-contract -# MPC_ENV=testnet -# MPC_HOME_DIR=/data -# NEAR_BOOT_NODES=boot1,boot2 -# RUST_BACKTRACE=1 -# RUST_LOG=info -# MPC_RESPONDER_ID=responder-xyz -# PORTS=11780:11780,2200:2200 - -# Define an allow-list of permitted environment variables that will be passed to MPC container. -# Note - extra hosts and port forwarding are explicitly defined in the docker run command generation. -# NOTE: Kept for backwards compatibility and for documentation purposes; the effective policy is: -# - allow MPC_* keys that match MPC_ENV_KEY_RE -# - plus existing non-MPC keys below (RUST_LOG / RUST_BACKTRACE / NEAR_BOOT_NODES) -ALLOWED_MPC_ENV_VARS = { - "MPC_ACCOUNT_ID", # ID of the MPC account on the network - "MPC_LOCAL_ADDRESS", # Local IP address or hostname used by the MPC node - "MPC_SECRET_STORE_KEY", # Key used to encrypt/decrypt secrets - "MPC_CONTRACT_ID", # Contract ID associated with the MPC node - "MPC_ENV", # Environment (e.g., 'testnet', 'mainnet') - "MPC_HOME_DIR", # Home directory for the MPC node - "NEAR_BOOT_NODES", # Comma-separated list of boot nodes - "RUST_BACKTRACE", # Enables backtraces for Rust errors - "RUST_LOG", # Logging level for Rust code - "MPC_RESPONDER_ID", # Unique responder ID for MPC communication - "MPC_BACKUP_ENCRYPTION_KEY_HEX", # encryption key for backups -} - -# Regex: hostnames must be alphanum + dash/dot, IPs must be valid IPv4 -HOST_ENTRY_RE = re.compile(r"^[a-zA-Z0-9\-\.]+:\d{1,3}(\.\d{1,3}){3}$") -PORT_MAPPING_RE = re.compile(r"^(\d{1,5}):(\d{1,5})$") - -# Updated regex to block any entry starting with '-' (including '--') and other unsafe characters -INVALID_HOST_ENTRY_PATTERN = re.compile(r"^[;&|`$\\<>-]|^--") - - -def _has_control_chars(s: str) -> bool: - # Disallow NUL + CR/LF at minimum; also block other ASCII control chars (< 0x20) except tab. - for ch in s: - oc = ord(ch) - if ch in ("\n", "\r", "\x00"): - return True - if oc < 0x20 and ch != "\t": - return True - return False - - -def is_safe_env_value(value: str) -> bool: - """ - Validates that an env value contains no unsafe control characters (CR/LF/NUL), - does not include LD_PRELOAD, and is within size limits to prevent injection or DoS. - """ - if not isinstance(value, str): - return False - if len(value) > MAX_ENV_VALUE_LEN: - return False - if _has_control_chars(value): - return False - if "LD_PRELOAD" in value: - return False - return True - - -def is_valid_port_mapping(entry: str) -> bool: - match = PORT_MAPPING_RE.match(entry) - if not match: - return False - host_port, container_port = map(int, match.groups()) - return 0 < host_port <= 65535 and 0 < container_port <= 65535 - - -def is_non_empty_and_cleaned(val: str) -> bool: - if not isinstance(val, str): - return False - if not val.strip(): - return False - return val.strip() == val - - -def is_safe_port_mapping(mapping: str) -> bool: - """Ensure that the port mapping does not contain unsafe characters or start with '--' or '-'.""" - return not INVALID_HOST_ENTRY_PATTERN.search(mapping) - - -def remove_existing_container(): - """Stop and remove the MPC container if it exists.""" - try: - containers = check_output( - ["docker", "ps", "-a", "--format", "{{.Names}}"], text=True - ).splitlines() - if MPC_CONTAINER_NAME in containers: - logging.info(f"Removing existing container: {MPC_CONTAINER_NAME}") - run(["docker", "rm", "-f", MPC_CONTAINER_NAME], check=False) - except Exception as e: - logging.warning(f"Failed to check/remove container {MPC_CONTAINER_NAME}: {e}") - - -@dataclass(frozen=True) -class ImageSpec: - tags: list[str] - image_name: str - registry: str - - def __post_init__(self): - if not self.tags or not all(is_non_empty_and_cleaned(tag) for tag in self.tags): - raise ValueError( - "tags must be a non-empty list of non-empty strings without whitespaces." - ) - - if not is_non_empty_and_cleaned(self.image_name): - raise ValueError( - "image_name must be a non-empty string without whitespaces." - ) - - if not is_non_empty_and_cleaned(self.registry): - raise ValueError("registry must be a non-empty string without whitespaces.") - - -@dataclass(frozen=True) -class ResolvedImage: - spec: ImageSpec - digest: str - - def __post_init__(self): - if not is_non_empty_and_cleaned(self.digest): - raise ValueError( - "image digest must be a non-empty string without whitespaces" - ) - # should we require specific lengths? - - def name(self) -> str: - return self.spec.image_name - - def tags(self) -> list[str]: - return self.spec.tags - - def registry(self) -> str: - return self.spec.registry - - -def parse_env_lines(lines: list[str]) -> dict: - env = {} - for line in lines: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - key = key.strip() - value = value.strip() - if not key: - continue - env[key] = value - return env - - -# Parses a .env-style file into a dictionary of key-value pairs. -def parse_env_file(path: str) -> dict: - with open(path, "r") as f: - return parse_env_lines(f.readlines()) - - -def is_unix_socket(path: str) -> bool: - try: - st = os.stat(path) - return stat.S_ISSOCK(st.st_mode) - except FileNotFoundError: - return False - except Exception: - return False - - -def parse_platform() -> Platform: - """ - Platform selection MUST be a measured input in TEE deployments. - Therefore, we only read it from process env (docker-compose 'environment'), - and never from /tapp/user_config. - """ - raw = os.environ.get(ENV_VAR_PLATFORM) - if raw is None: - raise RuntimeError( - f"{ENV_VAR_PLATFORM} must be set to one of {[p.value for p in Platform]}" - ) - - val = raw.strip() - try: - return Platform(val) - except ValueError as e: - allowed = ", ".join(p.value for p in Platform) - raise RuntimeError( - f"Invalid {ENV_VAR_PLATFORM}={raw!r}. Expected one of: {allowed}" - ) from e - - -def load_rpc_timing_config(dstack_config: dict[str, str]) -> RpcTimingConfig: - """ - Loads dockerhub RPC timing configuration from dstack_config, - falling back to defaults if not provided by user. - """ - - timeout_secs = float( - dstack_config.get(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, RPC_REQUEST_TIMEOUT_SECS) - ) - interval_secs = float( - dstack_config.get(ENV_VAR_RPC_REQUEST_INTERVAL_SECS, RPC_REQUEST_INTERVAL_SECS) - ) - max_attempts = int(dstack_config.get(ENV_VAR_RPC_MAX_ATTEMPTS, RPC_MAX_ATTEMPTS)) - - return RpcTimingConfig( - rpc_request_timeout_secs=timeout_secs, - rpc_request_interval_secs=interval_secs, - rpc_max_attempts=max_attempts, - ) - - -def get_image_spec(dstack_config: dict[str, str]) -> ImageSpec: - tags_values: list[str] = dstack_config.get( - DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, DEFAULT_MPC_IMAGE_TAG - ).split(",") - tags = [tag.strip() for tag in tags_values if tag.strip()] - logging.info(f"Using tags {tags} to find matching MPC node docker image.") - - image_name: str = dstack_config.get( - DSTACK_USER_CONFIG_MPC_IMAGE_NAME, DEFAULT_MPC_IMAGE_NAME - ) - logging.info(f"Using image name {image_name}.") - - registry: str = dstack_config.get( - DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, DEFAULT_MPC_REGISTRY - ) - logging.info(f"Using registry {registry}.") - - return ImageSpec(tags=tags, image_name=image_name, registry=registry) - - -def load_and_select_hash(dstack_config: dict) -> str: - """ - Load the approved MPC image hashes and deterministically select a single hash. - - Selection rules: - - If MPC_HASH_OVERRIDE is set → use it (after basic format validation) - - Else → use the newest approved hash (already first in the list) - - This function does NOT validate the hash — only selects it. - """ - - # IMAGE_DIGEST_FILE missing → use DEFAULT_IMAGE_DIGEST - if not os.path.isfile(IMAGE_DIGEST_FILE): - fallback = os.environ[ENV_VAR_DEFAULT_IMAGE_DIGEST].strip() - - if not fallback.startswith(SHA256_PREFIX): - fallback = SHA256_PREFIX + fallback - if not SHA256_REGEX.match(fallback): - raise RuntimeError(f"DEFAULT_IMAGE_DIGEST invalid: {fallback}") - - logging.info( - f"{IMAGE_DIGEST_FILE} missing → fallback to DEFAULT_IMAGE_DIGEST={fallback}" - ) - approved_hashes = [fallback] - - else: - # Load JSON with approved hashes - try: - with open(IMAGE_DIGEST_FILE, "r") as f: - data = json.load(f) - except Exception as e: - raise RuntimeError(f"Failed to parse {IMAGE_DIGEST_FILE}: {e}") - - hashes = data.get(JSON_KEY_APPROVED_HASHES) - - if not isinstance(hashes, list) or not hashes: - raise RuntimeError( - f"Invalid JSON in {IMAGE_DIGEST_FILE}: approved_hashes missing or empty" - ) - - # Hashes from the node are already ordered newest → oldest - approved_hashes = hashes - - # Print allowed hashes for operator UX - logging.info("Approved MPC image hashes (newest → oldest):") - for h in approved_hashes: - logging.info(f" - {h}") - - # --- Optional override --- - override = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) - - if override: - if not SHA256_REGEX.match(override): - raise RuntimeError(f"MPC_HASH_OVERRIDE invalid: {override}") - else: - if override not in approved_hashes: - logging.error( - f"MPC_HASH_OVERRIDE={override} does NOT match any approved hash!" - ) - raise RuntimeError(f"MPC_HASH_OVERRIDE invalid: {override}") - logging.info(f"MPC_HASH_OVERRIDE provided → selecting: {override}") - return override - - # No override → select newest - selected = approved_hashes[0] - logging.info(f"Selected MPC hash (newest allowed): {selected}") - return selected - - -def validate_image_hash( - image_digest: str, - dstack_config: dict, - rpc_request_timeout_secs: float, - rpc_request_interval_secs: float, - rpc_max_attempts: int, -) -> bool: - """ - Returns True if the given image digest is valid (pull + manifest + digest match). - Does NOT extend RTMR3 and does NOT run the container. - """ - try: - logging.info(f"Validating MPC hash: {image_digest}") - - image_spec = get_image_spec(dstack_config) - docker_image = ResolvedImage(spec=image_spec, digest=image_digest) - - manifest_digest = get_manifest_digest( - docker_image, - rpc_request_timeout_secs, - rpc_request_interval_secs, - rpc_max_attempts, - ) - - name_and_digest = f"{image_spec.image_name}@{manifest_digest}" - - # Pull - proc = run(["docker", "pull", name_and_digest], capture_output=True) - if proc.returncode != 0: - logging.error( - f"docker pull failed for {image_digest} using {name_and_digest}" - ) - logging.error( - f"stdout:\n{proc.stdout}", - ) - logging.error( - f"stderr:\n{proc.stderr}", - ) - return False - - # Verify digest - proc = run( - [ - "docker", - "image", - "inspect", - "--format", - "{{index .ID}}", - name_and_digest, - ], - capture_output=True, - ) - if proc.returncode != 0: - logging.error(f"docker inspect failed for {image_digest}") - return False - - pulled_digest = proc.stdout.decode("utf-8").strip() - if pulled_digest != image_digest: - logging.error(f"digest mismatch: {pulled_digest} != {image_digest}") - return False - logging.info(f"MPC hash {image_digest} validated successfully.") - return True - - except Exception as e: - logging.error(f"Validation failed for {image_digest}: {e}") - return False - - -def extend_rtmr3(platform: Platform, valid_hash: str) -> None: - if platform == Platform.NONTEE: - logging.info("PLATFORM=NONTEE → skipping RTMR3 extension step.") - return - - if not is_unix_socket(DSTACK_UNIX_SOCKET): - raise RuntimeError( - f"PLATFORM=TEE requires dstack unix socket at {DSTACK_UNIX_SOCKET}." - ) - - bare = get_bare_digest(valid_hash) - logging.info(f"Extending RTMR3 with validated hash: {bare}") - - # GetQuote first - proc = curl_unix_socket_post( - endpoint="GetQuote", payload='{"report_data": ""}', capture_output=True - ) - if proc.returncode: - raise RuntimeError("GetQuote failed before extending RTMR3") - - payload_json = json.dumps({"event": "mpc-image-digest", "payload": bare}) - - proc = curl_unix_socket_post( - endpoint="EmitEvent", payload=payload_json, capture_output=True - ) - if proc.returncode: - raise RuntimeError("EmitEvent failed while extending RTMR3") - - -def launch_mpc_container(platform: Platform, valid_hash: str, user_env: dict) -> None: - logging.info(f"Launching MPC node with validated hash: {valid_hash}") - - remove_existing_container() - docker_cmd = build_docker_cmd(platform, user_env, valid_hash) - - proc = run(docker_cmd) - if proc.returncode != 0: - raise RuntimeError(f"docker run failed for validated hash={valid_hash}") - - logging.info("MPC launched successfully.") - - -def curl_unix_socket_post( - endpoint: str, payload: Union[str, bytes], capture_output: bool = False -) -> CompletedProcess: - """ - Send a POST request via curl using the DSTACK UNIX socket. - Python's requests package cannot natively talk HTTP over a unix socket (which is the API - exposed by dstack's guest agent). To avoid installing another Python depdendency, namely - requests-unixsocket, we just use curl. - - Args: - endpoint: Path after `http://dstack/`, e.g. 'GetQuote', 'EmitEvent' - payload: JSON string or bytes to send as the request body - capture_output: Whether to capture stdout/stderr (default: False) - - Returns: - subprocess.CompletedProcess result - """ - url = f"http://dstack/{endpoint}" - cmd = [ - "curl", - "--unix-socket", - DSTACK_UNIX_SOCKET, - "-X", - "POST", - url, - "-H", - "Content-Type: application/json", - "-d", - payload, - ] - return run(cmd, capture_output=capture_output) - - -def main(): - logging.info("start") - - platform = parse_platform() - logging.info(f"Launcher platform: {platform.value}") - - if platform is Platform.TEE and not is_unix_socket(DSTACK_UNIX_SOCKET): - raise RuntimeError( - f"PLATFORM=TEE requires dstack unix socket at {DSTACK_UNIX_SOCKET}." - ) - - # DOCKER_CONTENT_TRUST must be enabled - if os.environ.get(OS_ENV_DOCKER_CONTENT_TRUST, "0") != "1": - raise RuntimeError( - "Environment variable DOCKER_CONTENT_TRUST must be set to 1." - ) - - # Load dstack configuration (tags, registry, image name) - # In dstack, /tapp/user_config provides unmeasured data to the CVM. - # We use this interface to make some aspects of the launcher configurable. - # *** Only security-irrelevant parts *** may be made configurable in this way, e.g., the specific image tag(s) we look up. - dstack_config: dict[str, str] = ( - parse_env_file(DSTACK_USER_CONFIG_FILE) - if os.path.isfile(DSTACK_USER_CONFIG_FILE) - else {} - ) - - rpc_cfg = load_rpc_timing_config(dstack_config) - - rpc_request_timeout_secs = rpc_cfg.rpc_request_timeout_secs - rpc_request_interval_secs = rpc_cfg.rpc_request_interval_secs - rpc_max_attempts = rpc_cfg.rpc_max_attempts - - # Choose exactly one allowed hash (override or newest) - selected_hash = load_and_select_hash(dstack_config) - - if not validate_image_hash( - selected_hash, - dstack_config, - rpc_request_timeout_secs, - rpc_request_interval_secs, - rpc_max_attempts, - ): - raise RuntimeError(f"MPC image hash validation failed: {selected_hash}") - - # Continue with validated hash - logging.info(f"MPC image hash validated successfully: {selected_hash}") - - extend_rtmr3(platform, selected_hash) - - launch_mpc_container(platform, selected_hash, dstack_config) - - -def request_until_success( - url: str, - headers: Dict[str, str], - rpc_request_timeout_secs: float, - rpc_request_interval_secs: float, - rpc_max_attempts: int, -) -> Response: - """ - Repeatedly sends a GET request to the specified URL until a successful (200 OK) response is received. - - Args: - url (str): The URL to request. - headers (Dict[str, str]): Optional headers to include in the request. - rpc_request_interval_secs (float): Time in seconds to wait between retries on failure. - rpc_request_timeout_secs (float): Maximum time in seconds to wait for a request to succeed. - - Returns: - Response: The successful HTTP response object with status code 200. - - Notes: - - Retries indefinitely until the request succeeds. - - Prints a warning with the response content on each failure. - """ - for attempt in range(1, rpc_max_attempts + 1): - # Ensure that we respect the backoff time. Performance is not a priority in this case. - time.sleep(rpc_request_interval_secs) - rpc_request_interval_secs = min(max(rpc_request_interval_secs, 1.0) * 1.5, 60.0) - try: - manifest_resp = requests.get( - url, headers=headers, timeout=rpc_request_timeout_secs - ) - except requests.exceptions.Timeout: - print( - f"[Warning] Attempt {attempt}/{rpc_max_attempts}: Failed to fetch {url} for headers {headers}. " - f"Status: Timeout" - ) - continue - if manifest_resp.status_code != 200: - print( - f"[Warning] Attempt {attempt}/{rpc_max_attempts}: Failed to fetch {url} for headers {headers}. " - f"Status: {manifest_resp.text} {manifest_resp.headers}" - ) - continue - else: - return manifest_resp - - raise RuntimeError( - f"Failed to get successful response from {url} after {rpc_max_attempts} attempts." - ) - - -def get_manifest_digest( - docker_image: ResolvedImage, - rpc_request_timeout_secs: float, - rpc_request_interval_secs: float, - rpc_max_attempts: int, -) -> str: - """ - Given an `image_digest` returns a manifest digest. - `docker pull` requires a manifest digest. This function translates an image digest into a manifest digest by talking to the Docker registry. - API doc for image registry https://distribution.github.io/distribution/spec/api/ - """ - if not docker_image.tags(): - raise Exception(f"No tags found for image {docker_image.spec.image_name}") - - # We need an authorization token to fetch manifests. - # TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint. - token_resp = requests.get( - f"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{docker_image.name()}:pull" - ) - token_resp.raise_for_status() - token = token_resp.json().get("token", []) - - tags = deque(docker_image.tags()) - - while tags: - tag = tags.popleft() - - manifest_url = f"https://{docker_image.registry()}/v2/{docker_image.name()}/manifests/{tag}" - headers = { - "Accept": "application/vnd.docker.distribution.manifest.v2+json", - "Authorization": f"Bearer {token}", - } - try: - manifest_resp = request_until_success( - url=manifest_url, - headers=headers, - rpc_request_timeout_secs=rpc_request_timeout_secs, - rpc_request_interval_secs=rpc_request_interval_secs, - rpc_max_attempts=rpc_max_attempts, - ) - manifest = manifest_resp.json() - match manifest["mediaType"]: - case "application/vnd.oci.image.index.v1+json": - # Multi-platform manifest; we scan for amd64/linux images and add them to `tags` - for image_manifest in manifest.get("manifests", []): - platform = image_manifest.get("platform", []) - if ( - platform.get("architecture") == "amd64" - and platform.get("os") == "linux" - ): - tags.append(image_manifest["digest"]) - case ( - "application/vnd.docker.distribution.manifest.v2+json" - | "application/vnd.oci.image.manifest.v1+json" - ): - config_digest = manifest["config"]["digest"] - if config_digest == docker_image.digest: - return manifest_resp.headers["Docker-Content-Digest"] - except RuntimeError as e: - print( - f"[Warning] {e}: Exceeded number of maximum RPC requests for any given attempt. Will continue in the hopes of finding the matching image hash among remaining tags" - ) - # Q: Do we expect all requests to succeed? - raise Exception("Image hash not found among tags.") - - -def get_bare_digest(full_digest: str) -> str: - """ - Extracts and returns the bare digest (without the sha256: prefix). - - Example: - 'sha256:abcd...' -> 'abcd...' - """ - if not full_digest.startswith("sha256:"): - raise ValueError(f"Invalid digest (missing sha256: prefix): {full_digest}") - - return full_digest.split(":", 1)[1] - - -def is_allowed_container_env_key(key: str) -> bool: - if key in DENIED_CONTAINER_ENV_KEYS: - return False - # Allow MPC_* keys with strict regex - if MPC_ENV_KEY_RE.match(key): - return True - # Keep allowlist - if key in ALLOWED_MPC_ENV_VARS: - return True - return False - - -def build_docker_cmd( - platform: Platform, user_env: dict[str, str], image_digest: str -) -> list[str]: - bare_digest = get_bare_digest(image_digest) - - docker_cmd = ["docker", "run"] - - # Required environment variables - docker_cmd += ["--env", f"MPC_IMAGE_HASH={bare_digest}"] - docker_cmd += ["--env", f"MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"] - - if platform is Platform.TEE: - docker_cmd += ["--env", f"DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"] - docker_cmd += ["-v", f"{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"] - - # Track env passthrough size/caps - passed_env_count = 0 - total_env_bytes = 0 - - # Deterministic iteration for stable logs/behavior - for key in sorted(user_env.keys()): - value = user_env[key] - - if key in ALLOWED_LAUNCHER_ENV_VARS: - # launcher-only env vars: never pass to container - continue - - if key == "PORTS": - for port_pair in value.split(","): - clean_host = port_pair.strip() - if is_safe_port_mapping(clean_host) and is_valid_port_mapping( - clean_host - ): - docker_cmd += ["-p", clean_host] - else: - logging.warning( - f"Ignoring invalid or unsafe PORTS entry: {clean_host}" - ) - continue - - if not is_allowed_container_env_key(key): - logging.warning(f"Ignoring unknown or unapproved env var: {key}") - continue - - if not is_safe_env_value(value): - logging.warning(f"Ignoring env var with unsafe value: {key}") - continue - - # Enforce caps - passed_env_count += 1 - if passed_env_count > MAX_PASSTHROUGH_ENV_VARS: - raise RuntimeError( - f"Too many env vars to pass through (>{MAX_PASSTHROUGH_ENV_VARS})." - ) - - # Approximate byte accounting (key=value plus overhead) - total_env_bytes += len(key) + 1 + len(value) - if total_env_bytes > MAX_TOTAL_ENV_BYTES: - raise RuntimeError( - f"Total env payload too large (>{MAX_TOTAL_ENV_BYTES} bytes)." - ) - - docker_cmd += ["--env", f"{key}={value}"] - - # Container run configuration - docker_cmd += [ - "--security-opt", - "no-new-privileges:true", - "-v", - "/tapp:/tapp:ro", - "-v", - "shared-volume:/mnt/shared", - "-v", - "mpc-data:/data", - "--name", - MPC_CONTAINER_NAME, - "--detach", - image_digest, # IMPORTANT: Docker must get the FULL digest - ] - - logging.info("docker cmd %s", " ".join(docker_cmd)) - - # Final LD_PRELOAD safeguard - docker_cmd_str = " ".join(docker_cmd) - if "LD_PRELOAD" in docker_cmd_str: - raise RuntimeError("Unsafe docker command: LD_PRELOAD detected.") - - return docker_cmd - - -if __name__ == "__main__": - try: - logging.basicConfig( - level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" - ) - - main() - sys.exit(0) - except Exception as e: - print("Error:", str(e), file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) diff --git a/tee_launcher/requirements.txt b/tee_launcher/requirements.txt deleted file mode 100644 index 61a0f40c5..000000000 --- a/tee_launcher/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==8.3.4 -requests==2.32.4 diff --git a/tee_launcher/test_launcher.py b/tee_launcher/test_launcher.py deleted file mode 100644 index 18d2960ad..000000000 --- a/tee_launcher/test_launcher.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest - -from launcher import ImageSpec, ResolvedImage, get_manifest_digest - - -class TestLauncher(unittest.TestCase): - def test_get_manifest_digest(self): - # Use a recent (at the time of writing) tag/image digest/manifest digest combination for testing - registry_url = "registry.hub.docker.com" - image_name = "nearone/mpc-node-gcp" - image_hash = ( - "sha256:7e5a6bcb6707d134fc479cc293830c05ce45891f0977d467362cbb7f55cde46b" - ) - expected_manifest_digest = ( - "sha256:005943bccdd401e71c5408d65cf301eeb8bfc3926fe346023912412aafda2490" - ) - tags = ["8805536ab98d924d980a58ecc0518a8c90204bec"] - image = ResolvedImage( - ImageSpec(tags=tags, image_name=image_name, registry=registry_url), - digest=image_hash, - ) - result = get_manifest_digest( - image, - rpc_request_timeout_secs=10, - rpc_request_interval_secs=0.5, - rpc_max_attempts=10, - ) - self.assertEqual(result, expected_manifest_digest) - - -if __name__ == "__main__": - unittest.main() diff --git a/tee_launcher/test_launcher_config.py b/tee_launcher/test_launcher_config.py deleted file mode 100644 index fd33007ec..000000000 --- a/tee_launcher/test_launcher_config.py +++ /dev/null @@ -1,844 +0,0 @@ -# test_launcher_config.py - -import inspect -import json -import os -import tempfile -import tee_launcher.launcher as launcher - - -import pytest -from unittest.mock import mock_open - -from tee_launcher.launcher import ( - load_and_select_hash, - validate_image_hash, - parse_env_lines, - build_docker_cmd, - is_valid_port_mapping, - Platform, - is_safe_env_value, - _has_control_chars, - is_allowed_container_env_key, - MAX_ENV_VALUE_LEN, - MAX_PASSTHROUGH_ENV_VARS, -) -from tee_launcher.launcher import ( - JSON_KEY_APPROVED_HASHES, - ENV_VAR_MPC_HASH_OVERRIDE, - ENV_VAR_DEFAULT_IMAGE_DIGEST, -) - - -# Test constants for user_config content -TEST_MPC_ACCOUNT_ID = "mpc-user-123" - -TEST_PORTS_WITH_INJECTION = "11780:11780,--env BAD=1" - - -def make_digest_json(hashes): - return json.dumps({JSON_KEY_APPROVED_HASHES: hashes}) - - -def parse_env_string(text: str) -> dict: - return parse_env_lines(text.splitlines()) - - -def test_parse_env_lines_basic(): - lines = [ - "# a comment", - "KEY1=value1", - " KEY2 = value2 ", - "", - "INVALIDLINE", - "EMPTY_KEY=", - ] - env = parse_env_lines(lines) - assert env == {"KEY1": "value1", "KEY2": "value2", "EMPTY_KEY": ""} - - -# test user config loading and parsing -def write_temp_config(content: str) -> str: - tmp = tempfile.NamedTemporaryFile(mode="w", delete=False) - tmp.write(content) - tmp.close() - return tmp.name - - -def test_valid_user_config_parsing(): - config_str = """ - MPC_ACCOUNT_ID=account123 - MPC_LOCAL_ADDRESS=127.0.0.1 - # A comment - MPC_ENV=testnet - """ - env = parse_env_string(config_str) - - assert env["MPC_ACCOUNT_ID"] == "account123" - assert env["MPC_LOCAL_ADDRESS"] == "127.0.0.1" - assert env["MPC_ENV"] == "testnet" - - -def test_config_ignores_blank_lines_and_comments(): - config_str = """ - - # This is a comment - MPC_SECRET_STORE_KEY=topsecret - - """ - env = parse_env_string(config_str) - - assert env["MPC_SECRET_STORE_KEY"] == "topsecret" - assert len(env) == 1 - - -def test_config_skips_malformed_lines(): - config_str = """ - GOOD_KEY=value - bad_line_without_equal - ANOTHER_GOOD=ok - = - """ - env = parse_env_string(config_str) - - assert "GOOD_KEY" in env - assert "ANOTHER_GOOD" in env - assert "bad_line_without_equal" not in env - assert "" not in env # ensure empty keys are skipped - - -def test_config_overrides_duplicate_keys(): - config_str = """ - MPC_ACCOUNT_ID=first - MPC_ACCOUNT_ID=second - """ - env = parse_env_string(config_str) - - assert env["MPC_ACCOUNT_ID"] == "second" # last one wins - - -# test valid and invalid host entries and port mappings - - -def test_valid_port_mapping(): - assert is_valid_port_mapping("11780:11780") - assert not is_valid_port_mapping("65536:11780") - assert not is_valid_port_mapping("--volume /:/mnt") - - -def test_build_docker_cmd_sanitizes_ports_and_hosts(): - env = { - "PORTS": TEST_PORTS_WITH_INJECTION, - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - func_name = inspect.currentframe().f_code.co_name - print(f"[{func_name}] CMD:", " ".join(cmd)) - - assert "--env" in cmd - assert f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}" in cmd - assert "-p" in cmd - assert "11780:11780" in cmd - - # Make sure injection strings were filtered - assert not any("BAD=1" in arg for arg in cmd) - assert not any("/:/mnt" in arg for arg in cmd) - - -def test_ports_does_not_allow_volume_injection(): - env = { - "PORTS": "2200:2200,--volume /:/mnt", - "MPC_ACCOUNT_ID": "safe", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert "2200:2200" in cmd - assert not any("/:/mnt" in arg for arg in cmd) - - -def test_invalid_env_key_is_ignored(): - env = { - "BAD_KEY": "should_not_be_used", - "MPC_ACCOUNT_ID": "safe", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert "should_not_be_used" not in " ".join(cmd) - assert "MPC_ACCOUNT_ID=safe" in cmd - - -def test_mpc_backup_encryption_key_is_allowed(): - env = { - "MPC_BACKUP_ENCRYPTION_KEY_HEX": "0000000000000000000000000000000000000000000000000000000000000000", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert ( - "MPC_BACKUP_ENCRYPTION_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000000" - in " ".join(cmd) - ) - - -def test_env_value_with_shell_injection_is_handled_safely(): - env = { - "MPC_ACCOUNT_ID": "safe; rm -rf /", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert "--env" in cmd - assert "MPC_ACCOUNT_ID=safe; rm -rf /" in cmd - - -def test_parse_and_build_docker_cmd_full_flow(): - config_str = """ - # Valid entries - MPC_ACCOUNT_ID=test-user - PORTS=11780:11780, --env BAD=oops - IMAGE_HASH=sha256:abc123 - """ - - env = parse_env_string(config_str) - image_hash = env.get("IMAGE_HASH", "sha256:default") - - cmd = build_docker_cmd(launcher.Platform.TEE, env, image_hash) - - print(f"[{inspect.currentframe().f_code.co_name}] CMD: {' '.join(cmd)}") - - assert "--env" in cmd - assert "MPC_ACCOUNT_ID=test-user" in cmd - assert "-p" in cmd - assert "11780:11780" in cmd - - # Confirm malicious injection is blocked - assert not any("--env BAD=oops" in s or "oops" in s for s in cmd) - - -# Test that ensures LD_PRELOAD cannot be injected into the docker command -def test_ld_preload_injection_blocked1(): - # Set up the environment variable with a dangerous LD_PRELOAD value - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "--env LD_PRELOAD": "/path/to/my/malloc.so", # The dangerous value - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not included in the command - assert "--env" in docker_cmd # Ensure there is an env var - assert ( - "LD_PRELOAD" not in docker_cmd - ) # Make sure LD_PRELOAD is not in the generated command - - # Alternatively, if you're using a regex to ensure safe environment variables - assert not any( - "-e " in arg for arg in docker_cmd - ) # Ensure no CLI injection for LD_PRELOAD - - -# Additional tests can go here for host/port validation - - -# Test that ensures LD_PRELOAD cannot be injected through ports -def test_ld_preload_in_ports1(): - # Set up environment with malicious PORTS containing LD_PRELOAD - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "PORTS": "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so", - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not part of the port mappings in the docker command - assert "-p" in docker_cmd # Ensure port mappings are included - assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command - - # Check that there are no malicious injections - assert not any( - "--env LD_PRELOAD" in arg for arg in docker_cmd - ) # No environment injection - - -# Additional tests could go here to check other edge cases - - -# Test that ensures LD_PRELOAD cannot be injected through mpc account id -def test_ld_preload_in_mpc_account_id(): - # Set up environment containing LD_PRELOAD - malicious_env = { - "MPC_ACCOUNT_ID": f"{TEST_MPC_ACCOUNT_ID}, --env LD_PRELOAD=/path/to/my/malloc.so", - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not part of the docker command - assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command - - # Check that there are no malicious injections - print(docker_cmd) - assert not any( - "--env LD_PRELOAD" in arg for arg in docker_cmd - ) # No environment injection - - -# Test that ensures LD_PRELOAD cannot be injected into the docker command -def test_ld_preload_injection_blocked2(): - # Set up the environment variable with a dangerous LD_PRELOAD value - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "-e LD_PRELOAD": "/path/to/my/malloc.so", # The dangerous value - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - assert ( - "-e LD_PRELOAD" not in docker_cmd - ) # Make sure LD_PRELOAD is not in the generated command - - -# Additional tests can go here for host/port validation - - -# Test that ensures LD_PRELOAD cannot be injected through ports -def test_ld_preload_in_ports2(): - # Set up environment with malicious PORTS containing LD_PRELOAD - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "PORTS": "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so", - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not part of the port mappings in the docker command - assert "-p" in docker_cmd # Ensure port mappings are included - assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command - - -def test_json_key_matches_node(): - """ - Ensure the JSON key used by the launcher to read approved image hashes - stays aligned with the MPC node implementation. - mpc/crates/node/src/tee/allowed_image_hashes_watcher.rs -> JSON_KEY_APPROVED_HASHES - - If this test fails, it means the launcher and MPC node are using different - JSON field names, which would break MPC hash propagation. - """ - assert launcher.JSON_KEY_APPROVED_HASHES == "approved_hashes" - - -def test_override_present(monkeypatch): - override_value = "sha256:" + "a" * 64 - approved = ["sha256:" + "b" * 64, override_value, "sha256:" + "c" * 64] - - fake_json = make_digest_json(approved) - - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) - monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) - - dstack_config = {ENV_VAR_MPC_HASH_OVERRIDE: override_value} - - selected = load_and_select_hash(dstack_config) - assert selected == override_value - - -def test_override_not_present(monkeypatch): - approved = ["sha256:aaa", "sha256:bbb"] - fake_json = make_digest_json(approved) - - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) - monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) - - dstack_config = { - ENV_VAR_MPC_HASH_OVERRIDE: "sha256:xyz" # NOT in list - } - - with pytest.raises(RuntimeError): - load_and_select_hash(dstack_config) - - -def test_no_override_picks_newest(monkeypatch): - approved = ["sha256:newest", "sha256:older", "sha256:oldest"] - fake_json = make_digest_json(approved) - - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) - monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) - - selected = load_and_select_hash({}) - assert selected == "sha256:newest" - - -def test_missing_file_fallback(monkeypatch): - # Pretend file does NOT exist - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: False) - - # Valid fallback digest (64 hex chars) - monkeypatch.setenv(ENV_VAR_DEFAULT_IMAGE_DIGEST, "a" * 64) - - selected = load_and_select_hash({}) - assert selected == "sha256:" + "a" * 64 - - -TEST_DIGEST = "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372" -# Important: ensure the config matches your test image -DSTACK_CONFIG = { - "MPC_IMAGE_TAGS": "83b52da4e2270c688cdd30da04f6b9d3565f25bb", - "MPC_IMAGE_NAME": "nearone/testing", - "MPC_REGISTRY": "registry.hub.docker.com", -} - -# Launcher defaults -RPC_REQUEST_TIMEOUT_SECS = 10.0 -RPC_REQUEST_INTERVAL_SECS = 1.0 -RPC_MAX_ATTEMPTS = 20 - - -# ------------------------------------------------------------------------------------ -# NOTE: Integration Test (External Dependency) -# -# This test validates that `validate_image_hash()` correctly: -# - contacts the real Docker registry, -# - resolves the manifest digest, -# - pulls the remote image, -# - and verifies that its sha256 digest matches the expected immutable value. -# -# The test image is a **pre-built, minimal Docker image containing only a tiny -# binary**, created intentionally for performance and fast pulls. -# This image is uploaded to Docker Hub together. -# -# IMPORTANT: -# • The digest in this test corresponds EXACTLY to that pre-built image. -# • Dockerfile used to build the image can be found at mpc/tee_launcher/launcher-test-image/Dockerfile -# • If the test image is rebuilt, the digest MUST be updated here. -# • If the registry is unavailable or slow, this test may fail. -# • CI will run this only if explicitly enabled. -# -# Please read that file before modifying the digest, registry, or test behavior. -# ------------------------------------------------------------------------------------ -def test_validate_image_hash(): - result = validate_image_hash( - TEST_DIGEST, - DSTACK_CONFIG, - RPC_REQUEST_TIMEOUT_SECS, - RPC_REQUEST_INTERVAL_SECS, - RPC_MAX_ATTEMPTS, - ) - assert result is True, "validate_image_hash() failed for test image" - - -# test launcher support for non TEE images. - - -class DummyProc: - def __init__(self, returncode=0, stdout=b"", stderr=b""): - self.returncode = returncode - self.stdout = stdout - self.stderr = stderr - - -@pytest.fixture -def base_env(monkeypatch): - # Required by launcher - monkeypatch.setenv(launcher.OS_ENV_DOCKER_CONTENT_TRUST, "1") - monkeypatch.setenv(launcher.ENV_VAR_DEFAULT_IMAGE_DIGEST, "sha256:" + "a" * 64) - - -def test_parse_platform_missing(monkeypatch, base_env): - monkeypatch.delenv(launcher.ENV_VAR_PLATFORM, raising=False) - with pytest.raises(RuntimeError): - launcher.parse_platform() - - -@pytest.mark.parametrize("val", ["", "foo", "TEE as", "NON_TEE", "1", "tee", "nontee"]) -def test_parse_platform_invalid(monkeypatch, base_env, val): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, val) - with pytest.raises(RuntimeError): - launcher.parse_platform() - - -@pytest.mark.parametrize( - "val,expected", - [ - ("TEE", launcher.Platform.TEE), - ("NONTEE", launcher.Platform.NONTEE), - ], -) -def test_parse_platform_valid(monkeypatch, base_env, val, expected): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, val) - assert launcher.parse_platform() is expected - - -def test_extend_rtmr3_nontee_skips_dstack(monkeypatch, base_env): - called = {"count": 0} - - def fake_curl(*args, **kwargs): - called["count"] += 1 - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) - - launcher.extend_rtmr3(launcher.Platform.NONTEE, "sha256:" + "b" * 64) - assert called["count"] == 0 - - -def test_extend_rtmr3_tee_requires_socket(monkeypatch, base_env): - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: False) - with pytest.raises(RuntimeError): - launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) - - -def test_extend_rtmr3_tee_getquote_fail(monkeypatch, base_env): - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: True) - - def fake_curl(endpoint, payload, capture_output=False): - # Fail only GetQuote - if endpoint == "GetQuote": - return DummyProc(returncode=7) - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) - with pytest.raises(RuntimeError, match="GetQuote failed"): - launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) - - -def test_extend_rtmr3_tee_emitevent_fail(monkeypatch, base_env): - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: True) - - def fake_curl(endpoint, payload, capture_output=False): - if endpoint == "GetQuote": - return DummyProc(returncode=0) - if endpoint == "EmitEvent": - return DummyProc(returncode=55) - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) - with pytest.raises(RuntimeError, match="EmitEvent failed"): - launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) - - -def test_build_docker_cmd_nontee_no_dstack_mount(base_env): - env = { - "MPC_ACCOUNT_ID": "x", - # launcher-only env should be ignored - launcher.ENV_VAR_RPC_MAX_ATTEMPTS: "5", - } - cmd = launcher.build_docker_cmd(launcher.Platform.NONTEE, env, "sha256:" + "c" * 64) - s = " ".join(cmd) - - assert "DSTACK_ENDPOINT=" not in s - assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" not in s - - -def test_build_docker_cmd_tee_has_dstack_mount(base_env): - env = {"MPC_ACCOUNT_ID": "x"} - cmd = launcher.build_docker_cmd(launcher.Platform.TEE, env, "sha256:" + "c" * 64) - s = " ".join(cmd) - - assert f"DSTACK_ENDPOINT={launcher.DSTACK_UNIX_SOCKET}" in s - assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" in s - - -def test_main_tee_fails_closed_before_launch(monkeypatch, base_env): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, launcher.Platform.TEE.value) - - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: False) - - # prevent any real docker/network - monkeypatch.setattr( - launcher, "load_and_select_hash", lambda cfg: "sha256:" + "d" * 64 - ) - monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) - monkeypatch.setattr( - launcher, - "launch_mpc_container", - lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not launch")), - ) - - with pytest.raises(RuntimeError, match="requires dstack unix socket"): - launcher.main() - - -def test_main_nontee_skips_extend_and_launches(monkeypatch, base_env): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, "NONTEE") - monkeypatch.setattr( - launcher, "is_unix_socket", lambda p: False - ) # should not matter - - monkeypatch.setattr( - launcher, "load_and_select_hash", lambda cfg: "sha256:" + "d" * 64 - ) - monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) - - called = {"extend": 0, "launch": 0} - monkeypatch.setattr( - launcher, - "extend_rtmr3", - lambda platform, h: called.__setitem__("extend", called["extend"] + 1), - ) - monkeypatch.setattr( - launcher, - "launch_mpc_container", - lambda platform, h, cfg: called.__setitem__("launch", called["launch"] + 1), - ) - - launcher.main() - assert called["extend"] == 1 - assert called["launch"] == 1 - - -def assert_subsequence(seq, subseq): - it = iter(seq) - for x in subseq: - for y in it: - if y == x: - break - else: - raise AssertionError(f"Missing subsequence item: {x}\nseq={seq}") - - -def test_main_nontee_builds_expected_mpc_docker_cmd(monkeypatch, tmp_path): - """ - Verify that launcher.main() builds the correct MPC docker command in NONTEE mode. - - Steps: - 1. Configure the launcher to run with PLATFORM=NONTEE. - 2. Set required environment variables (DOCKER_CONTENT_TRUST, DEFAULT_IMAGE_DIGEST). - 3. Create a temporary user_config file with MPC env vars, ports, and extra hosts. - 4. Simulate a missing IMAGE_DIGEST_FILE so the launcher falls back to DEFAULT_IMAGE_DIGEST. - 5. Stub image validation and docker interactions to avoid real network or docker usage. - 6. Invoke launcher.main(). - 7. Capture the docker run command used to start the MPC container. - 8. Assert that the command: - - Includes expected MPC configuration (env vars, ports, hosts, volumes). - - Does NOT include dstack socket mounts or DSTACK_ENDPOINT. - - Filters out injection attempts in ports and hosts. - - Uses the expected full image digest. - """ - # --- Arrange: environment (NONTEE) --- - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, launcher.Platform.NONTEE.value) - monkeypatch.setenv(launcher.OS_ENV_DOCKER_CONTENT_TRUST, "1") - - default_digest = "sha256:" + "a" * 64 - monkeypatch.setenv(launcher.ENV_VAR_DEFAULT_IMAGE_DIGEST, default_digest) - - # Provide a temp user config file so main() passes env into build_docker_cmd() - user_config = tmp_path / "user_config" - user_config.write_text( - "\n".join( - [ - f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}", - f"PORTS={TEST_PORTS_WITH_INJECTION}", # injection should be ignored - ] - ) - + "\n" - ) - - # Point launcher at our temp config - monkeypatch.setattr(launcher, "DSTACK_USER_CONFIG_FILE", str(user_config)) - - # Make IMAGE_DIGEST_FILE "missing" so DEFAULT_IMAGE_DIGEST is used - def fake_isfile(path: str) -> bool: - if path == launcher.IMAGE_DIGEST_FILE: - return False - if path == str(user_config): - return True - return os.path.isfile(path) - - monkeypatch.setattr(launcher.os.path, "isfile", fake_isfile) - - # Avoid network/docker verification in validate_image_hash - monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) - - # Avoid remove_existing_container touching real docker - monkeypatch.setattr(launcher, "check_output", lambda *a, **k: "") - - # Capture the docker run command used to launch MPC - captured = {"docker_run_cmd": None} - - def fake_run(cmd, *args, **kwargs): - # cmd is a list[str] - if ( - isinstance(cmd, list) - and len(cmd) >= 2 - and cmd[0] == "docker" - and cmd[1] == "run" - ): - captured["docker_run_cmd"] = cmd - return DummyProc(returncode=0) - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "run", fake_run) - - # --- Act --- - launcher.main() - - # --- Assert --- - cmd = captured["docker_run_cmd"] - assert cmd is not None, "Expected launcher to invoke 'docker run' for MPC container" - - cmd_str = " ".join(cmd) - - # NONTEE invariants - assert "DSTACK_ENDPOINT=" not in cmd_str - assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" not in cmd_str - - # Expected env propagation + sanitization - assert f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}" in cmd_str - assert "-p" in cmd and "11780:11780" in cmd_str - - # Injection strings filtered out - assert "BAD=1" not in cmd_str - assert "/:/mnt" not in cmd_str - - # Required mounts / flags from build_docker_cmd - assert "--security-opt" in cmd_str - assert "no-new-privileges:true" in cmd_str - assert "/tapp:/tapp:ro" in cmd_str - assert "shared-volume:/mnt/shared" in cmd_str - assert "mpc-data:/data" in cmd_str - assert f"--name {launcher.MPC_CONTAINER_NAME}" in cmd_str - - # Image digest should be the final argument and should be the FULL digest - assert cmd[-1] == default_digest - - expected_core = [ - "docker", - "run", - "--security-opt", - "no-new-privileges:true", - "-v", - "/tapp:/tapp:ro", - "-v", - "shared-volume:/mnt/shared", - "-v", - "mpc-data:/data", - "--name", - launcher.MPC_CONTAINER_NAME, - "--detach", - ] - assert_subsequence(cmd, expected_core) - - -def _base_env(): - # Minimal env for build_docker_cmd (launcher will add required MPC_IMAGE_HASH etc itself) - return { - "MPC_ACCOUNT_ID": "mpc-user-123", - "MPC_CONTRACT_ID": "contract.near", - "MPC_ENV": "testnet", - "MPC_HOME_DIR": "/data", - "NEAR_BOOT_NODES": "boot1,boot2", - "RUST_LOG": "info", - } - - -def test_has_control_chars_rejects_newline_and_cr(): - assert _has_control_chars("a\nb") is True - assert _has_control_chars("a\rb") is True - - -def test_has_control_chars_rejects_other_control_chars_but_allows_tab(): - # tab is allowed by the Python helper in the patched launcher - assert _has_control_chars("a\tb") is False - # ASCII control char 0x1F should be rejected - assert _has_control_chars("a" + chr(0x1F) + "b") is True - - -def test_is_safe_env_value_rejects_control_chars(): - assert is_safe_env_value("ok\nno") is False - assert is_safe_env_value("ok\rno") is False - assert is_safe_env_value("ok" + chr(0x1F) + "no") is False - - -def test_is_safe_env_value_rejects_ld_preload_substring(): - assert is_safe_env_value("LD_PRELOAD=/tmp/x.so") is False - assert is_safe_env_value("foo LD_PRELOAD bar") is False - - -def test_is_safe_env_value_rejects_too_long_value(): - assert is_safe_env_value("a" * (MAX_ENV_VALUE_LEN + 1)) is False - assert is_safe_env_value("a" * MAX_ENV_VALUE_LEN) is True - - -def testis_allowed_container_env_key_allows_mpc_prefix_uppercase(): - assert is_allowed_container_env_key("MPC_FOO") is True - assert is_allowed_container_env_key("MPC_FOO_123") is True - assert is_allowed_container_env_key("MPC_A_B_C") is True - - -def testis_allowed_container_env_key_rejects_lowercase_or_invalid_chars(): - assert is_allowed_container_env_key("MPC_foo") is False - assert is_allowed_container_env_key("MPC-FOO") is False - assert is_allowed_container_env_key("MPC.FOO") is False - assert is_allowed_container_env_key("MPC_") is False - - -def testis_allowed_container_env_key_allows_compat_non_mpc_keys(): - assert is_allowed_container_env_key("RUST_LOG") is True - assert is_allowed_container_env_key("RUST_BACKTRACE") is True - assert is_allowed_container_env_key("NEAR_BOOT_NODES") is True - - -def testis_allowed_container_env_key_denies_sensitive_keys(): - assert is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY") is False - assert is_allowed_container_env_key("MPC_ACCOUNT_SK") is False - - -def test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars(): - env = _base_env() - env["MPC_NEW_FEATURE_FLAG"] = "1" - env["MPC_SOME_CONFIG"] = "value" - - cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - - cmd_str = " ".join(cmd) - assert "--env MPC_NEW_FEATURE_FLAG=1" in cmd_str - assert "--env MPC_SOME_CONFIG=value" in cmd_str - - -def test_build_docker_cmd_blocks_sensitive_mpc_private_keys(): - env = _base_env() - env["MPC_P2P_PRIVATE_KEY"] = "supersecret" - env["MPC_ACCOUNT_SK"] = "supersecret2" - - cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - cmd_str = " ".join(cmd) - - assert "MPC_P2P_PRIVATE_KEY" not in cmd_str - assert "MPC_ACCOUNT_SK" not in cmd_str - - -def test_build_docker_cmd_rejects_env_value_with_newline(): - env = _base_env() - env["MPC_NEW_FEATURE_FLAG"] = "ok\nbad" - - cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - cmd_str = " ".join(cmd) - - # It should be ignored (not passed) - assert "MPC_NEW_FEATURE_FLAG" not in cmd_str - - -def test_build_docker_cmd_enforces_max_env_count_cap(): - env = _base_env() - # add many MPC_* keys to exceed cap - for i in range(MAX_PASSTHROUGH_ENV_VARS + 1): - env[f"MPC_X_{i}"] = "1" - - with pytest.raises(RuntimeError, match="Too many env vars"): - build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - - -def test_build_docker_cmd_enforces_total_env_bytes_cap(): - env = _base_env() - - # Each env contributes ~ len(key)+1+MAX_ENV_VALUE_LEN bytes. - # With MAX_ENV_VALUE_LEN=1024 and MAX_TOTAL_ENV_BYTES=32768, ~35-40 vars will exceed the cap. - for i in range(40): - env[f"MPC_BIG_{i}"] = "a" * MAX_ENV_VALUE_LEN - - with pytest.raises(RuntimeError, match="Total env payload too large"): - build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) diff --git a/tee_launcher/user-config.conf b/tee_launcher/user-config.conf deleted file mode 100644 index 0c8980d62..000000000 --- a/tee_launcher/user-config.conf +++ /dev/null @@ -1,18 +0,0 @@ -# Optional override parameters to find fetch the MPC docker image. -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=3.6.0 -MPC_REGISTRY=registry.hub.docker.com - -MPC_ACCOUNT_ID=mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=mpc-contract-barak-launch1-4c5e2fe1fb42.5035bf56abb0.testnet -MPC_ENV=testnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=mpc=debug,info - -NEAR_BOOT_NODES=ed25519:9qyu1RaJ5shX6UEb7UooPQYVXCC1tNHCiDPPxJ8Pv1UJ@116.202.220.238:34567,ed25519:8mzYnfuT5zQYqV99CfYAX6XoRmNxVJ1nAZHXXW4GrFD@34.221.144.70:24567,ed25519:B87Qq34LbWadFx2dq5bwUEtB5KBgr8ZhsoEpAiSP2qVX@142.132.203.80:24567,ed25519:EufXMhFVixgFpg2bBaHGL4Zrks1DDrhAZTQYwbjRTAUX@65.109.25.109:24567,ed25519:HJJde5skATXLA4wGk8P9awvfzaW47tCU2EsRXnMoFRA9@129.150.39.19:24567,ed25519:BavpjuYrnXRFQVWjLdx9vx9vAvanit9NhhcPeM6gjAkE@95.217.198.233:24567,ed25519:81zk9MvvoxB1AzTW721o9m2NeYx3pDFDZyRJUQej65uc@195.14.6.172:24567,ed25519:E4gQXBovauvqxx85TdemezhkDDsAsqEL7ZJ4cp5Cdhsb@129.80.119.109:24567,ed25519:6cWtXFAzqpZ8D7EpLGYBmkw95oKYkzN8i99UcRgsyRMy@164.132.247.155:24567,ed25519:CLnWy9xv2GUqfgepzLwpv4bozj3H3kgzjbVREyS6wcqq@47.242.112.172:24567,ed25519:2NmT9Wy9HGBmH8sTWSq2QfaMk4R8ZHBEhk8ZH4g4f1Qk@65.109.88.175:24567,ed25519:9dhPYd1ArZ6mTMP7nnRzm8JBPwKCaBxiYontS5KfXz5h@34.239.1.54:24567,ed25519:8iiQH4vtqsqWgsm4ypCJQQwqJR3AGp9o7F69YRaCHKxA@141.95.204.11:24567,ed25519:4L97JnFFFVbfE8M3tY9bRtgV5376y5dFH8cSaoBDRWnK@5.199.170.103:24567,ed25519:DGJ91V2wJ8NFpkqZvphtSeM4CBeiLsrHGdinTugiRoFF@52.35.74.212:24567,ed25519:B9LSvCTimoEUtuUvpfu1S54an54uTetVabmkT5dELUCN@91.134.22.129:24567,ed25519:cRGmtzkkSZT6wXNjbthSXMD6dHrEgSeDtiEJAcnLLxH@15.204.213.166:24567 -# needed: Port forwarding - telemetry. -PORTS=8080:8080,3030:3030,80:80,24567:24567 - From 7d4074eeef1ee45baef7e83f2c1d92638192a898 Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Wed, 25 Mar 2026 16:54:29 +0000 Subject: [PATCH 2/9] chore: restore old Python launcher files for separate removal in #2615 Restore all tee_launcher/ files and old .conf configs from main branch. These coexist alongside the new Rust launcher and deployment configs. The old files will be removed in a dedicated follow-up PR (#2615). Co-Authored-By: Claude Opus 4.6 (1M context) --- deployment/localnet/tee/frodo.conf | 19 + deployment/localnet/tee/sam.conf | 19 + deployment/testnet/frodo.conf | 19 + deployment/testnet/sam.conf | 19 + tee_launcher/__init__.py | 0 tee_launcher/configs/kms.env | 48 + tee_launcher/configs/sgx.env | 47 + tee_launcher/default.env | 49 + tee_launcher/deploy-launcher-guide.md | 154 +++ tee_launcher/deploy-launcher.sh | 232 +++++ tee_launcher/launcher-test-image/Dockerfile | 3 + tee_launcher/launcher.md | 102 ++ tee_launcher/launcher.py | 873 ++++++++++++++++++ tee_launcher/launcher_docker_compose.yaml | 30 + .../launcher_docker_compose_nontee.yaml | 28 + tee_launcher/requirements.txt | 2 + tee_launcher/test_launcher.py | 32 + tee_launcher/test_launcher_config.py | 844 +++++++++++++++++ tee_launcher/user-config.conf | 18 + .../using-the-launcher-in-nontee-setup.md | 147 +++ 20 files changed, 2685 insertions(+) create mode 100644 deployment/localnet/tee/frodo.conf create mode 100644 deployment/localnet/tee/sam.conf create mode 100644 deployment/testnet/frodo.conf create mode 100644 deployment/testnet/sam.conf create mode 100644 tee_launcher/__init__.py create mode 100644 tee_launcher/configs/kms.env create mode 100644 tee_launcher/configs/sgx.env create mode 100644 tee_launcher/default.env create mode 100644 tee_launcher/deploy-launcher-guide.md create mode 100755 tee_launcher/deploy-launcher.sh create mode 100644 tee_launcher/launcher-test-image/Dockerfile create mode 100644 tee_launcher/launcher.md create mode 100644 tee_launcher/launcher.py create mode 100644 tee_launcher/launcher_docker_compose.yaml create mode 100644 tee_launcher/launcher_docker_compose_nontee.yaml create mode 100644 tee_launcher/requirements.txt create mode 100644 tee_launcher/test_launcher.py create mode 100644 tee_launcher/test_launcher_config.py create mode 100644 tee_launcher/user-config.conf create mode 100644 tee_launcher/using-the-launcher-in-nontee-setup.md diff --git a/deployment/localnet/tee/frodo.conf b/deployment/localnet/tee/frodo.conf new file mode 100644 index 000000000..8d75a8465 --- /dev/null +++ b/deployment/localnet/tee/frodo.conf @@ -0,0 +1,19 @@ +# MPC Docker image override +MPC_IMAGE_NAME=nearone/mpc-node +MPC_IMAGE_TAGS=main-260e88b +MPC_REGISTRY=registry.hub.docker.com + +# MPC node settings +MPC_ACCOUNT_ID=frodo.test.near +MPC_LOCAL_ADDRESS=127.0.0.1 +MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +MPC_CONTRACT_ID=mpc-contract.test.near +MPC_ENV=mpc-localnet +MPC_HOME_DIR=/data +RUST_BACKTRACE=full +RUST_LOG=info + +NEAR_BOOT_NODES=ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566 + +# Port forwarding +PORTS=8080:8080,24566:24566,13001:13001 \ No newline at end of file diff --git a/deployment/localnet/tee/sam.conf b/deployment/localnet/tee/sam.conf new file mode 100644 index 000000000..507084bf2 --- /dev/null +++ b/deployment/localnet/tee/sam.conf @@ -0,0 +1,19 @@ +# MPC Docker image override +MPC_IMAGE_NAME=nearone/mpc-node +MPC_IMAGE_TAGS=main-260e88b +MPC_REGISTRY=registry.hub.docker.com + +# MPC node settings +MPC_ACCOUNT_ID=sam.test.near +MPC_LOCAL_ADDRESS=127.0.0.1 +MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +MPC_CONTRACT_ID=mpc-contract.test.near +MPC_ENV=mpc-localnet +MPC_HOME_DIR=/data +RUST_BACKTRACE=full +RUST_LOG=info + +NEAR_BOOT_NODES=ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566 + +# Port forwarding +PORTS=8080:8080,24566:24566,13002:13002 \ No newline at end of file diff --git a/deployment/testnet/frodo.conf b/deployment/testnet/frodo.conf new file mode 100644 index 000000000..a6705e1ce --- /dev/null +++ b/deployment/testnet/frodo.conf @@ -0,0 +1,19 @@ +# MPC Docker image override +MPC_IMAGE_NAME=nearone/mpc-node +MPC_IMAGE_TAGS=barak-doc-update_localnet_guide-b12bc7d +MPC_REGISTRY=registry.hub.docker.com + +# MPC node settings +MPC_ACCOUNT_ID=$FRODO_ACCOUNT +MPC_LOCAL_ADDRESS=127.0.0.1 +MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +MPC_CONTRACT_ID=$MPC_CONTRACT_ACCOUNT +MPC_ENV=testnet +MPC_HOME_DIR=/data +RUST_BACKTRACE=full +RUST_LOG=info + +NEAR_BOOT_NODES=$BOOTNODES + +# Port forwarding +PORTS=8080:8080,24567:24567,13001:13001,80:80 \ No newline at end of file diff --git a/deployment/testnet/sam.conf b/deployment/testnet/sam.conf new file mode 100644 index 000000000..4e63db03b --- /dev/null +++ b/deployment/testnet/sam.conf @@ -0,0 +1,19 @@ +# MPC Docker image override +MPC_IMAGE_NAME=nearone/mpc-node +MPC_IMAGE_TAGS=barak-doc-update_localnet_guide-b12bc7d +MPC_REGISTRY=registry.hub.docker.com + +# MPC node settings +MPC_ACCOUNT_ID=$SAM_ACCOUNT +MPC_LOCAL_ADDRESS=127.0.0.1 +MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +MPC_CONTRACT_ID=$MPC_CONTRACT_ACCOUNT +MPC_ENV=testnet +MPC_HOME_DIR=/data +RUST_BACKTRACE=full +RUST_LOG=info + +NEAR_BOOT_NODES=$BOOTNODES + +# Port forwarding +PORTS=8080:8080,24567:24567,13002:13002,80:80 \ No newline at end of file diff --git a/tee_launcher/__init__.py b/tee_launcher/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tee_launcher/configs/kms.env b/tee_launcher/configs/kms.env new file mode 100644 index 000000000..53229e022 --- /dev/null +++ b/tee_launcher/configs/kms.env @@ -0,0 +1,48 @@ +# Required environment variables for Launcher / MPC app deployment. +# Please uncomment and set values for the following variables: + +APP_NAME="launcher_test_app" + +# The URL of the dstack-vmm RPC service used to deploy the KMS app +#VMM_RPC=unix:../../../build/vmm.sock +VMM_RPC=http://127.0.0.1:16000 + +# The type of sealing key to used by the VM (SGX,KMS) +# KMS should only be used for development and SGX for production. +SEALING_KEY_TYPE=KMS + +# Port on the host machine to connect to the dstack guest agent +EXTERNAL_DSTACK_AGENT_PORT=127.0.0.1:9206 + +# SSH ports (only for dev images) +EXTERNAL_SSH_PORT=127.0.0.1:9207 + +# External MPC ports (host machine) +EXTERNAL_MPC_PUBLIC_DEBUG_PORT=0.0.0.0:8080 +EXTERNAL_MPC_LOCAL_DEBUG_PORT=127.0.0.1:3030 +EXTERNAL_MPC_DECENTRALIZED_STATE_SYNC=0.0.0.0:24567 +EXTERNAL_MPC_MAIN_PORT=0.0.0.0:80 + +# Internal MPC ports (inside CVM) +INTERNAL_MPC_PUBLIC_DEBUG_PORT=8080 +INTERNAL_MPC_LOCAL_DEBUG_PORT=3030 +INTERNAL_MPC_DECENTRALIZED_STATE_SYNC=24567 +INTERNAL_MPC_MAIN_PORT=80 + + + + +# The Dstack OS image name use for the CVM + +# production OS image +#OS_IMAGE=dstack-0.5.8 + +# development OS image +OS_IMAGE=dstack-dev-0.5.8 + +# Path of the launcher docker_compose_file +DOCKER_COMPOSE_FILE_PATH=launcher_docker_compose.yaml +# Path of the user_config file +USER_CONFIG_FILE_PATH=user-config.conf +# for testing use a smaller disk size 128G +DISK=500G \ No newline at end of file diff --git a/tee_launcher/configs/sgx.env b/tee_launcher/configs/sgx.env new file mode 100644 index 000000000..164d1eb4f --- /dev/null +++ b/tee_launcher/configs/sgx.env @@ -0,0 +1,47 @@ +# Required environment variables for Launcher / MPC app deployment. +# Please uncomment and set values for the following variables: + +APP_NAME="launcher_test_app" + +# The URL of the dstack-vmm RPC service used to deploy the KMS app +#VMM_RPC=unix:../../../build/vmm.sock +VMM_RPC=http://127.0.0.1:11100 + + +# The type of sealing key to used by the VM (SGX,KMS) +# KMS should only be used for development and SGX for production. +SEALING_KEY_TYPE=SGX + +# Port on the host machine to connect to the dstack guest agent +EXTERNAL_DSTACK_AGENT_PORT=127.0.0.1:9208 + +# SSH ports (only for dev images) +EXTERNAL_SSH_PORT=127.0.0.1:9207 + +# External MPC ports (host machine) +EXTERNAL_MPC_PUBLIC_DEBUG_PORT=0.0.0.0:8989 +EXTERNAL_MPC_LOCAL_DEBUG_PORT=127.0.0.1:3030 +EXTERNAL_MPC_DECENTRALIZED_STATE_SYNC=0.0.0.0:24567 +EXTERNAL_MPC_MAIN_PORT=0.0.0.0:80 + +# Internal MPC ports (inside CVM) +INTERNAL_MPC_PUBLIC_DEBUG_PORT=8080 +INTERNAL_MPC_LOCAL_DEBUG_PORT=3030 +INTERNAL_MPC_DECENTRALIZED_STATE_SYNC=24567 +INTERNAL_MPC_MAIN_PORT=80 + +# The dstack OS image name use for the CVM + +# production OS image +#OS_IMAGE=dstack-0.5.8 + +# development OS image +OS_IMAGE=dstack-dev-0.5.8 + +# Path of the launcher docker_compose_file +DOCKER_COMPOSE_FILE_PATH=launcher_docker_compose.yaml +# Path of the user_config file +USER_CONFIG_FILE_PATH=user-config.conf + +# for testing use a smaller disk size 128G +DISK=500G \ No newline at end of file diff --git a/tee_launcher/default.env b/tee_launcher/default.env new file mode 100644 index 000000000..7a3d7e264 --- /dev/null +++ b/tee_launcher/default.env @@ -0,0 +1,49 @@ +# Required environment variables for Launcher / MPC app deployment. +# Please uncomment and set values for the following variables: + +APP_NAME="launcher_test_app" + +# VMM_RPC -The RPC of the dstack-vmm RPC service used to deploy CVMs +# Can either be a unix socket or an http address. (make sure this is the same configuration as in the dstack-vmm service.toml) + +#VMM_RPC=unix:../../../build/vmm.sock + +VMM_RPC=http://127.0.0.1:10000 + +# The type of sealing key to used by the VM (SGX,KMS) +# KMS should only be used for development and SGX for production. +SEALING_KEY_TYPE=KMS + +# Port on the host machine to connect to the dstack guest agent +EXTERNAL_DSTACK_AGENT_PORT=127.0.0.1:9208 + + +EXTERNAL_SSH_PORT=127.0.0.1:9209 + +# External MPC ports (host machine) +EXTERNAL_MPC_PUBLIC_DEBUG_PORT=0.0.0.0:8989 +EXTERNAL_MPC_LOCAL_DEBUG_PORT=127.0.0.1:3030 +EXTERNAL_MPC_DECENTRALIZED_STATE_SYNC=0.0.0.0:24567 +EXTERNAL_MPC_MAIN_PORT=0.0.0.0:80 + +# Internal MPC ports (inside CVM) +INTERNAL_MPC_PUBLIC_DEBUG_PORT=8080 +INTERNAL_MPC_LOCAL_DEBUG_PORT=3030 +INTERNAL_MPC_DECENTRALIZED_STATE_SYNC=24567 +INTERNAL_MPC_MAIN_PORT=80 + +# The dstack OS image name use for the CVM + +# production OS image +#OS_IMAGE=dstack-0.5.8 + +# development OS image +OS_IMAGE=dstack-dev-0.5.8 + +# Path of the launcher docker_compose_file +DOCKER_COMPOSE_FILE_PATH=launcher_docker_compose.yaml +# Path of the user_config file +USER_CONFIG_FILE_PATH=user-config.conf + +# for testing use a smaller disk size 128G +DISK=500G \ No newline at end of file diff --git a/tee_launcher/deploy-launcher-guide.md b/tee_launcher/deploy-launcher-guide.md new file mode 100644 index 000000000..069c3a38f --- /dev/null +++ b/tee_launcher/deploy-launcher-guide.md @@ -0,0 +1,154 @@ +# 🛠 `deploy-launcher.sh` – Dstack VM Deployment Script + +This script automates the deployment of a **Dstack Launcher VM** (`launcher_test_app`) using a templated Docker Compose file and the Dstack VMM CLI. + +> **Note:** This script must be run **from within the server that hosts the VMs** (i.e., where `dstack-vmm` is running). + +It: + +- Loads deployment parameters from a `.env` file (defaults to `default.env`) +- Loads Docker Compose file and user\_config files +- Generates an `app-compose.json` configuration +- Deploys and starts CVM via `vmm-cli` + +--- + +## 📦 Requirements + +- A working Dstack setup and Dstack VMM service (`vmm-server`) accessible at `$VMM_RPC`. +See Phala's [setup guide](https://github.com/Dstack-TEE/dstack). +Also review specific MPC configuration in [running-an-mpc-node-in-tdx-external-guide.md](https://github.com/near/mpc/blob/main/docs/running-an-mpc-node-in-tdx-external-guide.md#mpc-node-setup-and-deployment) +- Python 3.6 or higher installed +- Required Python packages (cryptography, eth_keys, eth_utils) +- `vmm-cli.py` should be located under $basePath/vmm/src/vmm-cli.py +- Docker Compose template (`$DOCKER_COMPOSE_FILE_PATH`) +- Deployment configuration in `*.env` file +- user-config.conf file +- See full CLI documentation here: [vmm-cli-user-guide.md](https://github.com/Dstack-TEE/dstack/blob/master/docs/vmm-cli-user-guide.md) + +--- + +## 📂 Expected Files + +Ensure the following files are present in the working directory before running the script: + +- `default.env` – default environment configuration +- `$DOCKER_COMPOSE_FILE_PATH` – e.g. `launcher_docker_compose.yaml` +- `$USER_CONFIG_FILE_PATH` – e.g. `user-config.conf` + +You can also use the example `.env` files under `tee_deployment/configs/`: + +- `configs/kms.env` +- `configs/sgx.env` + +--- + +## 🚀 How to Use + +1. **Make the script executable** (if not already): + + ```bash + chmod +x deploy-launcher.sh + ``` + +2. **Run the script**, optionally specifying any of the following: + + ```bash + ./deploy-launcher.sh \ + --env-file tee_deployment/configs/sgx.env \ + --base-path /project \ + --python-exec /project/.venv/bin/python + ``` + + Or use just the `.env` override: + + ```bash + ./deploy-launcher.sh --env-file tee_deployment/configs/sgx.env + ``` + + Or use all defaults (`default.env`, default paths): + + ```bash + ./deploy-launcher.sh + ``` + +3. **Follow the prompt** to confirm deployment. + +--- + +## 🔧 Available Options + +| Option | Description | +| --------------------- | ------------------------------------------------------------------------- | +| `--env-file`, `-e` | Path to a `.env` file with deployment parameters (default: `default.env`) | +| `--base-path`, `-b` | Path to the parent directory containing the vmm folder . For example, if your Dstack installation is in /project/meta-dstack/dstack/vmm, then you should set --base-path /project/meta-dstack/dstack/ | +| `--python-exec`, `-p` | Path to the Python executable to use (default: under base path) | + +--- + +## 💡 Examples + +```bash +# Use KMS config from configs directory +./deploy-launcher.sh --env-file tee_deployment/configs/kms.env + +# Use SGX config +./deploy-launcher.sh --env-file tee_deployment/configs/sgx.env + +# Override Python path only +./deploy-launcher.sh --python-exec python3 + +# Override both base path (folder above meta-dstack) and Python path +./deploy-launcher.sh \ + --base-path /home/barak/project \ + --python-exec python3 +``` + +--- + +## 📄 `.env` File Format + +Make sure to create and fill in a `.env` file. Example (`default.env`): + +```env +APP_NAME=launcher_test_app +VMM_RPC=http://127.0.0.1:16000 + +# Sealing key type (KMS for deployment, SGX for production) +SEALING_KEY_TYPE=KMS + +# Port on the host machine to connect to the dstack guest agent +EXTERNAL_DSTACK_AGENT_PORT=127.0.0.1:9206 + +# SSH port on the host +EXTERNAL_SSH_PORT=127.0.0.1:9207 + +# External MPC ports (host machine) +EXTERNAL_MPC_PUBLIC_DEBUG_PORT=0.0.0.0:8080 +EXTERNAL_MPC_LOCAL_DEBUG_PORT=127.0.0.1:3030 +EXTERNAL_MPC_DECENTRALIZED_STATE_SYNC=0.0.0.0:24567 +EXTERNAL_MPC_MAIN_PORT=0.0.0.0:80 + +# Internal MPC ports (inside CVM) +INTERNAL_MPC_PUBLIC_DEBUG_PORT=8080 +INTERNAL_MPC_LOCAL_DEBUG_PORT=3030 +INTERNAL_MPC_DECENTRALIZED_STATE_SYNC=24567 +INTERNAL_MPC_MAIN_PORT=80 + +# OS image +OS_IMAGE=dstack-dev-0.5.8 + +# Path of the launcher docker_compose_file +DOCKER_COMPOSE_FILE_PATH=launcher_docker_compose.yaml +# Path of the user_config file +USER_CONFIG_FILE_PATH=user-config.conf + +# Resource configuration (defaults shown): +VCPU=8 # do not change since this is measured in the contract +MEMORY=64G # do not change since this is measured in the contract +DISK=128G # can change +``` + +--- + +Based on: [Original Dstack deploy script](https://github.com/Dstack-TEE/dstack/blob/be9d0476a63e937eda4c13659547a25088393394/kms/dstack-app/deploy-to-vmm.sh) diff --git a/tee_launcher/deploy-launcher.sh b/tee_launcher/deploy-launcher.sh new file mode 100755 index 000000000..0be5bf276 --- /dev/null +++ b/tee_launcher/deploy-launcher.sh @@ -0,0 +1,232 @@ +#!/bin/bash + +# Deploys a new launcher_test_app VM to dstack-vmm using a templated Docker Compose file. +# Loads environment variables from a .env file, generates app-compose.json, and runs deployment. +# Based on: https://github.com/Dstack-TEE/dstack/blob/be9d0476a63e937eda4c13659547a25088393394/kms/dstack-app/deploy-to-vmm.sh + +check_ports_in_use() { + PORT_VARS=" + EXTERNAL_DSTACK_AGENT_PORT + EXTERNAL_SSH_PORT + EXTERNAL_MPC_PUBLIC_DEBUG_PORT + EXTERNAL_MPC_LOCAL_DEBUG_PORT + EXTERNAL_MPC_DECENTRALIZED_STATE_SYNC + EXTERNAL_MPC_MAIN_PORT + EXTERNAL_MPC_FUTURE_PORT + " + + if ! command -v ss >/dev/null 2>&1; then + echo "⚠️ WARNING: could not check port conflict. please install ss" + exit 1 + fi + + any_in_use=0 + + # Only IPv4 listeners (-4). Local address is column 4 (e.g. 0.0.0.0:13002 or 51.68.219.11:13002) + addrs="$(ss -H -4 -ltn 2>/dev/null | awk '{print $4}')" + + for var in $PORT_VARS; do + val=$(eval echo \$$var) + [ -z "$val" ] && continue + + ip="${val%%:*}" + port="${val##*:}" + + echo "Checking $var ($ip:$port)..." + + conflict=0 + + if [[ "$ip" == "0.0.0.0" ]]; then + # Binding to all IPv4 addresses conflicts with ANY existing listener on that port. + if echo "$addrs" | grep -Eq ":$port$"; then + conflict=1 + fi + else + # Binding to a specific IPv4 address conflicts if: + # 1) someone already bound 0.0.0.0:port, OR + # 2) someone already bound that exact ip:port. + if echo "$addrs" | grep -Eq "0\.0\.0\.0:$port$|${ip//./\\.}:$port$"; then + conflict=1 + fi + fi + + if [ $conflict -eq 1 ]; then + echo " -> ❌ CONFLICT" + any_in_use=1 + else + echo " -> ✅ free" + fi + done + + if [ $any_in_use -eq 1 ]; then + echo "❌ One or more required IPv4 IP:ports conflict. Aborting." + exit 1 + else + echo "✅ All required IPv4 IP:ports are free." + fi +} + + +# Default .env path +ENV_FILE="default.env" + +# Parse optional arguments +while [[ "$#" -gt 0 ]]; do + case "$1" in + -e|--env-file) + ENV_FILE="$2" + shift 2 + ;; + -b|--base-path) + basePath="$2" + shift 2 + ;; + -p|--python-exec) + pythonExec="$2" + shift 2 + ;; + -y|--yes) + AUTO_YES=1 + shift 1 + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--env-file ] [--base-path ] [--python-exec ]" + exit 1 + ;; + esac +done + +# Check if .env file exists +if [ -f "$ENV_FILE" ]; then + echo "Loading environment variables from $ENV_FILE..." + set -a + source "$ENV_FILE" + set +a +else + echo "Creating template $ENV_FILE..." + cat >"$ENV_FILE" <-]|^--") + + +def _has_control_chars(s: str) -> bool: + # Disallow NUL + CR/LF at minimum; also block other ASCII control chars (< 0x20) except tab. + for ch in s: + oc = ord(ch) + if ch in ("\n", "\r", "\x00"): + return True + if oc < 0x20 and ch != "\t": + return True + return False + + +def is_safe_env_value(value: str) -> bool: + """ + Validates that an env value contains no unsafe control characters (CR/LF/NUL), + does not include LD_PRELOAD, and is within size limits to prevent injection or DoS. + """ + if not isinstance(value, str): + return False + if len(value) > MAX_ENV_VALUE_LEN: + return False + if _has_control_chars(value): + return False + if "LD_PRELOAD" in value: + return False + return True + + +def is_valid_port_mapping(entry: str) -> bool: + match = PORT_MAPPING_RE.match(entry) + if not match: + return False + host_port, container_port = map(int, match.groups()) + return 0 < host_port <= 65535 and 0 < container_port <= 65535 + + +def is_non_empty_and_cleaned(val: str) -> bool: + if not isinstance(val, str): + return False + if not val.strip(): + return False + return val.strip() == val + + +def is_safe_port_mapping(mapping: str) -> bool: + """Ensure that the port mapping does not contain unsafe characters or start with '--' or '-'.""" + return not INVALID_HOST_ENTRY_PATTERN.search(mapping) + + +def remove_existing_container(): + """Stop and remove the MPC container if it exists.""" + try: + containers = check_output( + ["docker", "ps", "-a", "--format", "{{.Names}}"], text=True + ).splitlines() + if MPC_CONTAINER_NAME in containers: + logging.info(f"Removing existing container: {MPC_CONTAINER_NAME}") + run(["docker", "rm", "-f", MPC_CONTAINER_NAME], check=False) + except Exception as e: + logging.warning(f"Failed to check/remove container {MPC_CONTAINER_NAME}: {e}") + + +@dataclass(frozen=True) +class ImageSpec: + tags: list[str] + image_name: str + registry: str + + def __post_init__(self): + if not self.tags or not all(is_non_empty_and_cleaned(tag) for tag in self.tags): + raise ValueError( + "tags must be a non-empty list of non-empty strings without whitespaces." + ) + + if not is_non_empty_and_cleaned(self.image_name): + raise ValueError( + "image_name must be a non-empty string without whitespaces." + ) + + if not is_non_empty_and_cleaned(self.registry): + raise ValueError("registry must be a non-empty string without whitespaces.") + + +@dataclass(frozen=True) +class ResolvedImage: + spec: ImageSpec + digest: str + + def __post_init__(self): + if not is_non_empty_and_cleaned(self.digest): + raise ValueError( + "image digest must be a non-empty string without whitespaces" + ) + # should we require specific lengths? + + def name(self) -> str: + return self.spec.image_name + + def tags(self) -> list[str]: + return self.spec.tags + + def registry(self) -> str: + return self.spec.registry + + +def parse_env_lines(lines: list[str]) -> dict: + env = {} + for line in lines: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + key = key.strip() + value = value.strip() + if not key: + continue + env[key] = value + return env + + +# Parses a .env-style file into a dictionary of key-value pairs. +def parse_env_file(path: str) -> dict: + with open(path, "r") as f: + return parse_env_lines(f.readlines()) + + +def is_unix_socket(path: str) -> bool: + try: + st = os.stat(path) + return stat.S_ISSOCK(st.st_mode) + except FileNotFoundError: + return False + except Exception: + return False + + +def parse_platform() -> Platform: + """ + Platform selection MUST be a measured input in TEE deployments. + Therefore, we only read it from process env (docker-compose 'environment'), + and never from /tapp/user_config. + """ + raw = os.environ.get(ENV_VAR_PLATFORM) + if raw is None: + raise RuntimeError( + f"{ENV_VAR_PLATFORM} must be set to one of {[p.value for p in Platform]}" + ) + + val = raw.strip() + try: + return Platform(val) + except ValueError as e: + allowed = ", ".join(p.value for p in Platform) + raise RuntimeError( + f"Invalid {ENV_VAR_PLATFORM}={raw!r}. Expected one of: {allowed}" + ) from e + + +def load_rpc_timing_config(dstack_config: dict[str, str]) -> RpcTimingConfig: + """ + Loads dockerhub RPC timing configuration from dstack_config, + falling back to defaults if not provided by user. + """ + + timeout_secs = float( + dstack_config.get(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, RPC_REQUEST_TIMEOUT_SECS) + ) + interval_secs = float( + dstack_config.get(ENV_VAR_RPC_REQUEST_INTERVAL_SECS, RPC_REQUEST_INTERVAL_SECS) + ) + max_attempts = int(dstack_config.get(ENV_VAR_RPC_MAX_ATTEMPTS, RPC_MAX_ATTEMPTS)) + + return RpcTimingConfig( + rpc_request_timeout_secs=timeout_secs, + rpc_request_interval_secs=interval_secs, + rpc_max_attempts=max_attempts, + ) + + +def get_image_spec(dstack_config: dict[str, str]) -> ImageSpec: + tags_values: list[str] = dstack_config.get( + DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, DEFAULT_MPC_IMAGE_TAG + ).split(",") + tags = [tag.strip() for tag in tags_values if tag.strip()] + logging.info(f"Using tags {tags} to find matching MPC node docker image.") + + image_name: str = dstack_config.get( + DSTACK_USER_CONFIG_MPC_IMAGE_NAME, DEFAULT_MPC_IMAGE_NAME + ) + logging.info(f"Using image name {image_name}.") + + registry: str = dstack_config.get( + DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, DEFAULT_MPC_REGISTRY + ) + logging.info(f"Using registry {registry}.") + + return ImageSpec(tags=tags, image_name=image_name, registry=registry) + + +def load_and_select_hash(dstack_config: dict) -> str: + """ + Load the approved MPC image hashes and deterministically select a single hash. + + Selection rules: + - If MPC_HASH_OVERRIDE is set → use it (after basic format validation) + - Else → use the newest approved hash (already first in the list) + + This function does NOT validate the hash — only selects it. + """ + + # IMAGE_DIGEST_FILE missing → use DEFAULT_IMAGE_DIGEST + if not os.path.isfile(IMAGE_DIGEST_FILE): + fallback = os.environ[ENV_VAR_DEFAULT_IMAGE_DIGEST].strip() + + if not fallback.startswith(SHA256_PREFIX): + fallback = SHA256_PREFIX + fallback + if not SHA256_REGEX.match(fallback): + raise RuntimeError(f"DEFAULT_IMAGE_DIGEST invalid: {fallback}") + + logging.info( + f"{IMAGE_DIGEST_FILE} missing → fallback to DEFAULT_IMAGE_DIGEST={fallback}" + ) + approved_hashes = [fallback] + + else: + # Load JSON with approved hashes + try: + with open(IMAGE_DIGEST_FILE, "r") as f: + data = json.load(f) + except Exception as e: + raise RuntimeError(f"Failed to parse {IMAGE_DIGEST_FILE}: {e}") + + hashes = data.get(JSON_KEY_APPROVED_HASHES) + + if not isinstance(hashes, list) or not hashes: + raise RuntimeError( + f"Invalid JSON in {IMAGE_DIGEST_FILE}: approved_hashes missing or empty" + ) + + # Hashes from the node are already ordered newest → oldest + approved_hashes = hashes + + # Print allowed hashes for operator UX + logging.info("Approved MPC image hashes (newest → oldest):") + for h in approved_hashes: + logging.info(f" - {h}") + + # --- Optional override --- + override = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) + + if override: + if not SHA256_REGEX.match(override): + raise RuntimeError(f"MPC_HASH_OVERRIDE invalid: {override}") + else: + if override not in approved_hashes: + logging.error( + f"MPC_HASH_OVERRIDE={override} does NOT match any approved hash!" + ) + raise RuntimeError(f"MPC_HASH_OVERRIDE invalid: {override}") + logging.info(f"MPC_HASH_OVERRIDE provided → selecting: {override}") + return override + + # No override → select newest + selected = approved_hashes[0] + logging.info(f"Selected MPC hash (newest allowed): {selected}") + return selected + + +def validate_image_hash( + image_digest: str, + dstack_config: dict, + rpc_request_timeout_secs: float, + rpc_request_interval_secs: float, + rpc_max_attempts: int, +) -> bool: + """ + Returns True if the given image digest is valid (pull + manifest + digest match). + Does NOT extend RTMR3 and does NOT run the container. + """ + try: + logging.info(f"Validating MPC hash: {image_digest}") + + image_spec = get_image_spec(dstack_config) + docker_image = ResolvedImage(spec=image_spec, digest=image_digest) + + manifest_digest = get_manifest_digest( + docker_image, + rpc_request_timeout_secs, + rpc_request_interval_secs, + rpc_max_attempts, + ) + + name_and_digest = f"{image_spec.image_name}@{manifest_digest}" + + # Pull + proc = run(["docker", "pull", name_and_digest], capture_output=True) + if proc.returncode != 0: + logging.error( + f"docker pull failed for {image_digest} using {name_and_digest}" + ) + logging.error( + f"stdout:\n{proc.stdout}", + ) + logging.error( + f"stderr:\n{proc.stderr}", + ) + return False + + # Verify digest + proc = run( + [ + "docker", + "image", + "inspect", + "--format", + "{{index .ID}}", + name_and_digest, + ], + capture_output=True, + ) + if proc.returncode != 0: + logging.error(f"docker inspect failed for {image_digest}") + return False + + pulled_digest = proc.stdout.decode("utf-8").strip() + if pulled_digest != image_digest: + logging.error(f"digest mismatch: {pulled_digest} != {image_digest}") + return False + logging.info(f"MPC hash {image_digest} validated successfully.") + return True + + except Exception as e: + logging.error(f"Validation failed for {image_digest}: {e}") + return False + + +def extend_rtmr3(platform: Platform, valid_hash: str) -> None: + if platform == Platform.NONTEE: + logging.info("PLATFORM=NONTEE → skipping RTMR3 extension step.") + return + + if not is_unix_socket(DSTACK_UNIX_SOCKET): + raise RuntimeError( + f"PLATFORM=TEE requires dstack unix socket at {DSTACK_UNIX_SOCKET}." + ) + + bare = get_bare_digest(valid_hash) + logging.info(f"Extending RTMR3 with validated hash: {bare}") + + # GetQuote first + proc = curl_unix_socket_post( + endpoint="GetQuote", payload='{"report_data": ""}', capture_output=True + ) + if proc.returncode: + raise RuntimeError("GetQuote failed before extending RTMR3") + + payload_json = json.dumps({"event": "mpc-image-digest", "payload": bare}) + + proc = curl_unix_socket_post( + endpoint="EmitEvent", payload=payload_json, capture_output=True + ) + if proc.returncode: + raise RuntimeError("EmitEvent failed while extending RTMR3") + + +def launch_mpc_container(platform: Platform, valid_hash: str, user_env: dict) -> None: + logging.info(f"Launching MPC node with validated hash: {valid_hash}") + + remove_existing_container() + docker_cmd = build_docker_cmd(platform, user_env, valid_hash) + + proc = run(docker_cmd) + if proc.returncode != 0: + raise RuntimeError(f"docker run failed for validated hash={valid_hash}") + + logging.info("MPC launched successfully.") + + +def curl_unix_socket_post( + endpoint: str, payload: Union[str, bytes], capture_output: bool = False +) -> CompletedProcess: + """ + Send a POST request via curl using the DSTACK UNIX socket. + Python's requests package cannot natively talk HTTP over a unix socket (which is the API + exposed by dstack's guest agent). To avoid installing another Python depdendency, namely + requests-unixsocket, we just use curl. + + Args: + endpoint: Path after `http://dstack/`, e.g. 'GetQuote', 'EmitEvent' + payload: JSON string or bytes to send as the request body + capture_output: Whether to capture stdout/stderr (default: False) + + Returns: + subprocess.CompletedProcess result + """ + url = f"http://dstack/{endpoint}" + cmd = [ + "curl", + "--unix-socket", + DSTACK_UNIX_SOCKET, + "-X", + "POST", + url, + "-H", + "Content-Type: application/json", + "-d", + payload, + ] + return run(cmd, capture_output=capture_output) + + +def main(): + logging.info("start") + + platform = parse_platform() + logging.info(f"Launcher platform: {platform.value}") + + if platform is Platform.TEE and not is_unix_socket(DSTACK_UNIX_SOCKET): + raise RuntimeError( + f"PLATFORM=TEE requires dstack unix socket at {DSTACK_UNIX_SOCKET}." + ) + + # DOCKER_CONTENT_TRUST must be enabled + if os.environ.get(OS_ENV_DOCKER_CONTENT_TRUST, "0") != "1": + raise RuntimeError( + "Environment variable DOCKER_CONTENT_TRUST must be set to 1." + ) + + # Load dstack configuration (tags, registry, image name) + # In dstack, /tapp/user_config provides unmeasured data to the CVM. + # We use this interface to make some aspects of the launcher configurable. + # *** Only security-irrelevant parts *** may be made configurable in this way, e.g., the specific image tag(s) we look up. + dstack_config: dict[str, str] = ( + parse_env_file(DSTACK_USER_CONFIG_FILE) + if os.path.isfile(DSTACK_USER_CONFIG_FILE) + else {} + ) + + rpc_cfg = load_rpc_timing_config(dstack_config) + + rpc_request_timeout_secs = rpc_cfg.rpc_request_timeout_secs + rpc_request_interval_secs = rpc_cfg.rpc_request_interval_secs + rpc_max_attempts = rpc_cfg.rpc_max_attempts + + # Choose exactly one allowed hash (override or newest) + selected_hash = load_and_select_hash(dstack_config) + + if not validate_image_hash( + selected_hash, + dstack_config, + rpc_request_timeout_secs, + rpc_request_interval_secs, + rpc_max_attempts, + ): + raise RuntimeError(f"MPC image hash validation failed: {selected_hash}") + + # Continue with validated hash + logging.info(f"MPC image hash validated successfully: {selected_hash}") + + extend_rtmr3(platform, selected_hash) + + launch_mpc_container(platform, selected_hash, dstack_config) + + +def request_until_success( + url: str, + headers: Dict[str, str], + rpc_request_timeout_secs: float, + rpc_request_interval_secs: float, + rpc_max_attempts: int, +) -> Response: + """ + Repeatedly sends a GET request to the specified URL until a successful (200 OK) response is received. + + Args: + url (str): The URL to request. + headers (Dict[str, str]): Optional headers to include in the request. + rpc_request_interval_secs (float): Time in seconds to wait between retries on failure. + rpc_request_timeout_secs (float): Maximum time in seconds to wait for a request to succeed. + + Returns: + Response: The successful HTTP response object with status code 200. + + Notes: + - Retries indefinitely until the request succeeds. + - Prints a warning with the response content on each failure. + """ + for attempt in range(1, rpc_max_attempts + 1): + # Ensure that we respect the backoff time. Performance is not a priority in this case. + time.sleep(rpc_request_interval_secs) + rpc_request_interval_secs = min(max(rpc_request_interval_secs, 1.0) * 1.5, 60.0) + try: + manifest_resp = requests.get( + url, headers=headers, timeout=rpc_request_timeout_secs + ) + except requests.exceptions.Timeout: + print( + f"[Warning] Attempt {attempt}/{rpc_max_attempts}: Failed to fetch {url} for headers {headers}. " + f"Status: Timeout" + ) + continue + if manifest_resp.status_code != 200: + print( + f"[Warning] Attempt {attempt}/{rpc_max_attempts}: Failed to fetch {url} for headers {headers}. " + f"Status: {manifest_resp.text} {manifest_resp.headers}" + ) + continue + else: + return manifest_resp + + raise RuntimeError( + f"Failed to get successful response from {url} after {rpc_max_attempts} attempts." + ) + + +def get_manifest_digest( + docker_image: ResolvedImage, + rpc_request_timeout_secs: float, + rpc_request_interval_secs: float, + rpc_max_attempts: int, +) -> str: + """ + Given an `image_digest` returns a manifest digest. + `docker pull` requires a manifest digest. This function translates an image digest into a manifest digest by talking to the Docker registry. + API doc for image registry https://distribution.github.io/distribution/spec/api/ + """ + if not docker_image.tags(): + raise Exception(f"No tags found for image {docker_image.spec.image_name}") + + # We need an authorization token to fetch manifests. + # TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint. + token_resp = requests.get( + f"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{docker_image.name()}:pull" + ) + token_resp.raise_for_status() + token = token_resp.json().get("token", []) + + tags = deque(docker_image.tags()) + + while tags: + tag = tags.popleft() + + manifest_url = f"https://{docker_image.registry()}/v2/{docker_image.name()}/manifests/{tag}" + headers = { + "Accept": "application/vnd.docker.distribution.manifest.v2+json", + "Authorization": f"Bearer {token}", + } + try: + manifest_resp = request_until_success( + url=manifest_url, + headers=headers, + rpc_request_timeout_secs=rpc_request_timeout_secs, + rpc_request_interval_secs=rpc_request_interval_secs, + rpc_max_attempts=rpc_max_attempts, + ) + manifest = manifest_resp.json() + match manifest["mediaType"]: + case "application/vnd.oci.image.index.v1+json": + # Multi-platform manifest; we scan for amd64/linux images and add them to `tags` + for image_manifest in manifest.get("manifests", []): + platform = image_manifest.get("platform", []) + if ( + platform.get("architecture") == "amd64" + and platform.get("os") == "linux" + ): + tags.append(image_manifest["digest"]) + case ( + "application/vnd.docker.distribution.manifest.v2+json" + | "application/vnd.oci.image.manifest.v1+json" + ): + config_digest = manifest["config"]["digest"] + if config_digest == docker_image.digest: + return manifest_resp.headers["Docker-Content-Digest"] + except RuntimeError as e: + print( + f"[Warning] {e}: Exceeded number of maximum RPC requests for any given attempt. Will continue in the hopes of finding the matching image hash among remaining tags" + ) + # Q: Do we expect all requests to succeed? + raise Exception("Image hash not found among tags.") + + +def get_bare_digest(full_digest: str) -> str: + """ + Extracts and returns the bare digest (without the sha256: prefix). + + Example: + 'sha256:abcd...' -> 'abcd...' + """ + if not full_digest.startswith("sha256:"): + raise ValueError(f"Invalid digest (missing sha256: prefix): {full_digest}") + + return full_digest.split(":", 1)[1] + + +def is_allowed_container_env_key(key: str) -> bool: + if key in DENIED_CONTAINER_ENV_KEYS: + return False + # Allow MPC_* keys with strict regex + if MPC_ENV_KEY_RE.match(key): + return True + # Keep allowlist + if key in ALLOWED_MPC_ENV_VARS: + return True + return False + + +def build_docker_cmd( + platform: Platform, user_env: dict[str, str], image_digest: str +) -> list[str]: + bare_digest = get_bare_digest(image_digest) + + docker_cmd = ["docker", "run"] + + # Required environment variables + docker_cmd += ["--env", f"MPC_IMAGE_HASH={bare_digest}"] + docker_cmd += ["--env", f"MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"] + + if platform is Platform.TEE: + docker_cmd += ["--env", f"DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"] + docker_cmd += ["-v", f"{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"] + + # Track env passthrough size/caps + passed_env_count = 0 + total_env_bytes = 0 + + # Deterministic iteration for stable logs/behavior + for key in sorted(user_env.keys()): + value = user_env[key] + + if key in ALLOWED_LAUNCHER_ENV_VARS: + # launcher-only env vars: never pass to container + continue + + if key == "PORTS": + for port_pair in value.split(","): + clean_host = port_pair.strip() + if is_safe_port_mapping(clean_host) and is_valid_port_mapping( + clean_host + ): + docker_cmd += ["-p", clean_host] + else: + logging.warning( + f"Ignoring invalid or unsafe PORTS entry: {clean_host}" + ) + continue + + if not is_allowed_container_env_key(key): + logging.warning(f"Ignoring unknown or unapproved env var: {key}") + continue + + if not is_safe_env_value(value): + logging.warning(f"Ignoring env var with unsafe value: {key}") + continue + + # Enforce caps + passed_env_count += 1 + if passed_env_count > MAX_PASSTHROUGH_ENV_VARS: + raise RuntimeError( + f"Too many env vars to pass through (>{MAX_PASSTHROUGH_ENV_VARS})." + ) + + # Approximate byte accounting (key=value plus overhead) + total_env_bytes += len(key) + 1 + len(value) + if total_env_bytes > MAX_TOTAL_ENV_BYTES: + raise RuntimeError( + f"Total env payload too large (>{MAX_TOTAL_ENV_BYTES} bytes)." + ) + + docker_cmd += ["--env", f"{key}={value}"] + + # Container run configuration + docker_cmd += [ + "--security-opt", + "no-new-privileges:true", + "-v", + "/tapp:/tapp:ro", + "-v", + "shared-volume:/mnt/shared", + "-v", + "mpc-data:/data", + "--name", + MPC_CONTAINER_NAME, + "--detach", + image_digest, # IMPORTANT: Docker must get the FULL digest + ] + + logging.info("docker cmd %s", " ".join(docker_cmd)) + + # Final LD_PRELOAD safeguard + docker_cmd_str = " ".join(docker_cmd) + if "LD_PRELOAD" in docker_cmd_str: + raise RuntimeError("Unsafe docker command: LD_PRELOAD detected.") + + return docker_cmd + + +if __name__ == "__main__": + try: + logging.basicConfig( + level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" + ) + + main() + sys.exit(0) + except Exception as e: + print("Error:", str(e), file=sys.stderr) + traceback.print_exc(file=sys.stderr) + sys.exit(1) diff --git a/tee_launcher/launcher_docker_compose.yaml b/tee_launcher/launcher_docker_compose.yaml new file mode 100644 index 000000000..b415cd02e --- /dev/null +++ b/tee_launcher/launcher_docker_compose.yaml @@ -0,0 +1,30 @@ +version: '3.8' + +services: + launcher: + image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054 + + container_name: launcher + + environment: + - PLATFORM=TEE + - DOCKER_CONTENT_TRUST=1 + - DEFAULT_IMAGE_DIGEST=sha256:e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0 + + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /var/run/dstack.sock:/var/run/dstack.sock + - /tapp:/tapp:ro + - shared-volume:/mnt/shared:ro + + security_opt: + - no-new-privileges:true + + read_only: true + + tmpfs: + - /tmp + +volumes: + shared-volume: + name: shared-volume diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml new file mode 100644 index 000000000..48b0bc4fc --- /dev/null +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -0,0 +1,28 @@ +services: + launcher: + image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054 + container_name: "${LAUNCHER_IMAGE_NAME}" + + environment: + - PLATFORM=NONTEE + - DOCKER_CONTENT_TRUST=1 + - DEFAULT_IMAGE_DIGEST=sha256:9143bc98aaae3408c14cf4490d7b0e96a5a32d989ec865a0cf8dde391831a7a9 # 3.6.0 release + + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./user-config.conf:/tapp/user_config:ro + - shared-volume:/mnt/shared + - mpc-data:/data + + security_opt: + - no-new-privileges:true + + read_only: true + tmpfs: + - /tmp + +volumes: + shared-volume: + name: shared-volume + mpc-data: + name: mpc-data diff --git a/tee_launcher/requirements.txt b/tee_launcher/requirements.txt new file mode 100644 index 000000000..61a0f40c5 --- /dev/null +++ b/tee_launcher/requirements.txt @@ -0,0 +1,2 @@ +pytest==8.3.4 +requests==2.32.4 diff --git a/tee_launcher/test_launcher.py b/tee_launcher/test_launcher.py new file mode 100644 index 000000000..18d2960ad --- /dev/null +++ b/tee_launcher/test_launcher.py @@ -0,0 +1,32 @@ +import unittest + +from launcher import ImageSpec, ResolvedImage, get_manifest_digest + + +class TestLauncher(unittest.TestCase): + def test_get_manifest_digest(self): + # Use a recent (at the time of writing) tag/image digest/manifest digest combination for testing + registry_url = "registry.hub.docker.com" + image_name = "nearone/mpc-node-gcp" + image_hash = ( + "sha256:7e5a6bcb6707d134fc479cc293830c05ce45891f0977d467362cbb7f55cde46b" + ) + expected_manifest_digest = ( + "sha256:005943bccdd401e71c5408d65cf301eeb8bfc3926fe346023912412aafda2490" + ) + tags = ["8805536ab98d924d980a58ecc0518a8c90204bec"] + image = ResolvedImage( + ImageSpec(tags=tags, image_name=image_name, registry=registry_url), + digest=image_hash, + ) + result = get_manifest_digest( + image, + rpc_request_timeout_secs=10, + rpc_request_interval_secs=0.5, + rpc_max_attempts=10, + ) + self.assertEqual(result, expected_manifest_digest) + + +if __name__ == "__main__": + unittest.main() diff --git a/tee_launcher/test_launcher_config.py b/tee_launcher/test_launcher_config.py new file mode 100644 index 000000000..fd33007ec --- /dev/null +++ b/tee_launcher/test_launcher_config.py @@ -0,0 +1,844 @@ +# test_launcher_config.py + +import inspect +import json +import os +import tempfile +import tee_launcher.launcher as launcher + + +import pytest +from unittest.mock import mock_open + +from tee_launcher.launcher import ( + load_and_select_hash, + validate_image_hash, + parse_env_lines, + build_docker_cmd, + is_valid_port_mapping, + Platform, + is_safe_env_value, + _has_control_chars, + is_allowed_container_env_key, + MAX_ENV_VALUE_LEN, + MAX_PASSTHROUGH_ENV_VARS, +) +from tee_launcher.launcher import ( + JSON_KEY_APPROVED_HASHES, + ENV_VAR_MPC_HASH_OVERRIDE, + ENV_VAR_DEFAULT_IMAGE_DIGEST, +) + + +# Test constants for user_config content +TEST_MPC_ACCOUNT_ID = "mpc-user-123" + +TEST_PORTS_WITH_INJECTION = "11780:11780,--env BAD=1" + + +def make_digest_json(hashes): + return json.dumps({JSON_KEY_APPROVED_HASHES: hashes}) + + +def parse_env_string(text: str) -> dict: + return parse_env_lines(text.splitlines()) + + +def test_parse_env_lines_basic(): + lines = [ + "# a comment", + "KEY1=value1", + " KEY2 = value2 ", + "", + "INVALIDLINE", + "EMPTY_KEY=", + ] + env = parse_env_lines(lines) + assert env == {"KEY1": "value1", "KEY2": "value2", "EMPTY_KEY": ""} + + +# test user config loading and parsing +def write_temp_config(content: str) -> str: + tmp = tempfile.NamedTemporaryFile(mode="w", delete=False) + tmp.write(content) + tmp.close() + return tmp.name + + +def test_valid_user_config_parsing(): + config_str = """ + MPC_ACCOUNT_ID=account123 + MPC_LOCAL_ADDRESS=127.0.0.1 + # A comment + MPC_ENV=testnet + """ + env = parse_env_string(config_str) + + assert env["MPC_ACCOUNT_ID"] == "account123" + assert env["MPC_LOCAL_ADDRESS"] == "127.0.0.1" + assert env["MPC_ENV"] == "testnet" + + +def test_config_ignores_blank_lines_and_comments(): + config_str = """ + + # This is a comment + MPC_SECRET_STORE_KEY=topsecret + + """ + env = parse_env_string(config_str) + + assert env["MPC_SECRET_STORE_KEY"] == "topsecret" + assert len(env) == 1 + + +def test_config_skips_malformed_lines(): + config_str = """ + GOOD_KEY=value + bad_line_without_equal + ANOTHER_GOOD=ok + = + """ + env = parse_env_string(config_str) + + assert "GOOD_KEY" in env + assert "ANOTHER_GOOD" in env + assert "bad_line_without_equal" not in env + assert "" not in env # ensure empty keys are skipped + + +def test_config_overrides_duplicate_keys(): + config_str = """ + MPC_ACCOUNT_ID=first + MPC_ACCOUNT_ID=second + """ + env = parse_env_string(config_str) + + assert env["MPC_ACCOUNT_ID"] == "second" # last one wins + + +# test valid and invalid host entries and port mappings + + +def test_valid_port_mapping(): + assert is_valid_port_mapping("11780:11780") + assert not is_valid_port_mapping("65536:11780") + assert not is_valid_port_mapping("--volume /:/mnt") + + +def test_build_docker_cmd_sanitizes_ports_and_hosts(): + env = { + "PORTS": TEST_PORTS_WITH_INJECTION, + "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, + } + cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") + + func_name = inspect.currentframe().f_code.co_name + print(f"[{func_name}] CMD:", " ".join(cmd)) + + assert "--env" in cmd + assert f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}" in cmd + assert "-p" in cmd + assert "11780:11780" in cmd + + # Make sure injection strings were filtered + assert not any("BAD=1" in arg for arg in cmd) + assert not any("/:/mnt" in arg for arg in cmd) + + +def test_ports_does_not_allow_volume_injection(): + env = { + "PORTS": "2200:2200,--volume /:/mnt", + "MPC_ACCOUNT_ID": "safe", + } + cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") + + assert "2200:2200" in cmd + assert not any("/:/mnt" in arg for arg in cmd) + + +def test_invalid_env_key_is_ignored(): + env = { + "BAD_KEY": "should_not_be_used", + "MPC_ACCOUNT_ID": "safe", + } + cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") + + assert "should_not_be_used" not in " ".join(cmd) + assert "MPC_ACCOUNT_ID=safe" in cmd + + +def test_mpc_backup_encryption_key_is_allowed(): + env = { + "MPC_BACKUP_ENCRYPTION_KEY_HEX": "0000000000000000000000000000000000000000000000000000000000000000", + } + cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") + + assert ( + "MPC_BACKUP_ENCRYPTION_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000000" + in " ".join(cmd) + ) + + +def test_env_value_with_shell_injection_is_handled_safely(): + env = { + "MPC_ACCOUNT_ID": "safe; rm -rf /", + } + cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") + + assert "--env" in cmd + assert "MPC_ACCOUNT_ID=safe; rm -rf /" in cmd + + +def test_parse_and_build_docker_cmd_full_flow(): + config_str = """ + # Valid entries + MPC_ACCOUNT_ID=test-user + PORTS=11780:11780, --env BAD=oops + IMAGE_HASH=sha256:abc123 + """ + + env = parse_env_string(config_str) + image_hash = env.get("IMAGE_HASH", "sha256:default") + + cmd = build_docker_cmd(launcher.Platform.TEE, env, image_hash) + + print(f"[{inspect.currentframe().f_code.co_name}] CMD: {' '.join(cmd)}") + + assert "--env" in cmd + assert "MPC_ACCOUNT_ID=test-user" in cmd + assert "-p" in cmd + assert "11780:11780" in cmd + + # Confirm malicious injection is blocked + assert not any("--env BAD=oops" in s or "oops" in s for s in cmd) + + +# Test that ensures LD_PRELOAD cannot be injected into the docker command +def test_ld_preload_injection_blocked1(): + # Set up the environment variable with a dangerous LD_PRELOAD value + malicious_env = { + "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, + "--env LD_PRELOAD": "/path/to/my/malloc.so", # The dangerous value + } + + # Call build_docker_cmd to generate the docker command + docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") + + # Check that LD_PRELOAD is not included in the command + assert "--env" in docker_cmd # Ensure there is an env var + assert ( + "LD_PRELOAD" not in docker_cmd + ) # Make sure LD_PRELOAD is not in the generated command + + # Alternatively, if you're using a regex to ensure safe environment variables + assert not any( + "-e " in arg for arg in docker_cmd + ) # Ensure no CLI injection for LD_PRELOAD + + +# Additional tests can go here for host/port validation + + +# Test that ensures LD_PRELOAD cannot be injected through ports +def test_ld_preload_in_ports1(): + # Set up environment with malicious PORTS containing LD_PRELOAD + malicious_env = { + "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, + "PORTS": "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so", + } + + # Call build_docker_cmd to generate the docker command + docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") + + # Check that LD_PRELOAD is not part of the port mappings in the docker command + assert "-p" in docker_cmd # Ensure port mappings are included + assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command + + # Check that there are no malicious injections + assert not any( + "--env LD_PRELOAD" in arg for arg in docker_cmd + ) # No environment injection + + +# Additional tests could go here to check other edge cases + + +# Test that ensures LD_PRELOAD cannot be injected through mpc account id +def test_ld_preload_in_mpc_account_id(): + # Set up environment containing LD_PRELOAD + malicious_env = { + "MPC_ACCOUNT_ID": f"{TEST_MPC_ACCOUNT_ID}, --env LD_PRELOAD=/path/to/my/malloc.so", + } + + # Call build_docker_cmd to generate the docker command + docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") + + # Check that LD_PRELOAD is not part of the docker command + assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command + + # Check that there are no malicious injections + print(docker_cmd) + assert not any( + "--env LD_PRELOAD" in arg for arg in docker_cmd + ) # No environment injection + + +# Test that ensures LD_PRELOAD cannot be injected into the docker command +def test_ld_preload_injection_blocked2(): + # Set up the environment variable with a dangerous LD_PRELOAD value + malicious_env = { + "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, + "-e LD_PRELOAD": "/path/to/my/malloc.so", # The dangerous value + } + + # Call build_docker_cmd to generate the docker command + docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") + + assert ( + "-e LD_PRELOAD" not in docker_cmd + ) # Make sure LD_PRELOAD is not in the generated command + + +# Additional tests can go here for host/port validation + + +# Test that ensures LD_PRELOAD cannot be injected through ports +def test_ld_preload_in_ports2(): + # Set up environment with malicious PORTS containing LD_PRELOAD + malicious_env = { + "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, + "PORTS": "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so", + } + + # Call build_docker_cmd to generate the docker command + docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") + + # Check that LD_PRELOAD is not part of the port mappings in the docker command + assert "-p" in docker_cmd # Ensure port mappings are included + assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command + + +def test_json_key_matches_node(): + """ + Ensure the JSON key used by the launcher to read approved image hashes + stays aligned with the MPC node implementation. + mpc/crates/node/src/tee/allowed_image_hashes_watcher.rs -> JSON_KEY_APPROVED_HASHES + + If this test fails, it means the launcher and MPC node are using different + JSON field names, which would break MPC hash propagation. + """ + assert launcher.JSON_KEY_APPROVED_HASHES == "approved_hashes" + + +def test_override_present(monkeypatch): + override_value = "sha256:" + "a" * 64 + approved = ["sha256:" + "b" * 64, override_value, "sha256:" + "c" * 64] + + fake_json = make_digest_json(approved) + + monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) + monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) + + dstack_config = {ENV_VAR_MPC_HASH_OVERRIDE: override_value} + + selected = load_and_select_hash(dstack_config) + assert selected == override_value + + +def test_override_not_present(monkeypatch): + approved = ["sha256:aaa", "sha256:bbb"] + fake_json = make_digest_json(approved) + + monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) + monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) + + dstack_config = { + ENV_VAR_MPC_HASH_OVERRIDE: "sha256:xyz" # NOT in list + } + + with pytest.raises(RuntimeError): + load_and_select_hash(dstack_config) + + +def test_no_override_picks_newest(monkeypatch): + approved = ["sha256:newest", "sha256:older", "sha256:oldest"] + fake_json = make_digest_json(approved) + + monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) + monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) + + selected = load_and_select_hash({}) + assert selected == "sha256:newest" + + +def test_missing_file_fallback(monkeypatch): + # Pretend file does NOT exist + monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: False) + + # Valid fallback digest (64 hex chars) + monkeypatch.setenv(ENV_VAR_DEFAULT_IMAGE_DIGEST, "a" * 64) + + selected = load_and_select_hash({}) + assert selected == "sha256:" + "a" * 64 + + +TEST_DIGEST = "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372" +# Important: ensure the config matches your test image +DSTACK_CONFIG = { + "MPC_IMAGE_TAGS": "83b52da4e2270c688cdd30da04f6b9d3565f25bb", + "MPC_IMAGE_NAME": "nearone/testing", + "MPC_REGISTRY": "registry.hub.docker.com", +} + +# Launcher defaults +RPC_REQUEST_TIMEOUT_SECS = 10.0 +RPC_REQUEST_INTERVAL_SECS = 1.0 +RPC_MAX_ATTEMPTS = 20 + + +# ------------------------------------------------------------------------------------ +# NOTE: Integration Test (External Dependency) +# +# This test validates that `validate_image_hash()` correctly: +# - contacts the real Docker registry, +# - resolves the manifest digest, +# - pulls the remote image, +# - and verifies that its sha256 digest matches the expected immutable value. +# +# The test image is a **pre-built, minimal Docker image containing only a tiny +# binary**, created intentionally for performance and fast pulls. +# This image is uploaded to Docker Hub together. +# +# IMPORTANT: +# • The digest in this test corresponds EXACTLY to that pre-built image. +# • Dockerfile used to build the image can be found at mpc/tee_launcher/launcher-test-image/Dockerfile +# • If the test image is rebuilt, the digest MUST be updated here. +# • If the registry is unavailable or slow, this test may fail. +# • CI will run this only if explicitly enabled. +# +# Please read that file before modifying the digest, registry, or test behavior. +# ------------------------------------------------------------------------------------ +def test_validate_image_hash(): + result = validate_image_hash( + TEST_DIGEST, + DSTACK_CONFIG, + RPC_REQUEST_TIMEOUT_SECS, + RPC_REQUEST_INTERVAL_SECS, + RPC_MAX_ATTEMPTS, + ) + assert result is True, "validate_image_hash() failed for test image" + + +# test launcher support for non TEE images. + + +class DummyProc: + def __init__(self, returncode=0, stdout=b"", stderr=b""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +@pytest.fixture +def base_env(monkeypatch): + # Required by launcher + monkeypatch.setenv(launcher.OS_ENV_DOCKER_CONTENT_TRUST, "1") + monkeypatch.setenv(launcher.ENV_VAR_DEFAULT_IMAGE_DIGEST, "sha256:" + "a" * 64) + + +def test_parse_platform_missing(monkeypatch, base_env): + monkeypatch.delenv(launcher.ENV_VAR_PLATFORM, raising=False) + with pytest.raises(RuntimeError): + launcher.parse_platform() + + +@pytest.mark.parametrize("val", ["", "foo", "TEE as", "NON_TEE", "1", "tee", "nontee"]) +def test_parse_platform_invalid(monkeypatch, base_env, val): + monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, val) + with pytest.raises(RuntimeError): + launcher.parse_platform() + + +@pytest.mark.parametrize( + "val,expected", + [ + ("TEE", launcher.Platform.TEE), + ("NONTEE", launcher.Platform.NONTEE), + ], +) +def test_parse_platform_valid(monkeypatch, base_env, val, expected): + monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, val) + assert launcher.parse_platform() is expected + + +def test_extend_rtmr3_nontee_skips_dstack(monkeypatch, base_env): + called = {"count": 0} + + def fake_curl(*args, **kwargs): + called["count"] += 1 + return DummyProc(returncode=0) + + monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) + + launcher.extend_rtmr3(launcher.Platform.NONTEE, "sha256:" + "b" * 64) + assert called["count"] == 0 + + +def test_extend_rtmr3_tee_requires_socket(monkeypatch, base_env): + monkeypatch.setattr(launcher, "is_unix_socket", lambda p: False) + with pytest.raises(RuntimeError): + launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) + + +def test_extend_rtmr3_tee_getquote_fail(monkeypatch, base_env): + monkeypatch.setattr(launcher, "is_unix_socket", lambda p: True) + + def fake_curl(endpoint, payload, capture_output=False): + # Fail only GetQuote + if endpoint == "GetQuote": + return DummyProc(returncode=7) + return DummyProc(returncode=0) + + monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) + with pytest.raises(RuntimeError, match="GetQuote failed"): + launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) + + +def test_extend_rtmr3_tee_emitevent_fail(monkeypatch, base_env): + monkeypatch.setattr(launcher, "is_unix_socket", lambda p: True) + + def fake_curl(endpoint, payload, capture_output=False): + if endpoint == "GetQuote": + return DummyProc(returncode=0) + if endpoint == "EmitEvent": + return DummyProc(returncode=55) + return DummyProc(returncode=0) + + monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) + with pytest.raises(RuntimeError, match="EmitEvent failed"): + launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) + + +def test_build_docker_cmd_nontee_no_dstack_mount(base_env): + env = { + "MPC_ACCOUNT_ID": "x", + # launcher-only env should be ignored + launcher.ENV_VAR_RPC_MAX_ATTEMPTS: "5", + } + cmd = launcher.build_docker_cmd(launcher.Platform.NONTEE, env, "sha256:" + "c" * 64) + s = " ".join(cmd) + + assert "DSTACK_ENDPOINT=" not in s + assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" not in s + + +def test_build_docker_cmd_tee_has_dstack_mount(base_env): + env = {"MPC_ACCOUNT_ID": "x"} + cmd = launcher.build_docker_cmd(launcher.Platform.TEE, env, "sha256:" + "c" * 64) + s = " ".join(cmd) + + assert f"DSTACK_ENDPOINT={launcher.DSTACK_UNIX_SOCKET}" in s + assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" in s + + +def test_main_tee_fails_closed_before_launch(monkeypatch, base_env): + monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, launcher.Platform.TEE.value) + + monkeypatch.setattr(launcher, "is_unix_socket", lambda p: False) + + # prevent any real docker/network + monkeypatch.setattr( + launcher, "load_and_select_hash", lambda cfg: "sha256:" + "d" * 64 + ) + monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) + monkeypatch.setattr( + launcher, + "launch_mpc_container", + lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not launch")), + ) + + with pytest.raises(RuntimeError, match="requires dstack unix socket"): + launcher.main() + + +def test_main_nontee_skips_extend_and_launches(monkeypatch, base_env): + monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, "NONTEE") + monkeypatch.setattr( + launcher, "is_unix_socket", lambda p: False + ) # should not matter + + monkeypatch.setattr( + launcher, "load_and_select_hash", lambda cfg: "sha256:" + "d" * 64 + ) + monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) + + called = {"extend": 0, "launch": 0} + monkeypatch.setattr( + launcher, + "extend_rtmr3", + lambda platform, h: called.__setitem__("extend", called["extend"] + 1), + ) + monkeypatch.setattr( + launcher, + "launch_mpc_container", + lambda platform, h, cfg: called.__setitem__("launch", called["launch"] + 1), + ) + + launcher.main() + assert called["extend"] == 1 + assert called["launch"] == 1 + + +def assert_subsequence(seq, subseq): + it = iter(seq) + for x in subseq: + for y in it: + if y == x: + break + else: + raise AssertionError(f"Missing subsequence item: {x}\nseq={seq}") + + +def test_main_nontee_builds_expected_mpc_docker_cmd(monkeypatch, tmp_path): + """ + Verify that launcher.main() builds the correct MPC docker command in NONTEE mode. + + Steps: + 1. Configure the launcher to run with PLATFORM=NONTEE. + 2. Set required environment variables (DOCKER_CONTENT_TRUST, DEFAULT_IMAGE_DIGEST). + 3. Create a temporary user_config file with MPC env vars, ports, and extra hosts. + 4. Simulate a missing IMAGE_DIGEST_FILE so the launcher falls back to DEFAULT_IMAGE_DIGEST. + 5. Stub image validation and docker interactions to avoid real network or docker usage. + 6. Invoke launcher.main(). + 7. Capture the docker run command used to start the MPC container. + 8. Assert that the command: + - Includes expected MPC configuration (env vars, ports, hosts, volumes). + - Does NOT include dstack socket mounts or DSTACK_ENDPOINT. + - Filters out injection attempts in ports and hosts. + - Uses the expected full image digest. + """ + # --- Arrange: environment (NONTEE) --- + monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, launcher.Platform.NONTEE.value) + monkeypatch.setenv(launcher.OS_ENV_DOCKER_CONTENT_TRUST, "1") + + default_digest = "sha256:" + "a" * 64 + monkeypatch.setenv(launcher.ENV_VAR_DEFAULT_IMAGE_DIGEST, default_digest) + + # Provide a temp user config file so main() passes env into build_docker_cmd() + user_config = tmp_path / "user_config" + user_config.write_text( + "\n".join( + [ + f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}", + f"PORTS={TEST_PORTS_WITH_INJECTION}", # injection should be ignored + ] + ) + + "\n" + ) + + # Point launcher at our temp config + monkeypatch.setattr(launcher, "DSTACK_USER_CONFIG_FILE", str(user_config)) + + # Make IMAGE_DIGEST_FILE "missing" so DEFAULT_IMAGE_DIGEST is used + def fake_isfile(path: str) -> bool: + if path == launcher.IMAGE_DIGEST_FILE: + return False + if path == str(user_config): + return True + return os.path.isfile(path) + + monkeypatch.setattr(launcher.os.path, "isfile", fake_isfile) + + # Avoid network/docker verification in validate_image_hash + monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) + + # Avoid remove_existing_container touching real docker + monkeypatch.setattr(launcher, "check_output", lambda *a, **k: "") + + # Capture the docker run command used to launch MPC + captured = {"docker_run_cmd": None} + + def fake_run(cmd, *args, **kwargs): + # cmd is a list[str] + if ( + isinstance(cmd, list) + and len(cmd) >= 2 + and cmd[0] == "docker" + and cmd[1] == "run" + ): + captured["docker_run_cmd"] = cmd + return DummyProc(returncode=0) + return DummyProc(returncode=0) + + monkeypatch.setattr(launcher, "run", fake_run) + + # --- Act --- + launcher.main() + + # --- Assert --- + cmd = captured["docker_run_cmd"] + assert cmd is not None, "Expected launcher to invoke 'docker run' for MPC container" + + cmd_str = " ".join(cmd) + + # NONTEE invariants + assert "DSTACK_ENDPOINT=" not in cmd_str + assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" not in cmd_str + + # Expected env propagation + sanitization + assert f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}" in cmd_str + assert "-p" in cmd and "11780:11780" in cmd_str + + # Injection strings filtered out + assert "BAD=1" not in cmd_str + assert "/:/mnt" not in cmd_str + + # Required mounts / flags from build_docker_cmd + assert "--security-opt" in cmd_str + assert "no-new-privileges:true" in cmd_str + assert "/tapp:/tapp:ro" in cmd_str + assert "shared-volume:/mnt/shared" in cmd_str + assert "mpc-data:/data" in cmd_str + assert f"--name {launcher.MPC_CONTAINER_NAME}" in cmd_str + + # Image digest should be the final argument and should be the FULL digest + assert cmd[-1] == default_digest + + expected_core = [ + "docker", + "run", + "--security-opt", + "no-new-privileges:true", + "-v", + "/tapp:/tapp:ro", + "-v", + "shared-volume:/mnt/shared", + "-v", + "mpc-data:/data", + "--name", + launcher.MPC_CONTAINER_NAME, + "--detach", + ] + assert_subsequence(cmd, expected_core) + + +def _base_env(): + # Minimal env for build_docker_cmd (launcher will add required MPC_IMAGE_HASH etc itself) + return { + "MPC_ACCOUNT_ID": "mpc-user-123", + "MPC_CONTRACT_ID": "contract.near", + "MPC_ENV": "testnet", + "MPC_HOME_DIR": "/data", + "NEAR_BOOT_NODES": "boot1,boot2", + "RUST_LOG": "info", + } + + +def test_has_control_chars_rejects_newline_and_cr(): + assert _has_control_chars("a\nb") is True + assert _has_control_chars("a\rb") is True + + +def test_has_control_chars_rejects_other_control_chars_but_allows_tab(): + # tab is allowed by the Python helper in the patched launcher + assert _has_control_chars("a\tb") is False + # ASCII control char 0x1F should be rejected + assert _has_control_chars("a" + chr(0x1F) + "b") is True + + +def test_is_safe_env_value_rejects_control_chars(): + assert is_safe_env_value("ok\nno") is False + assert is_safe_env_value("ok\rno") is False + assert is_safe_env_value("ok" + chr(0x1F) + "no") is False + + +def test_is_safe_env_value_rejects_ld_preload_substring(): + assert is_safe_env_value("LD_PRELOAD=/tmp/x.so") is False + assert is_safe_env_value("foo LD_PRELOAD bar") is False + + +def test_is_safe_env_value_rejects_too_long_value(): + assert is_safe_env_value("a" * (MAX_ENV_VALUE_LEN + 1)) is False + assert is_safe_env_value("a" * MAX_ENV_VALUE_LEN) is True + + +def testis_allowed_container_env_key_allows_mpc_prefix_uppercase(): + assert is_allowed_container_env_key("MPC_FOO") is True + assert is_allowed_container_env_key("MPC_FOO_123") is True + assert is_allowed_container_env_key("MPC_A_B_C") is True + + +def testis_allowed_container_env_key_rejects_lowercase_or_invalid_chars(): + assert is_allowed_container_env_key("MPC_foo") is False + assert is_allowed_container_env_key("MPC-FOO") is False + assert is_allowed_container_env_key("MPC.FOO") is False + assert is_allowed_container_env_key("MPC_") is False + + +def testis_allowed_container_env_key_allows_compat_non_mpc_keys(): + assert is_allowed_container_env_key("RUST_LOG") is True + assert is_allowed_container_env_key("RUST_BACKTRACE") is True + assert is_allowed_container_env_key("NEAR_BOOT_NODES") is True + + +def testis_allowed_container_env_key_denies_sensitive_keys(): + assert is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY") is False + assert is_allowed_container_env_key("MPC_ACCOUNT_SK") is False + + +def test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars(): + env = _base_env() + env["MPC_NEW_FEATURE_FLAG"] = "1" + env["MPC_SOME_CONFIG"] = "value" + + cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) + + cmd_str = " ".join(cmd) + assert "--env MPC_NEW_FEATURE_FLAG=1" in cmd_str + assert "--env MPC_SOME_CONFIG=value" in cmd_str + + +def test_build_docker_cmd_blocks_sensitive_mpc_private_keys(): + env = _base_env() + env["MPC_P2P_PRIVATE_KEY"] = "supersecret" + env["MPC_ACCOUNT_SK"] = "supersecret2" + + cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) + cmd_str = " ".join(cmd) + + assert "MPC_P2P_PRIVATE_KEY" not in cmd_str + assert "MPC_ACCOUNT_SK" not in cmd_str + + +def test_build_docker_cmd_rejects_env_value_with_newline(): + env = _base_env() + env["MPC_NEW_FEATURE_FLAG"] = "ok\nbad" + + cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) + cmd_str = " ".join(cmd) + + # It should be ignored (not passed) + assert "MPC_NEW_FEATURE_FLAG" not in cmd_str + + +def test_build_docker_cmd_enforces_max_env_count_cap(): + env = _base_env() + # add many MPC_* keys to exceed cap + for i in range(MAX_PASSTHROUGH_ENV_VARS + 1): + env[f"MPC_X_{i}"] = "1" + + with pytest.raises(RuntimeError, match="Too many env vars"): + build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) + + +def test_build_docker_cmd_enforces_total_env_bytes_cap(): + env = _base_env() + + # Each env contributes ~ len(key)+1+MAX_ENV_VALUE_LEN bytes. + # With MAX_ENV_VALUE_LEN=1024 and MAX_TOTAL_ENV_BYTES=32768, ~35-40 vars will exceed the cap. + for i in range(40): + env[f"MPC_BIG_{i}"] = "a" * MAX_ENV_VALUE_LEN + + with pytest.raises(RuntimeError, match="Total env payload too large"): + build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) diff --git a/tee_launcher/user-config.conf b/tee_launcher/user-config.conf new file mode 100644 index 000000000..0c8980d62 --- /dev/null +++ b/tee_launcher/user-config.conf @@ -0,0 +1,18 @@ +# Optional override parameters to find fetch the MPC docker image. +MPC_IMAGE_NAME=nearone/mpc-node +MPC_IMAGE_TAGS=3.6.0 +MPC_REGISTRY=registry.hub.docker.com + +MPC_ACCOUNT_ID=mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet +MPC_LOCAL_ADDRESS=127.0.0.1 +MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +MPC_CONTRACT_ID=mpc-contract-barak-launch1-4c5e2fe1fb42.5035bf56abb0.testnet +MPC_ENV=testnet +MPC_HOME_DIR=/data +RUST_BACKTRACE=full +RUST_LOG=mpc=debug,info + +NEAR_BOOT_NODES=ed25519:9qyu1RaJ5shX6UEb7UooPQYVXCC1tNHCiDPPxJ8Pv1UJ@116.202.220.238:34567,ed25519:8mzYnfuT5zQYqV99CfYAX6XoRmNxVJ1nAZHXXW4GrFD@34.221.144.70:24567,ed25519:B87Qq34LbWadFx2dq5bwUEtB5KBgr8ZhsoEpAiSP2qVX@142.132.203.80:24567,ed25519:EufXMhFVixgFpg2bBaHGL4Zrks1DDrhAZTQYwbjRTAUX@65.109.25.109:24567,ed25519:HJJde5skATXLA4wGk8P9awvfzaW47tCU2EsRXnMoFRA9@129.150.39.19:24567,ed25519:BavpjuYrnXRFQVWjLdx9vx9vAvanit9NhhcPeM6gjAkE@95.217.198.233:24567,ed25519:81zk9MvvoxB1AzTW721o9m2NeYx3pDFDZyRJUQej65uc@195.14.6.172:24567,ed25519:E4gQXBovauvqxx85TdemezhkDDsAsqEL7ZJ4cp5Cdhsb@129.80.119.109:24567,ed25519:6cWtXFAzqpZ8D7EpLGYBmkw95oKYkzN8i99UcRgsyRMy@164.132.247.155:24567,ed25519:CLnWy9xv2GUqfgepzLwpv4bozj3H3kgzjbVREyS6wcqq@47.242.112.172:24567,ed25519:2NmT9Wy9HGBmH8sTWSq2QfaMk4R8ZHBEhk8ZH4g4f1Qk@65.109.88.175:24567,ed25519:9dhPYd1ArZ6mTMP7nnRzm8JBPwKCaBxiYontS5KfXz5h@34.239.1.54:24567,ed25519:8iiQH4vtqsqWgsm4ypCJQQwqJR3AGp9o7F69YRaCHKxA@141.95.204.11:24567,ed25519:4L97JnFFFVbfE8M3tY9bRtgV5376y5dFH8cSaoBDRWnK@5.199.170.103:24567,ed25519:DGJ91V2wJ8NFpkqZvphtSeM4CBeiLsrHGdinTugiRoFF@52.35.74.212:24567,ed25519:B9LSvCTimoEUtuUvpfu1S54an54uTetVabmkT5dELUCN@91.134.22.129:24567,ed25519:cRGmtzkkSZT6wXNjbthSXMD6dHrEgSeDtiEJAcnLLxH@15.204.213.166:24567 +# needed: Port forwarding - telemetry. +PORTS=8080:8080,3030:3030,80:80,24567:24567 + diff --git a/tee_launcher/using-the-launcher-in-nontee-setup.md b/tee_launcher/using-the-launcher-in-nontee-setup.md new file mode 100644 index 000000000..a5d66bb41 --- /dev/null +++ b/tee_launcher/using-the-launcher-in-nontee-setup.md @@ -0,0 +1,147 @@ +# Running the MPC Launcher in Non-TEE Mode + +This guide describes the **high-level steps** required to run the MPC launcher **outside of a TEE**, using the same launcher and MPC Docker images as in TEE deployments. + +The goal is to allow developers and operators to exercise the **real production launcher flow** (image selection, validation, container launch, upgrades) **without relying on TEE / dstack infrastructure**. + +--- + +## Overview + +In non-TEE mode: +- The launcher runs with `PLATFORM=NONTEE` +- No dstack socket or attestation is used +- RTMR extensions are skipped +- Image hash validation and upgrade logic remain unchanged +- The MPC container is launched with DSTACK_ENDPOINT set to dstack.sock + +This provides maximum parity with production while keeping the setup simple. + +--- + +## Prerequisites + +- Docker installed and running +- Docker Compose (classic `docker-compose` or plugin `docker compose`) +- Network access to pull Docker images +- A valid MPC Docker image digest + +--- + +## Files Used + +- **Launcher Docker Compose (non-TEE)** + Example: + `./launcher_docker_compose_nontee.yaml` + +- **User configuration file** + Example: + `./user-config.conf` (mounted into the launcher container) + +--- + +## Step-by-Step Instructions + +### 1. Prepare the non-TEE docker-compose file + +Create or use a non-TEE launcher compose file with the following properties: + +- Set `PLATFORM=NONTEE` +- Mount `/var/run/docker.sock` +- Do **not** mount `/var/run/dstack.sock` +- Mount the user config file at `/tapp/user_config` +- Provide persistent volumes for shared state and MPC data + +See example: +``` +./tee_launcher/launcher_docker_compose_nontee.yaml +``` + +--- + +### 2. Prepare the user configuration file + +Create a `user-config.conf` file containing: +- MPC image selection parameters (`MPC_IMAGE_NAME`, `MPC_IMAGE_TAGS`, `MPC_REGISTRY`) +- MPC runtime configuration (account ID, contract ID, network, logging, etc.) +- Optional port mappings using `PORTS=` + +This file is read by the launcher and passed (safely) into the MPC container. + +--- + +### 3. Start the launcher + +From the directory containing the non-TEE compose file: + +```bash +docker-compose -f launcher_docker_compose_nontee.yaml up -d +``` + +--- + +### 4. Monitor launcher logs + +```bash +docker logs -f launcher +``` + +You should see: +- `Launcher platform: NONTEE` +- Image hash selection and validation +- `PLATFORM=NONTEE → skipping RTMR3 extension` +- A `docker run ...` command launching `mpc-node` + +--- + +### 5. Verify the MPC container + +Check that the MPC container is running: + +```bash +docker ps +``` + +Inspect the MPC container to confirm: +- No `DSTACK_ENDPOINT` environment variable exists +- No dstack socket is mounted +- Expected ports are published + +Example: +```bash +docker inspect mpc-node +``` + +--- + + + +--- + +## Stopping and Restarting + +Stop and remove containers: +```bash +docker-compose -f launcher_docker_compose_nontee.yaml down +``` + +Stop and remove containers **and volumes** (full reset): +```bash +docker-compose -f launcher_docker_compose_nontee.yaml down -v +``` + +Restart: +```bash +docker-compose -f launcher_docker_compose_nontee.yaml up -d +``` + +--- + +## Notes + +- Non-TEE mode is intended for **testing, development, and debugging** +- Security-sensitive behavior for TEE deployments remains unchanged +- The same launcher image and MPC image hashes are used in both modes + +--- + From a2754594409c6b4bec9fd598b1c39d51d0a4e57e Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Wed, 25 Mar 2026 17:22:09 +0000 Subject: [PATCH 3/9] fix: update Cargo.lock and fix broken markdown link - Regenerate Cargo.lock after merge with main (reqwest 0.12 -> 0.13 for tee-launcher) - Fix link to renamed docs/updating-launcher-internal-guide.md in test-utils README Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 2 +- crates/test-utils/assets/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7dd17889a..3e16097a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10840,7 +10840,7 @@ dependencies = [ "httpmock", "launcher-interface", "near-mpc-bounded-collections", - "reqwest 0.12.28", + "reqwest 0.13.2", "serde", "serde_json", "tempfile", diff --git a/crates/test-utils/assets/README.md b/crates/test-utils/assets/README.md index c0445a918..7f4b63000 100644 --- a/crates/test-utils/assets/README.md +++ b/crates/test-utils/assets/README.md @@ -1,6 +1,6 @@ # Updating Test Assets -Updating test assets is needed when updating launcher code (or when updating other measured components). See [UPDATING_LAUNCHER.md](../../../docs/UPDATING_LAUNCHER.md) +Updating test assets is needed when updating launcher code (or when updating other measured components). See [updating-launcher-internal-guide.md](../../../docs/updating-launcher-internal-guide.md) To update the test asset files, fetch `/public_data` from the MPC node’s public HTTP endpoint and save the response to a JSON file. From 2f1835c5329200ff5532ef723fb7e01da4a9cc53 Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Mon, 23 Mar 2026 14:00:20 +0000 Subject: [PATCH 4/9] add TOML config template and localnet env script for Rust launcher --- .../tee/scripts/node.conf.localnet.toml.tpl | 58 +++++++++++++++++++ localnet/tee/scripts/set-localnet-env.sh | 31 ++++++++++ 2 files changed, 89 insertions(+) create mode 100644 localnet/tee/scripts/node.conf.localnet.toml.tpl create mode 100644 localnet/tee/scripts/set-localnet-env.sh diff --git a/localnet/tee/scripts/node.conf.localnet.toml.tpl b/localnet/tee/scripts/node.conf.localnet.toml.tpl new file mode 100644 index 000000000..292b628d2 --- /dev/null +++ b/localnet/tee/scripts/node.conf.localnet.toml.tpl @@ -0,0 +1,58 @@ +[launcher_config] +image_tags = ["${MPC_IMAGE_TAGS}"] +image_name = "${MPC_IMAGE_NAME}" +registry = "${MPC_REGISTRY}" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ +${PORTS_TOML}] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "plain" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "${NEAR_BOOT_NODES}" +genesis_path = "/app/localnet-genesis.json" +download_genesis = false + +[mpc_node_config.secrets] +secret_store_key_hex = "${MPC_SECRET_STORE_KEY}" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_node_config.node] +my_near_account_id = "${MPC_ACCOUNT_ID}" +near_responder_account_id = "${MPC_ACCOUNT_ID}" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "${MPC_CONTRACT_ID}" +finality = "optimistic" + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/localnet/tee/scripts/set-localnet-env.sh b/localnet/tee/scripts/set-localnet-env.sh new file mode 100644 index 000000000..176098bab --- /dev/null +++ b/localnet/tee/scripts/set-localnet-env.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Source this file before running deploy-tee-localnet.sh: +# source localnet/tee/scripts/set-localnet-env.sh + +export HOST_PROFILE=alice +export MODE=localnet +export MPC_NETWORK_BASE_NAME=mpc-local +export REUSE_NETWORK_NAME=mpc-local +export N=2 + +export MACHINE_IP=51.68.219.1 +export BASE_PATH=/mnt/data/barak/dstack +export VMM_RPC=http://127.0.0.1:10000 + +export MPC_IMAGE_NAME=nearone/mpc-node +export MPC_IMAGE_TAGS=main-9515e18 +export MPC_REGISTRY=registry.hub.docker.com + +export NEAR_NETWORK_CONFIG=mpc-localnet +export NEAR_RPC_URL=http://127.0.0.1:3030 +export ACCOUNT_SUFFIX=.test.near + +export FUNDER_ACCOUNT=test.near +export FUNDER_PRIVATE_KEY="$(jq -r '.secret_key' ~/.near/mpc-localnet/validator_key.json)" + +export MAX_NODES_TO_FUND=2 + +export NEAR_TX_SLEEP_SEC=1 +export NEAR_RETRY_SLEEP_SEC=2 + +export RESUME=0 From 23afcc4dbcd6100895dcfc53ed1e27ec7e8115d5 Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Mon, 23 Mar 2026 14:12:14 +0000 Subject: [PATCH 5/9] move Rust launcher scripts to rust-launcher/ subfolder --- .../rust-launcher/deploy-tee-localnet.sh | 1656 +++++++++++++++++ .../how-to-run-localnet-tee-setup-script.md | 194 ++ .../node.conf.localnet.toml.tpl | 0 .../{ => rust-launcher}/set-localnet-env.sh | 0 .../tee/scripts/rust-launcher/single-node.sh | 295 +++ 5 files changed, 2145 insertions(+) create mode 100644 localnet/tee/scripts/rust-launcher/deploy-tee-localnet.sh create mode 100644 localnet/tee/scripts/rust-launcher/how-to-run-localnet-tee-setup-script.md rename localnet/tee/scripts/{ => rust-launcher}/node.conf.localnet.toml.tpl (100%) rename localnet/tee/scripts/{ => rust-launcher}/set-localnet-env.sh (100%) create mode 100644 localnet/tee/scripts/rust-launcher/single-node.sh diff --git a/localnet/tee/scripts/rust-launcher/deploy-tee-localnet.sh b/localnet/tee/scripts/rust-launcher/deploy-tee-localnet.sh new file mode 100644 index 000000000..9223e9e9a --- /dev/null +++ b/localnet/tee/scripts/rust-launcher/deploy-tee-localnet.sh @@ -0,0 +1,1656 @@ +#!/usr/bin/env bash +set -euo pipefail +export NEAR_CLI_DISABLE_SPINNER=1 + +# NEAR network selection +# - For testnet: NEAR_NETWORK_CONFIG=testnet (default) +# - For localnet: export NEAR_NETWORK_CONFIG=mpc-localnet (matches your near-cli config name) +NEAR_NETWORK_CONFIG="${NEAR_NETWORK_CONFIG:-testnet}" +# Used by curl-based RPC helpers (balance/bootnodes). Override for localnet if needed. +NEAR_RPC_URL="${NEAR_RPC_URL:-$NEAR_RPC_URL}" +# MPC environment label used by templates / node config +# Default: "testnet" when NEAR_NETWORK_CONFIG=testnet, otherwise "localnet" +MPC_ENV="${MPC_ENV:-mpc-localnet}" + + + + +### ========================= +### Fully-automated, resumable MPC testnet TEE deploy (with subaccounts) +### - Avoids faucet 429 by allowing a funded FUNDER_ACCOUNT to create/top-up ROOT +### - Subaccounts are ALWAYS created by ROOT (required by NEAR permission model) +### - Contract created as mpc., nodes as node{i}. +### - Default funding supports contract (~16 NEAR) + up to 10 nodes (0.3 NEAR ea) + buffer +### - Resume logic + per-phase ENTER prompts +### - NEW: scale/extend network with vote_new_parameters (add nodes) +### +### NOTE on RESUME: +### - RESUME=1 is intended for reruns: it reuses existing artifacts and "auto" phase selection. +### - RESUME=0 primarily affects auto phase selection; artifacts may still exist on disk and +### some steps may still skip based on those artifacts unless FORCE_* is set. +### ========================= + +### Required inputs +: "${MPC_NETWORK_BASE_NAME:?Must set MPC_NETWORK_BASE_NAME (e.g. export MPC_NETWORK_BASE_NAME=barak-test)}" +: "${N:?Must set N (e.g. export N=10)}" +: "${BASE_PATH:?Must set BASE_PATH to dstack base path (contains vmm/src/vmm-cli.py)}" +: "${MACHINE_IP:?Must set MACHINE_IP (external IP for localnet node comms)}" + +: "${MPC_IMAGE_TAGS:?Must set MPC_IMAGE_TAGS (e.g. export MPC_IMAGE_TAGS=3.3.0)}" + +# If set, use this funded testnet account instead of faucet to create/top-up the ROOT account. +# Example: export FUNDER_ACCOUNT=barak_tee_test1.testnet +FUNDER_ACCOUNT="${FUNDER_ACCOUNT:-}" +# Optional: private key for FUNDER_ACCOUNT (useful for localnet where funder may not be in keychain) +FUNDER_PRIVATE_KEY="${FUNDER_PRIVATE_KEY:-}" + +# How much balance to ensure on ROOT (used for creating contract+nodes subaccounts) +# Default supports ~16 NEAR contract + 10 * 0.3 NEAR nodes + ~1 NEAR buffer => 20 NEAR +ROOT_INITIAL_BALANCE="${ROOT_INITIAL_BALANCE:-20 NEAR}" + +### Optional controls +ACCOUNT_MODE="${ACCOUNT_MODE:-subaccounts}" # subaccounts|faucet (faucet is fallback only) + +# Initial balances (for subaccounts mode) +CONTRACT_INITIAL_BALANCE="${CONTRACT_INITIAL_BALANCE:-20 NEAR}" +NODE_INITIAL_BALANCE="${NODE_INITIAL_BALANCE:-3.0 NEAR}" + +# How many nodes to fund for, even if N is smaller (so you can scale later without re-funding root) +MAX_NODES_TO_FUND="${MAX_NODES_TO_FUND:-10}" + +# Faucet retry/backoff (for root creation if FUNDER_ACCOUNT is not set) +FAUCET_MAX_RETRIES="${FAUCET_MAX_RETRIES:-8}" +FAUCET_BACKOFF_BASE_SEC="${FAUCET_BACKOFF_BASE_SEC:-10}" + +# Resume behavior +RESUME="${RESUME:-1}" +FORCE_REDEPLOY="${FORCE_REDEPLOY:-0}" +FORCE_RECOLLECT="${FORCE_RECOLLECT:-0}" +FORCE_REINIT_ARGS="${FORCE_REINIT_ARGS:-0}" + +# Phase gating +START_FROM_PHASE="${START_FROM_PHASE:-auto}" # auto recommended +STOP_AFTER_PHASE="${STOP_AFTER_PHASE:-}" + +# Pause between phases +NO_PAUSE="${NO_PAUSE:-0}" + +# --- Scale / add nodes controls --- +# Set ADD_NODES>0 (or set NEW_TOTAL_N) and run phase near_vote_new_parameters (add nodes) +ADD_NODES="${ADD_NODES:-0}" # how many NEW nodes to add +NEW_TOTAL_N="${NEW_TOTAL_N:-}" # optional: absolute total count after scaling +NEW_THRESHOLD_OVERRIDE="${NEW_THRESHOLD_OVERRIDE:-}" # optional: override threshold for new proposal + +# If set, reuse existing network name (and NEAR accounts) +if [ -n "${REUSE_NETWORK_NAME:-}" ]; then + RAND_SUFFIX="(reused)" + MPC_NETWORK_NAME="${REUSE_NETWORK_NAME}" +else + RAND_SUFFIX="$(printf '%04x' $((RANDOM % 65536)))" + MPC_NETWORK_NAME="${MPC_NETWORK_BASE_NAME}-${RAND_SUFFIX}" +fi + +### Constants / defaults +# Host profile: alice | bob +HOST_PROFILE="${HOST_PROFILE:-bob}" + +case "$HOST_PROFILE" in + alice) + IP_PREFIX="51.68.219." + IP_START_OCTET=1 + ;; + bob) + IP_PREFIX="5.196.36." + IP_START_OCTET=113 + ;; + *) + err "Unknown HOST_PROFILE: $HOST_PROFILE (supported: alice | bob)" + exit 1 + ;; +esac + +# Optional per-node IP override (format: "5=5.196.36.113 6=5.196.36.114 ...") +NODE_IP_OVERRIDES="${NODE_IP_OVERRIDES:-}" + +SSH_BASE=1220 +AGENT_BASE=18090 +PUBLIC_DATA_BASE=18081 +LOCAL_DEBUG_BASE=3031 + +STATE_SYNC_PORT=24567 +MAIN_PORT=80 +FUTURE_PORT=13001 +FUTURE_BASE_PORT="${FUTURE_BASE_PORT:-13001}" # host-side per-node future/N2N port base +future_port_for_i() { echo $((FUTURE_BASE_PORT + $1)); } + +INTERNAL_PUBLIC_DEBUG_PORT=8080 +INTERNAL_LOCAL_DEBUG_PORT=3030 +INTERNAL_STATE_SYNC_PORT=24567 +INTERNAL_MAIN_PORT=80 +INTERNAL_FUTURE_PORT=13001 + +OS_IMAGE="${OS_IMAGE:-dstack-dev-0.5.4}" +SEALING_KEY_TYPE="${SEALING_KEY_TYPE:-SGX}" +VMM_RPC="${VMM_RPC:-http://127.0.0.1:10000}" + +# Repo-relative paths (assumes you're running from repo root) +REPO_ROOT="$(pwd)" +TEE_LAUNCHER_DIR="$REPO_ROOT/deployment/cvm-deployment" +COMPOSE_YAML="$TEE_LAUNCHER_DIR/launcher_docker_compose.yaml" +ADD_DOMAIN_JSON="$REPO_ROOT/docs/localnet/args/add_domain.json" + +MODE="${MODE:-testnet}" # testnet|localnet + +# templates live here (UPDATED for move to localnet/tee/scripts) +ENV_TPL="$REPO_ROOT/localnet/tee/scripts/node.env.tpl" +if [ "$MODE" = "localnet" ]; then + CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl" +else + CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.tpl" +fi + +# Convert comma-separated "host:container" port string to TOML inline table array entries. +# E.g. "8080:8080,24566:24566" -> " { host = 8080, container = 8080 },\n..." +ports_to_toml() { + local ports="$1" result="" + IFS=',' read -ra pairs <<< "$ports" + for pair in "${pairs[@]}"; do + local host_port="${pair%%:*}" + local container_port="${pair##*:}" + result+=" { host =$host_port, container =$container_port }, +" + done + echo -n "$result" +} + +WORKDIR="/tmp/$USER/mpc_testnet_scale/$MPC_NETWORK_NAME" +mkdir -p "$WORKDIR" + +# Derived accounts +ACCOUNT_SUFFIX="${ACCOUNT_SUFFIX:-.testnet}" # localnet example: ".test.near" +ROOT_ACCOUNT="${MPC_NETWORK_NAME}${ACCOUNT_SUFFIX}" + +# Subaccount naming (REQUIRED for subaccounts mode) +MPC_CONTRACT_ACCOUNT="${MPC_CONTRACT_ACCOUNT:-mpc.${ROOT_ACCOUNT}}" +node_account_for_i() { echo "node$1.${ROOT_ACCOUNT}"; } + +NODE_RANGE_START="${NODE_RANGE_START:-0}" +NODE_RANGE_END="${NODE_RANGE_END:-$((N-1))}" + +# Artifact paths +KEYS_JSON="$WORKDIR/keys.json" +INIT_ARGS_JSON="$WORKDIR/init_args.json" +KEYS_NEW_JSON="$WORKDIR/keys_new.json" +VOTE_PARAMS_JSON="$WORKDIR/vote_new_parameters.json" + +# Retry/sleep knobs (FIX #1) +NEAR_TX_SLEEP_SEC="${NEAR_TX_SLEEP_SEC:-3}" + +near_sleep() { + local reason="${1:-after NEAR tx}" + log "Sleeping ${NEAR_TX_SLEEP_SEC}s (${reason})" + sleep "$NEAR_TX_SLEEP_SEC" +} + +# ---------- logging ---------- +log() { echo -e "\033[1;34m[INFO]\033[0m $*"; } +warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; } +err() { echo -e "\033[1;31m[ERROR]\033[0m $*"; } + +pause_phase() { + local name="$1" + if [ "$NO_PAUSE" = "1" ]; then + log "NO_PAUSE=1 -> continuing automatically (phase: $name)" + return 0 + fi + echo + echo "------------------------------------------------------------" + echo "Phase: $name" + echo "Network: $MPC_NETWORK_NAME" + echo "Workdir: $WORKDIR" + echo "Account mode: $ACCOUNT_MODE" + echo "ROOT_ACCOUNT: $ROOT_ACCOUNT" + echo "CONTRACT_ACCOUNT: $MPC_CONTRACT_ACCOUNT" + echo "FUNDER_ACCOUNT: ${FUNDER_ACCOUNT:-}" + echo "------------------------------------------------------------" + read -r -p "Press ENTER to continue (or Ctrl+C to abort)..." _ + echo +} + +# ---------- helpers ---------- +ceil_2n_3() { local n="$1"; echo $(( (2*n + 2) / 3 )); } + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || { err "Missing required command: $1"; exit 1; } +} + +ip_for_i() { + local i="$1" + if [ -n "$NODE_IP_OVERRIDES" ]; then + local ip + ip="$(echo " $NODE_IP_OVERRIDES " | sed -n "s/.*[[:space:]]${i}=\([^[:space:]]*\).*/\1/p")" + if [ -n "$ip" ]; then + echo "$ip" + return 0 + fi + fi + echo "${IP_PREFIX}$((IP_START_OCTET + i))" +} + +ssh_port_for_i() { echo $((SSH_BASE + $1)); } +agent_port_for_i() { echo $((AGENT_BASE + $1)); } +public_port_for_i() { echo 18082; } # fixed public_data port +local_dbg_port_for_i() { echo $((LOCAL_DEBUG_BASE + $1)); } + +# NOTE: this preflight check assumes the node IPs are configured as /32 addresses on the host +# (common for IP aliasing setups). If your environment uses a different prefix length, adjust +# the grep pattern accordingly. +host_has_ip() { local ip="$1"; ip addr show | grep -qE "inet ${ip}/32"; } + +port_free() { + local ip="$1" port="$2" + local addrs + addrs="$(ss -H -4 -ltn 2>/dev/null | awk '{print $4}')" + if echo "$addrs" | grep -Eq "0\.0\.0\.0:${port}$|${ip//./\\.}:${port}$"; then + return 1 + fi + return 0 +} + +file_nonempty() { local p="$1"; [ -f "$p" ] && [ -s "$p" ]; } + +# keys.json is a JSON array; treat [] as "empty" even though the file is non-empty. +json_array_has_entries() { + local p="$1" + [ -f "$p" ] || return 1 + set +e + local n + n="$(jq 'length' "$p" 2>/dev/null)" + local rc=$? + set -e + [ $rc -eq 0 ] && [[ "$n" =~ ^[0-9]+$ ]] && [ "$n" -gt 0 ] +} + +maybe_stop_after_phase() { + local phase="$1" + if [ -n "$STOP_AFTER_PHASE" ] && [ "$STOP_AFTER_PHASE" = "$phase" ]; then + warn "STOP_AFTER_PHASE=$STOP_AFTER_PHASE requested. Stopping now." + exit 0 + fi +} + +# ---------- phase gating ---------- +phase_rank() { + case "$1" in + preflight) echo 10 ;; + render) echo 20 ;; + near_accounts) echo 30 ;; + near_nodes) echo 40 ;; + near_contract) echo 50 ;; + deploy) echo 60 ;; + collect) echo 70 ;; + init_args) echo 75 ;; + near_keys) echo 80 ;; + near_init) echo 90 ;; + near_vote_hash) echo 93 ;; + near_vote_launcher_hash) echo 94 ;; + near_vote_measurement) echo 95 ;; + near_vote_domain) echo 96 ;; + near_vote_new_params) echo 98 ;; + near_vote_new_params_votes) echo 99 ;; + + auto) echo 0 ;; + *) err "Unknown phase name: $1"; exit 1 ;; + esac +} + +should_run_from_start() { + local phase="$1" + local start="$START_FROM_PHASE" + if [ "$start" = "auto" ]; then + return 0 + fi + local pr sr + pr="$(phase_rank "$phase")" + sr="$(phase_rank "$start")" + [ "$pr" -ge "$sr" ] +} + +compute_auto_start_phase() { + if [ "$START_FROM_PHASE" != "auto" ]; then + echo "$START_FROM_PHASE" + return 0 + fi + if [ "$RESUME" != "1" ]; then + echo "preflight" + return 0 + fi + if file_nonempty "$INIT_ARGS_JSON"; then + echo "near_keys" + return 0 + fi + if json_array_has_entries "$KEYS_JSON"; then + echo "init_args" + return 0 + fi + echo "preflight" +} + +### ========================= +### BOOTNODES (DEDUP BY ADDR) +### ========================= +fetch_bootnodes() { + # Bootnodes + # - Override via NEAR_BOOT_NODES_OVERRIDE (recommended for fully offline setups) + # - Otherwise, query the configured RPC (NEAR_RPC_URL) for network_info + if [ -n "${NEAR_BOOT_NODES_OVERRIDE:-}" ]; then + echo "$NEAR_BOOT_NODES_OVERRIDE" + return 0 + fi + + # Best-effort: if RPC doesn't support network_info or returns empty, return empty string + set +e + local out + out="$(curl -s -X POST "$NEAR_RPC_URL" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "method": "network_info", "params": [], "id": "dontcare"}' 2>/dev/null)" + local rc=$? + set -e + if [ $rc -ne 0 ] || [ -z "$out" ]; then + echo "" + return 0 + fi + echo "$out" \ + | jq -r '.result.active_peers[]? | "\(.id)@\(.addr)"' 2>/dev/null \ + | awk -F'@' '!seen[$2]++ {print $0}' \ + | paste -sd',' - +} + + +### ========================= +### NEAR helpers (balance, create, topup) +### ========================= + +is_existing_key_error() { + local out="$1" + # This matches the exact near-cli-rs message + echo "$out" | grep -Eq "Public key is already used for an existing account ID <" +} + +near_add_key_skip_if_exists() { + # Usage: near_add_key_skip_if_exists "" "" "