diff --git a/.agent/GPU_TEE_DEPLOYMENT.md b/.agent/GPU_TEE_DEPLOYMENT.md new file mode 100644 index 00000000..ff664eae --- /dev/null +++ b/.agent/GPU_TEE_DEPLOYMENT.md @@ -0,0 +1,235 @@ +# GPU TEE Deployment Guide + +Learnings from deploying GPU workloads to Phala Cloud TEE infrastructure. + +## Instance Types + +Query available instance types: +```bash +curl -s "https://cloud-api.phala.network/api/v1/instance-types" | jq +``` + +### CPU-only (Intel TDX) +- `tdx.small` through `tdx.8xlarge` + +### GPU (H200 + TDX) +- `h200.small` — Single H200 GPU, suitable for inference +- `h200.16xlarge` — Multi-GPU for larger workloads +- `h200.8x.large` — High-memory configuration + +## Deployment Commands + +### GPU Deployment +```bash +phala deploy -n my-app -c docker-compose.yaml \ + --instance-type h200.small \ + --region US-EAST-1 \ + --image dstack-nvidia-dev-0.5.4.1 +``` + +Key flags: +- `--instance-type h200.small` — Required for GPU access +- `--image dstack-nvidia-dev-0.5.4.1` — NVIDIA development image with GPU drivers +- `--region US-EAST-1` — Region with GPU nodes (gpu-use2) + +### Debugging +```bash +# Check CVM status +phala cvms list + +# View serial logs (boot + container output) +phala cvms serial-logs --tail 100 + +# Delete CVM +phala cvms delete --force +``` + +## Docker Compose GPU Configuration + +GPU devices must be explicitly reserved in docker-compose.yaml: + +```yaml +services: + my-gpu-app: + image: my-image + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] +``` + +Without the `deploy.resources.reservations.devices` section, the container will fail with: +``` +libcuda.so.1: cannot open shared object file: No such file or directory +``` + +## vLLM Example + +Working docker-compose.yaml for vLLM inference: + +```yaml +services: + vllm: + image: vllm/vllm-openai:latest + volumes: + - /var/run/dstack.sock:/var/run/dstack.sock + environment: + - NVIDIA_VISIBLE_DEVICES=all + - HF_TOKEN=${HF_TOKEN:-} + ports: + - "8000:8000" + command: > + --model Qwen/Qwen2.5-1.5B-Instruct + --host 0.0.0.0 + --port 8000 + --max-model-len 4096 + --gpu-memory-utilization 0.8 + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] +``` + +## Endpoint URLs + +After deployment, the app is accessible at: +``` +https://-.dstack-pha-.phala.network +``` + +Example for vLLM on port 8000: +```bash +# List models +curl https://-8000.dstack-pha-use2.phala.network/v1/models + +# Chat completion +curl -X POST https://-8000.dstack-pha-use2.phala.network/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model": "Qwen/Qwen2.5-1.5B-Instruct", "messages": [{"role": "user", "content": "Hello"}]}' +``` + +## vllm-proxy (Response Signing) + +vllm-proxy provides response signing and attestation for vLLM inference. It sits between clients and vLLM, signing responses with TEE-derived keys. + +### Configuration + +**IMPORTANT**: The authentication environment variable is `TOKEN`, not `AUTH_TOKEN`. + +```yaml +services: + vllm: + image: vllm/vllm-openai:latest + environment: + - NVIDIA_VISIBLE_DEVICES=all + command: > + --model Qwen/Qwen2.5-1.5B-Instruct + --host 0.0.0.0 + --port 8000 + --max-model-len 4096 + --gpu-memory-utilization 0.8 + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + + proxy: + image: phalanetwork/vllm-proxy:v0.2.18 + volumes: + - /var/run/dstack.sock:/var/run/dstack.sock # Required for TEE key derivation + environment: + - VLLM_BASE_URL=http://vllm:8000 + - MODEL_NAME=Qwen/Qwen2.5-1.5B-Instruct + - TOKEN=your-secret-token # NOT AUTH_TOKEN + ports: + - "8000:8000" + depends_on: + - vllm +``` + +### API Endpoints + +```bash +# List models (no auth required) +curl https:///v1/models + +# Chat completion (requires auth) +curl -X POST https:///v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-secret-token" \ + -d '{"model": "Qwen/Qwen2.5-1.5B-Instruct", "messages": [{"role": "user", "content": "Hello"}]}' + +# Get response signature +curl https:///v1/signature/ \ + -H "Authorization: Bearer your-secret-token" + +# Attestation report +curl https:///v1/attestation/report \ + -H "Authorization: Bearer your-secret-token" +``` + +### Tested Configuration + +- Image: `phalanetwork/vllm-proxy:v0.2.18` +- Instance: `h200.small` +- Region: `US-EAST-1` +- Model: `Qwen/Qwen2.5-1.5B-Instruct` + +### vllm-proxy Issues + +**"Invalid token" error**: +- Check that you're using `TOKEN` environment variable, not `AUTH_TOKEN` +- Verify the token value matches your request header + +**"All connection attempts failed" from proxy**: +- vLLM is still loading the model (takes 1-2 minutes after container starts) +- Wait for vLLM to show "Uvicorn running on" in serial logs + +**NVML error on attestation**: +- GPU confidential computing attestation may not be fully available +- This doesn't affect inference or response signing + +## Common Issues + +### "No available resources match your requirements" +- GPU nodes are limited. Wait for other CVMs to finish or try a different region. +- Ensure you're using the correct instance type (`h200.small`). + +### Container crashes with GPU errors +- Add `deploy.resources.reservations.devices` section to docker-compose.yaml. +- Verify using NVIDIA development image (`dstack-nvidia-dev-*`). + +### Image pull takes too long +- Large images (5GB+ for vLLM) take 3-5 minutes to download and extract. +- Check serial logs for progress. + +## Testing Workflow + +1. Deploy: `phala deploy -n test -c docker-compose.yaml --instance-type h200.small --region US-EAST-1 --image dstack-nvidia-dev-0.5.4.1` +2. Wait for status: `phala cvms list` (wait for "running") +3. Check logs: `phala cvms serial-logs --tail 100` +4. Test API: `curl https://-.dstack-pha-use2.phala.network/...` +5. Cleanup: `phala cvms delete --force` + +## GPU Wrapper Script + +For repeated GPU deployments, use a wrapper script: + +```bash +#!/bin/bash +# phala-gpu.sh +source "$(dirname "$0")/.env" +export PHALA_CLOUD_API_KEY=$PHALA_CLOUD_API_GPU +phala "$@" +``` + +This allows maintaining separate API keys for CPU and GPU workspaces. diff --git a/.agent/WRITING_GUIDE.md b/.agent/WRITING_GUIDE.md new file mode 100644 index 00000000..aaf28fef --- /dev/null +++ b/.agent/WRITING_GUIDE.md @@ -0,0 +1,137 @@ +# Documentation Writing Guide + +Guidelines for writing dstack documentation, README, and marketing content. + +## Writing Style + +- **Don't over-explain** why a framework is needed — assert the solution, hint at alternatives being insufficient +- **Avoid analogies as taglines** (e.g., "X for Y") — if it's a new category, don't frame it as a better version of something else +- **Problem → Solution flow** without explicit labels like "The problem:" or "The solution:" +- **Demonstrate features through actions**, not parenthetical annotations + - Bad: "Generates quotes (enabling *workload identity*)" + - Good: "Generates TDX attestation quotes so users can verify exactly what's running" + +## Procedural Documentation (Guides & Tutorials) + +### Test Before You Document +- **Run every command** before documenting it — reading code is not enough +- Commands may prompt for confirmation, require undocumented env vars, or fail silently +- Create a test environment and execute the full flow end-to-end + +### Show What Success Looks Like +- **Add sample outputs** after commands so users can verify they're on track +- For deployment commands, show the key values users need to note (addresses, IDs) +- For validation commands, show both success and failure outputs + +### Environment Variables +- **List all required env vars explicitly** — don't assume users will discover them +- If multiple tools use similar-but-different var names, clarify which is which +- Show the export pattern once, then reference it in subsequent commands + +### Avoid Expert Blind Spots +- If you say "add the hash", explain how to compute the hash +- If you reference a file, explain where to find it +- If a value comes from a previous step, remind users which step + +### Cross-Reference Related Docs +- Link to prerequisite guides (don't repeat content) +- Link to detailed guides for optional deep-dives +- Use anchor links for specific sections when possible + +## Security Documentation + +### Trust Model Framing + +**Distinguish trust from verification:** +- "Trust" = cannot be verified, must assume correct (e.g., hardware) +- "Verify" = can be cryptographically proven (e.g., measured software) + +**Correct framing:** +- Bad: "You must trust the OS" (when it's verifiable) +- Good: "The OS is measured during boot and recorded in the attestation quote. You verify it by..." + +### Limitations: Be Honest, Not Alarmist + +State limitations plainly without false mitigations: +- Bad: "X is a single point of failure. Mitigate by running your own X." +- Good: "X is protected by [mechanism]. Like all [category] systems, [inherent limitation]. We are developing [actual solution] to address this." + +Don't suggest mitigations that don't actually help. If something is an inherent limitation of the technology, say so. + +## Documentation Quality Checklist + +From doc-requirements.md: + +1. **No bullet point walls** — Max 3-5 bullets before breaking with prose +2. **No redundancy** — Don't present same info from opposite perspectives +3. **Conversational language** — Write like explaining to a peer +4. **Short paragraphs** — Max 4 sentences per paragraph +5. **Lead with key takeaway** — First sentence tells reader why this matters +6. **Active voice** — "TEE encrypts memory" not "Memory is encrypted by TEE" +7. **Minimal em-dashes** — Max 1-2 per page, replace with "because", "so", or separate sentences + +### Redundancy Patterns to Avoid + +These often say the same thing: +- "What we protect against" + "What you don't need to trust" +- "Security guarantees" + "What attestation proves" + +Combine into single sections. One detailed explanation, brief references elsewhere. + +## README Structure + +### Order Matters +- **Quick Start before Prerequisites** — Lead with what it does, not setup +- **How It Works after Quick Start** — Users want to run it first, understand later +- Cleanup at the end, Further Reading last + +### Don't Duplicate +- Link to conceptual docs instead of repeating content +- If an overview README duplicates an example README, cut the overview +- One detailed explanation, brief references elsewhere + +### Remove Unrealistic Sections +- If most users can't actually do something (e.g., run locally without special hardware), don't include it +- Don't document workflows that require resources users don't have + +### Match the Workflow to the User +- Use tools your audience already knows (e.g., Jupyter for ML practitioners) +- Prefer official/existing images when they exist — don't reinvent +- Make the correct path the default, mention alternatives briefly + +## Code Examples + +### Question Every Snippet +- Does this code actually demonstrate something meaningful? +- Would a reader understand what it does without the prose? +- `do_thing(b"magic-string")` means nothing — show real use or remove it + +### Diagrams +- Mermaid over ASCII art — GitHub renders it nicely +- Keep diagrams simple — 3-5 nodes max +- Label edges with actions, not just arrows + +## Conciseness + +### Less is More +- 30 lines beats 150 if it says the same thing +- Cut sections that don't help users accomplish their goal +- Tables for reference, prose for explanation — don't over-table + +### Performance and Benchmarks +- One memorable number + link to full report +- Don't overwhelm with data the reader didn't ask for + +### Reader-First Writing +- Ask "what does the reader want to know?" not "what do I want to say?" +- If a section answers a question nobody asked, cut it + +## Maintenance + +### Consistency Checks +- After terminology changes, grep for related terms across all files +- Use correct industry/vendor terminology (e.g., "Confidential Computing" not "Encrypted Computing") + +### Clean Up Old Files +- When approach changes, delete orphaned files (old scripts, Dockerfiles) +- Don't leave artifacts from previous implementations diff --git a/.gitignore b/.gitignore index c8b7bba0..8c0d716f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ node_modules/ /tmp .claude/settings.local.json __pycache__ +.planning/ diff --git a/CLAUDE.md b/CLAUDE.md index 502564d0..2a2f5043 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,3 +224,9 @@ RPC definitions use `prpc` framework with Protocol Buffers: - Design decisions: `docs/design-and-hardening-decisions.md` When need more detailed info, try to use deepwiki mcp. + +## Agent Resources + +The `.agent/` directory contains AI assistant resources: +- `WRITING_GUIDE.md` — Documentation and README writing guidelines (messaging, style, audiences) +- `GPU_TEE_DEPLOYMENT.md` — GPU deployment to Phala Cloud (instance types, docker-compose config, debugging) diff --git a/Cargo.lock b/Cargo.lock index 348a9493..9b4babe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1731,9 +1731,8 @@ version = "0.5.5" dependencies = [ "anyhow", "clap", - "dstack-gateway-rpc", + "hex", "hex_fmt", - "ra-rpc", "regex", "reqwest", "serde", diff --git a/README.md b/README.md index ac18dd98..2ed0d551 100644 --- a/README.md +++ b/README.md @@ -2,499 +2,208 @@ ![dstack](./dstack-logo.svg) -**Deploy containerized apps to TEE with end-to-end security in minutes.** +### The open framework for confidential AI. [![GitHub Stars](https://img.shields.io/github/stars/dstack-tee/dstack?style=flat-square&logo=github)](https://github.com/Dstack-TEE/dstack/stargazers) [![License](https://img.shields.io/github/license/dstack-tee/dstack?style=flat-square)](https://github.com/Dstack-TEE/dstack/blob/master/LICENSE) -[![REUSE status](https://api.reuse.software/badge/github.com/fsfe/reuse-tool)](https://api.reuse.software/info/github.com/fsfe/reuse-tool) -[![Telegram](https://img.shields.io/badge/Community-blue?style=flat-square&logo=telegram&logoColor=fff)](https://t.me/+UO4bS4jflr45YmUx) +[![REUSE status](https://api.reuse.software/badge/github.com/Dstack-TEE/dstack)](https://api.reuse.software/info/github.com/Dstack-TEE/dstack) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Dstack-TEE/dstack) +[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-square&logo=telegram&logoColor=white)](https://t.me/+UO4bS4jflr45YmUx) -[![Repobeats Analytics](https://repobeats.axiom.co/api/embed/0a001cc3c1f387fae08172a9e116b0ec367b8971.svg "Repobeats analytics image")](https://github.com/Dstack-TEE/dstack/pulse) +[Documentation](https://docs.phala.com/dstack) · [Examples](https://github.com/Dstack-TEE/dstack-examples) · [Community](https://t.me/+UO4bS4jflr45YmUx) --- -## 🚀 Overview +## What is dstack? -dstack is a **developer friendly** and **security first** SDK to simplify the deployment of arbitrary containerized apps into TEE. +dstack is the open framework for confidential AI - deploy AI applications with cryptographic privacy guarantees. -### ✨ Key Features +AI providers ask users to trust them with sensitive data. But trust doesn't scale, and trust can't be verified. With dstack, your containers run inside confidential VMs (Intel TDX) with native support for NVIDIA Confidential Computing (H100, Blackwell). Users can cryptographically verify exactly what's running: private AI with your existing Docker workflow. -- 🔒 **Secure Deployment**: Deploy containerized apps securely in TEE in minutes -- 🛠️ **Familiar Tools**: Use familiar tools - just write a docker-compose.yaml -- 🔑 **Secret Management**: Safely manage secrets and sensitive data -- 📡 **ZT-HTTPS**: Expose services via automated TLS termination +### Features ---- - -## 👥 Community - -dstack is community driven. Open sourced and built by [Kevin Wang](https://github.com/kvinwang) and many others from [Phala Network](https://github.com/Phala-Network), inspired by [Andrew Miller](https://github.com/amiller) (Flashbots & Teleport), and contributed by [Nethermind](https://github.com/NethermindEth/nethermind) and [many others](#contributors). - ---- - -## 📋 Table of Contents - -- [Architecture](#%EF%B8%8F-architecture) -- [Getting Started](#-getting-started) - - [Prerequisites](#prerequisites) - - [Install Dependencies](#install-dependencies) - - [Build and Run](#build-and-run) -- [Usage](#-usage) - - [Deploy an App](#deploy-an-app) - - [Pass Secrets to Apps](#pass-secrets-to-apps) - - [Access the App](#access-the-app) - - [Getting TDX Quote](#getting-tdx-quote-in-docker-container) - - [Container Logs](#container-logs) - - [TLS Passthrough](#tls-passthrough-with-custom-domain) - - [Upgrade an App](#upgrade-an-app) -- [Advanced Features](#-advanced-features) - - [Zero Trust HTTPS](#zero-trust-https) - - [Certificate Transparency Log Monitor](#certificate-transparency-log-monitor) -- [Security](#-security) -- [Troubleshooting](#-troubleshooting) -- [Media Kit](#-media-kit) -- [License](#-license) - ---- - -## 🏗️ Architecture - -
- -Architecture Diagram - -
- -- **`dstack-vmm`**: A service running in a bare TDX host to manage CVMs -- **`dstack-gateway`**: A reverse proxy to forward TLS connections to CVMs -- **`dstack-kms`**: A KMS server to generate keys for CVMs -- **`dstack-guest-agent`**: A service running in CVM to serve containers' key derivation and attestation requests -- **`meta-dstack`**: A Yocto meta layer to build CVM guest images - ---- - -## 🚀 Getting Started - -### Prerequisites - -- A bare metal TDX server setup following [canonical/tdx](https://github.com/canonical/tdx) -- Public IPv4 address assigned to the machine -- At least 16GB RAM, 100GB free disk space -- A domain with DNS access if you want to set up `dstack-gateway` for Zero Trust HTTPS - -> [!NOTE] -> -> Check the [Hardware Requirements](https://docs.phala.network/dstack/hardware-requirements) for more information on buying a bare metal server or renting a server from cloud providers. -> -> If you are looking for a cloud-managed dstack, go to the docs to learn how to [sign up for a Phala Cloud Account](https://docs.phala.network/phala-cloud/getting-started/sign-up-for-cloud-account) and [deploy your first CVM on dstack](https://docs.phala.network/phala-cloud/getting-started/start-from-cloud-ui). - -### Install Dependencies - -```bash -# For Ubuntu 24.04 -sudo apt install -y build-essential chrpath diffstat lz4 wireguard-tools xorriso - -# Install Rust -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -### Build and Run - -#### 1. Build the Artifacts - -```bash -git clone https://github.com/Dstack-TEE/meta-dstack.git --recursive -cd meta-dstack/ - -mkdir build -cd build -../build.sh hostcfg -``` - -This outputs the following message: -``` -Config file ../build-config.sh created, please edit it to configure the build -``` - -Review and customize the `build-config.sh` configuration file according to your environment requirements. The file contains network ports, domain settings, and other important parameters. - -```bash -vim ./build-config.sh -``` -Once configured, run the build script again to generate the necessary artifacts: - -```bash -../build.sh hostcfg -``` - -If everything is okay, you should see the built artifacts in the `build` directory: - -```bash -$ ls -certs images dstack-kms kms.toml run dstack-vmm vmm.toml dstack-gateway gateway.toml -``` - -#### 2. Download or Build Guest Image - -**Option A: Download guest image** -```bash -# This will download the guest image from the release page -../build.sh dl 0.5.2 -``` - -**Option B: Build from source** -```bash -# This will build the guest image from source using the yocto meta layer. This will take a while. -../build.sh guest -``` - -> [!IMPORTANT] -> **Note on Reproducible Builds**: The build command above does not guarantee reproducibility. For reproducible builds across different build environments, please refer to the [reproducible build instructions](https://github.com/Dstack-TEE/meta-dstack?tab=readme-ov-file#reproducible-build-the-guest-image) in the meta-dstack repository. - -#### 3. Run Components - -Now you can open 3 terminals to start the components: - -1. **KMS**: `./dstack-kms -c kms.toml` -2. **Gateway**: `sudo ./dstack-gateway -c gateway.toml` -3. **VMM**: `./dstack-vmm -c vmm.toml` - -> [!WARNING] -> This configuration is for local development, and the kms is not secure. You should not use it in production. For production, you should follow the [deployment guide](./docs/deployment.md) and read the [security guide](./docs/security-guide/security-guide.md). - ---- - -## 📱 Usage - -You can manage VMs via the dashboard or [CLI](./docs/vmm-cli-user-guide.md). - -### Deploy an App - -Open the dstack-vmm webpage [http://localhost:9080](http://localhost:9080) (change the port according to your configuration) on your local machine to deploy a `docker-compose.yaml` file: - -
- -VMM Interface - -
- -After the container is deployed, it should take some time to start the CVM and the containers. Time would be vary depending on your workload. - -- **[Logs]**: Click this button to view the CVM logs and monitor container startup progress -- **[Dashboard]**: Once the container is running, click this button to view container information and logs. (Note: This button is only visible when dstack-gateway is enabled. If disabled, you'll need to add a port mapping to port 8090 to access the CVM dashboard) - -
- -Guest Agent Dashboard - -
- -### Pass Secrets to Apps +**Zero friction onboarding** +- **Docker Compose native**: Bring your docker-compose.yaml as-is. No SDK, no code changes. +- **Encrypted by default**: Network traffic and disk storage encrypted out of the box. -When deploying a new App, you can pass private data via Encrypted Environment Variables. These variables can be referenced in the docker-compose.yaml file as shown below: +**Hardware-rooted security** +- **Private by hardware**: Data encrypted in memory, inaccessible even to the host. +- **Reproducible OS**: Deterministic builds mean anyone can verify the OS image hash. +- **Workload identity**: Every app gets an attested identity users can verify cryptographically. +- **Confidential GPUs**: Native support for NVIDIA Confidential Computing (H100, Blackwell). -
- -Secret Management - -
+**Trustless operations** +- **Isolated keys**: Per-app keys derived in TEE. Survives hardware failure. Never exposed to operators. +- **Code governance**: Updates follow predefined rules (e.g., multi-party approval). Operators can't swap code or access secrets. -The environment variables will be encrypted in the client-side and decrypted in the CVM before being passed to the containers. +## Getting Started -### Access the App +**Try it now:** Chat with LLMs running in TEE at [chat.redpill.ai](https://chat.redpill.ai). Click the shield icon to verify attestations from Intel TDX and NVIDIA GPUs. -Once your app is deployed and listening on an HTTP port, you can access it through dstack-gateway's public domain using these ingress mapping rules: - -- `[-[][s|g]].` → maps to port `` in the CVM - -**Examples**: - -- `3327603e03f5bd1f830812ca4a789277fc31f577-8080.test0.dstack.org` → port `8080` (TLS termination to any TCP) -- `3327603e03f5bd1f830812ca4a789277fc31f577-8080g.test0.dstack.org` → port `8080` (TLS termination with HTTP/2 negotiation) -- `3327603e03f5bd1f830812ca4a789277fc31f577-8080s.test0.dstack.org` → port `8080` (TLS passthrough to any TCP) - -The `` can be either the app ID or instance ID. When using the app ID, the load balancer will select one of the available instances. Adding an `s` suffix enables TLS passthrough to the app instead of terminating at dstack-gateway. Adding a `g` suffix enables HTTPS/2 with TLS termination for gRPC applications. - -**Note**: If dstack-gateway is disabled, you'll need to use port mappings configured during deployment to access your application via the host's IP address and mapped ports. - -For development images (`dstack-x.x.x-dev`), you can SSH into the CVM for inspection: - -```bash -# Find the CVM wg IP address in the dstack-vmm dashboard -ssh root@10.0.3.2 -``` - -### Getting TDX Quote in Docker Container - -To get a TDX quote within app containers: - -**1. Mount the socket in `docker-compose.yaml`** +**Deploy your own:** ```yaml -version: '3' +# docker-compose.yaml services: - nginx: - image: nginx:latest - volumes: - - /var/run/dstack.sock:/var/run/dstack.sock + vllm: + image: vllm/vllm-openai:latest + runtime: nvidia + command: --model Qwen/Qwen2.5-7B-Instruct ports: - - "8080:80" - restart: always + - "8000:8000" ``` -**2. Execute the quote request command** +Deploy to any TDX host with the [`dstack-nvidia-0.5.x` base image](https://github.com/Dstack-TEE/meta-dstack/releases), or use [Phala Cloud](https://cloud.phala.network) for managed infrastructure. -```bash -# The argument report_data accepts binary data encoding in hex string. -# The actual report_data passing the to the underlying TDX driver is sha2_256(report_data). -curl --unix-socket /var/run/dstack.sock http://localhost/GetQuote?report_data=0x1234deadbeef | jq . -``` +Want to deploy a self hosted dstack? Check our [full deployment guide →](./docs/deployment.md) -### Container Logs +## Architecture -Container logs can be obtained from the CVM's `dashboard` page or by curl: +![Architecture](./docs/assets/arch.png) -```bash -curl 'http://.:9090/logs/?since=0&until=0&follow=true&text=true×tamps=true&bare=true' -``` +Your container runs inside a Confidential VM (Intel TDX) with optional GPU isolation via NVIDIA Confidential Computing. The CPU TEE protects application logic; the GPU TEE protects model weights and inference data. -Replace `` and `` with actual values. Available parameters: - -| Parameter | Description | -|-----------|-------------| -| `since=0` | Starting Unix timestamp for log retrieval | -| `until=0` | Ending Unix timestamp for log retrieval | -| `follow` | Enables continuous log streaming | -| `text` | Returns human-readable text instead of base64 encoding | -| `timestamps` | Adds timestamps to each log line | -| `bare` | Returns the raw log lines without json format | - -**Example response:** -```bash -$ curl 'http://0.0.0.0:9190/logs/zk-provider-server?text×tamps' -{"channel":"stdout","message":"2024-09-29T03:05:45.209507046Z Initializing Rust backend...\n"} -{"channel":"stdout","message":"2024-09-29T03:05:45.209543047Z Calling Rust function: init\n"} -{"channel":"stdout","message":"2024-09-29T03:05:45.209544957Z [2024-09-29T03:05:44Z INFO rust_prover] Initializing...\n"} -{"channel":"stdout","message":"2024-09-29T03:05:45.209546381Z [2024-09-29T03:05:44Z INFO rust_prover::groth16] Starting setup process\n"} -``` +**Core components:** -### TLS Passthrough with Custom Domain +- **Guest Agent**: Runs inside each CVM. Generates TDX attestation quotes so users can verify exactly what's running. Provisions per-app cryptographic keys from KMS. Encrypts local storage. Apps interact via `/var/run/dstack.sock`. -dstack-gateway supports TLS passthrough for custom domains. +- **KMS**: Runs in its own TEE. Verifies TDX quotes before releasing keys. Enforces authorization policies defined in on-chain smart contracts — operators cannot bypass these checks. Derives deterministic keys bound to each app's attested identity. -See the example [here](https://github.com/Dstack-TEE/dstack-examples/tree/main/custom-domain/dstack-ingress) for more details. +- **Gateway**: Terminates TLS at the edge and provisions ACME certificates automatically. Routes traffic to CVMs. All internal communication uses RA-TLS for mutual attestation. -### Upgrade an App +- **VMM**: Runs on bare-metal TDX hosts. Parses docker-compose files directly — no app changes needed. Boots CVMs from a reproducible OS image. Allocates CPU, memory, and confidential GPU resources. -Got to the dstack-vmm webpage, click the **[Upgrade]** button, select or paste the compose file you want to upgrade to, and click the **[Upgrade]** button again. The app id does not change after the upgrade. Stop and start the app to apply the upgrade. +[Full security model →](./docs/security/security-model.md) ---- +## SDKs -## 🔐 Advanced Features +Apps communicate with the guest agent via HTTP over `/var/run/dstack.sock`. Use the [HTTP API](./sdk/curl/api.md) directly with curl, or use a language SDK: -### Zero Trust HTTPS +| Language | Install | Docs | +|----------|---------|------| +| Python | `pip install dstack-sdk` | [README](./sdk/python/README.md) | +| TypeScript | `npm install @phala/dstack-sdk` | [README](./sdk/js/README.md) | +| Rust | `cargo add dstack-sdk` | [README](./sdk/rust/README.md) | +| Go | `go get github.com/Dstack-TEE/dstack/sdk/go` | [README](./sdk/go/README.md) | -In the tutorial above, we used a TLS certificate with a private key external to the TEE. To establish trust, we need to generate and maintain the certificate's private key within the TEE and provide evidence that all TLS certificates for the domain originated solely from dstack-gateway CVM. +## Documentation -By combining Certificate Transparency Logs and CAA DNS records, we can make the best effort to minimize security risks. Here's our approach: +**For Developers** +- [Confidential AI](./docs/confidential-ai.md) - Inference, agents, and training with hardware privacy +- [Usage Guide](./docs/usage.md) - Deploying and managing apps +- [Verification](./docs/verification.md) - How to verify TEE attestation -- Set CAA records to allow only the account created in dstack-gateway CVM to request Certificates -- Launch a program to monitor Certificate Transparency Log and give alarm once any certificate issued to a pubkey that isn't generated by dstack-gateway CVM +**For Operators** +- [Deployment](./docs/deployment.md) - Self-hosting on TDX hardware +- [On-Chain Governance](./docs/onchain-governance.md) - Smart contract authorization +- [Gateway](./docs/dstack-gateway.md) - Gateway configuration -#### Configurations +**Reference** +- [App Compose Format](./docs/normalized-app-compose.md) - Compose file specification +- [VMM CLI Guide](./docs/vmm-cli-user-guide.md) - Command-line reference +- [Design Decisions](./docs/design-and-hardening-decisions.md) - Architecture rationale +- [FAQ](./docs/faq.md) - Frequently asked questions -To launch Certbot, you need to own a domain hosted on Cloudflare. Obtain an API token with DNS operation permissions from the Cloudflare dashboard. Configure it in the `build-config.sh`: +## Security -```bash -# The directory to store the auto obtained TLS certificate and key -GATEWAY_CERT=${CERBOT_WORKDIR}/live/cert.pem -GATEWAY_KEY=${CERBOT_WORKDIR}/live/key.pem +- [Security Overview](./docs/security/) - Security documentation and responsible disclosure +- [Security Model](./docs/security/security-model.md) - Threat model and trust boundaries +- [Security Best Practices](./docs/security/security-best-practices.md) - Production hardening +- [Security Audit](./docs/security/dstack-audit.pdf) - Third-party audit by zkSecurity +- [CVM Boundaries](./docs/security/cvm-boundaries.md) - Information exchange and isolation -# For certbot -CF_API_TOKEN=g-DwMH... -# ACME_URL=https://acme-v02.api.letsencrypt.org/directory -ACME_URL=https://acme-staging-v02.api.letsencrypt.org/directory -``` +## FAQ -Then re-run the build script: +
+Why not use AWS Nitro / Azure Confidential VMs / GCP directly? -```bash -../build.sh -``` +You can — but you'll build everything yourself: attestation verification, key management, Docker orchestration, certificate provisioning, and governance. dstack provides all of this out of the box. -#### Launch Certbot - -Then run the certbot in the `build/` and you will see the following log: - -```text -$ RUST_LOG=info,certbot=debug ./certbot renew -c certbot.toml -2024-10-25T07:41:00.682990Z INFO certbot::bot: creating new ACME account -2024-10-25T07:41:00.869246Z INFO certbot::bot: created new ACME account: https://acme-staging-v02.api.letsencrypt.org/acme/acct/168601853 -... -2024-10-25T07:41:35.852790Z DEBUG certbot::acme_client: setting challenge ready for https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/14584884443/mQ-I2A -2024-10-25T07:41:35.934425Z DEBUG certbot::acme_client: challenges are ready, waiting for order to be ready -2024-10-25T07:41:37.972434Z DEBUG certbot::acme_client: order is ready, uploading csr -2024-10-25T07:41:38.052901Z DEBUG certbot::acme_client: order is processing, waiting for challenge to be accepted -2024-10-25T07:41:40.088190Z DEBUG certbot::acme_client: order is valid, getting certificate -2024-10-25T07:41:40.125988Z DEBUG certbot::acme_client: removing dns record 6ab5724e8fa7e3e8f14e93333a98866a -2024-10-25T07:41:40.377379Z DEBUG certbot::acme_client: stored new cert in /home/kvin/codes/meta-dstack/dstack/build/run/certbot/backup/2024-10-25T07:41:40.377174477Z -2024-10-25T07:41:40.377472Z INFO certbot::bot: checking if certificate needs to be renewed -2024-10-25T07:41:40.377719Z DEBUG certbot::acme_client: will expire in Duration { seconds: 7772486, nanoseconds: 622281542 } -2024-10-25T07:41:40.377752Z INFO certbot::bot: certificate /home/kvin/codes/meta-dstack/dstack/build/run/certbot/live/cert.pem is up to date -``` +| Approach | Docker native | GPU TEE | Key management | Attestation tooling | Open source | +|----------|:-------------:|:-------:|:--------------:|:-------------------:|:-----------:| +| **dstack** | ✓ | ✓ | ✓ | ✓ | ✓ | +| AWS Nitro Enclaves | - | - | Manual | Manual | - | +| Azure Confidential VMs | - | Preview | Manual | Manual | - | +| GCP Confidential Computing | - | - | Manual | Manual | - | -**What the command does:** +Cloud providers give you the hardware primitive. dstack gives you the full stack: reproducible OS images, automatic attestation, per-app key derivation, TLS certificates, and smart contract governance. No vendor lock-in. -- Registered to letsencrypt and got a new account `https://acme-staging-v02.api.letsencrypt.org/acme/acct/168601853` -- Auto set CAA records for the domain on cloudflare, you can open the CF dashboard to see the record: +
-
+
+How is this different from SGX/Gramine? -Certbot CAA +SGX requires porting applications to enclaves. dstack uses full-VM isolation (Intel TDX) — bring your Docker containers as-is. Plus GPU TEE support that SGX doesn't offer. -
+ -- Auto requested a new certificate from Let's Encrypt. Automatically renews the certificate to maintain its validity +
+What's the performance overhead? -#### Launch dstack-gateway +Minimal. Intel TDX adds ~2-5% overhead for CPU workloads. NVIDIA Confidential Computing has negligible impact on GPU inference. The main cost is memory encryption, which is hardware-accelerated on supported CPUs. -Execute dstack-gateway with `sudo ./dstack-gateway -c gateway.toml`, then access the web portal to check the dstack-gateway CVM managed Let's Encrypt account. The account's private key remains securely sealed within the TEE. +
-
+
+Is this production-ready? -Gateway Account ID +Yes. dstack powers production AI infrastructure at [OpenRouter](https://openrouter.ai/provider/phala) and [NEAR AI](https://x.com/ilblackdragon/status/1962920246148268235). The framework has been [audited by zkSecurity](./docs/security/dstack-audit.pdf) and is a Linux Foundation Confidential Computing Consortium project. -
+ -### Certificate Transparency Log Monitor +
+Can I run this on my own hardware? -To enhance security, we've limited TLS certificate issuance to dstack-gateway via CAA records. However, since these records can be modified through Cloudflare's domain management, we need to implement global CA certificate monitoring to maintain security oversight. +Yes. dstack runs on any Intel TDX-capable server. See the [deployment guide](./docs/deployment.md) for self-hosting instructions. You can also use [Phala Cloud](https://cloud.phala.network) for managed infrastructure. -`ct_monitor` tracks Certificate Transparency logs via https://crt.sh, comparing their public key with the ones got from dstack-gateway RPC. It immediately alerts when detecting unauthorized certificates not issued through dstack-gateway: +
-```text -$ ./ct_monitor -t https://localhost:9010/prpc -d -2024-10-25T08:12:11.366463Z INFO ct_monitor: monitoring ... -2024-10-25T08:12:11.366488Z INFO ct_monitor: fetching known public keys from https://localhost:9010/prpc -2024-10-25T08:12:11.566222Z INFO ct_monitor: got 2 known public keys -2024-10-25T08:12:13.142122Z INFO ct_monitor: ✅ checked log id=14705660685 -2024-10-25T08:12:13.802573Z INFO ct_monitor: ✅ checked log id=14705656674 -2024-10-25T08:12:14.494944Z ERROR ct_monitor: ❌ error in CTLog { id: 14666084839, issuer_ca_id: 295815, issuer_name: "C=US, O=Let's Encrypt, CN=R11", common_name: "", name_value: "*.", not_before: "2024-09-24T02:23:15", not_after: "2024-12-23T02:23:14", serial_number: "03ae796f56a933c8ff7e32c7c0d662a253d4", result_count: 1, entry_timestamp: "2024-09-24T03:21:45.825" } -2024-10-25T08:12:14.494998Z ERROR ct_monitor: error: certificate has issued to unknown pubkey: 30820122300d06092a864886f70d01010105000382010f003082010a02820101009de65c767caf117880626d1acc1ee78f3c6a992e3fe458f34066f92812ac550190a67e49ebf4f537003c393c000a8ec3e114da088c0cb02ffd0881fd39a2b32cc60d2e9989f0efab3345bee418262e0179d307d8d361fd0837f85d17eab92ec6f4126247e614aa01f4efcc05bc6303a8be68230f04326c9e85406fc4d234e9ce92089253b11d002cdf325582df45d5da42981cd546cbd2e9e49f0fa6636e747a345aaf8cefa02556aa258e1f7f90906be8fe51567ac9626f35bc46837e4f3203387fee59c71cea400000007c24e7537debc1941b36ff1612990233e4c219632e35858b1771f17a71944adf6c657dd7303583e3aeed199bd36a3152f49980f4f30203010001 -``` +
+What TEE hardware is supported? ---- +Currently: Intel TDX (4th/5th Gen Xeon) and NVIDIA Confidential Computing (H100, Blackwell). AMD SEV-SNP support is planned. -## 🔒 Security +
-dstack undergoes regular security audits to ensure the highest standards of security for TEE-based deployments. +
+How do users verify my deployment? -### Security Audit Report +Your app exposes attestation quotes via the SDK. Users verify these quotes using [dstack-verifier](https://github.com/Dstack-TEE/dstack/tree/master/verifier), [dcap-qvl](https://github.com/Phala-Network/dcap-qvl), or the [Trust Center](https://trust.phala.com). See the [verification guide](./docs/verification.md) for details. -A comprehensive security audit was conducted to evaluate the security architecture and implementation of dstack. The audit report is available at: +
-📄 **[Security Audit Report](./docs/security/dstack-audit.pdf)** +## Trusted by -The audit covers key security aspects including: -- TEE integration and attestation mechanisms -- Cryptographic implementations and key management -- Network security and TLS configurations -- Container isolation and runtime security -- Smart contract security (for blockchain integrations) +- [OpenRouter](https://openrouter.ai/provider/phala) - Confidential AI inference providers powered by dstack +- [NEAR AI](https://x.com/ilblackdragon/status/1962920246148268235) - Private AI infrastructure powered by dstack -For additional security guidance and best practices, see the [Security Guide](./docs/security-guide/security-guide.md). +dstack is a Linux Foundation [Confidential Computing Consortium](https://confidentialcomputing.io/2025/10/02/welcoming-phala-to-the-confidential-computing-consortium/) open source project. ---- - -## 🔧 Troubleshooting - -### Error: qemu-system-x86_64: vhost-vsock: unable to set guest cid: Address already in use - -`dstack-vmm` may throw this error when creating a new VM if the [Unix Socket CID](https://man7.org/linux/man-pages/man7/vsock.7.html) is occupied. - -**Solution:** - -1. **List the occupied CID:** - ```bash - ps aux | grep 'guest-cid=' - ``` - -2. **Choose a new range** of the CID not conflicting with the CID in use. You can change `build/vmm.toml` file and restart `dstack-vmm`. For example, you may find 33000-34000 free to use: - ```toml - [cvm] - cid_start = 33000 - cid_pool_size = 1000 - ``` - -3. **When building from scratch**, change the CID configs in `build-config.sh` instead, because `vmm.toml` file is generated by `build.sh`. - -> [!NOTE] -> You may encounter this problem when upgrading from an older version of dstack, because CID was introduced in `build-config.sh` in later versions. In such case, please follow the docs to add the missing entries in `build-config.sh` and rebuild dstack. - -### Error: Operation not permitted when building guest image - -When running `../build.sh guest`, you might encounter this error: - -``` -Traceback (most recent call last): - File "/meta-dstack/poky/bitbake/bin/bitbake-worker", line 278, in child - bb.utils.disable_network(uid, gid) - File "/meta-dstack/poky/bitbake/lib/bb/utils.py", line 1696, in disable_network - with open("/proc/self/uid_map", "w") as f: -PermissionError: [Errno 1] Operation not permitted -``` - -**Solution:** - -This error occurs because Ubuntu 23.10 and later versions restrict unprivileged user namespaces by default. +## Community -```bash -sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 -``` - -Then try building again. For more information about this restriction, see the [Ubuntu discourse post](https://discourse.ubuntu.com/t/spec-unprivileged-user-namespace-restrictions-via-apparmor-in-ubuntu-23-10/37626). - ---- - -## 🎨 Media Kit - -The dstack logo and branding assets are available in the [media kit](./docs/assets/Dstack%20Logo%20Kit/): - -- **Horizontal logos**: Primary and dark versions in PNG/SVG formats -- **Vertical logos**: Primary and dark versions in PNG/SVG formats -- **Icons**: Primary and dark versions in PNG/SVG formats - -All assets are provided in high-resolution formats suitable for both digital and print use. +[Telegram](https://t.me/+UO4bS4jflr45YmUx) · [GitHub Discussions](https://github.com/Dstack-TEE/dstack/discussions) · [Examples](https://github.com/Dstack-TEE/dstack-examples) ---- - -## 📄 License - -``` -Copyright 2024 Phala Network and Contributors. +[![Repobeats](https://repobeats.axiom.co/api/embed/0a001cc3c1f387fae08172a9e116b0ec367b8971.svg)](https://github.com/Dstack-TEE/dstack/pulse) -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +## Cite - http://www.apache.org/licenses/LICENSE-2.0 +If you use dstack in your research, please cite: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +```bibtex +@article{zhou2025dstack, + title={Dstack: A Zero Trust Framework for Confidential Containers}, + author={Zhou, Shunfan and Wang, Kevin and Yin, Hang}, + journal={arXiv preprint arXiv:2509.11555}, + year={2025} +} ``` ---- - -
+## Media Kit -**[⬆ Back to top](#-Overview)** +Logo and branding assets: [dstack-logo-kit](./docs/assets/dstack-logo-kit/) -Made with ❤️ by the dstack community +## License -
+Apache 2.0 diff --git a/REUSE.toml b/REUSE.toml index 8f345c27..1b6dff22 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -27,6 +27,8 @@ path = [ "**/package-lock.json", "kms/auth-eth/.openzeppelin/unknown-2035.json", "kms/auth-mock/.oxlintrc.json", + "kms/auth-simple/.oxlintrc.json", + "kms/auth-simple/auth-config.example.json", "sdk/simulator/*.json", "sdk/go/go.sum", "kms/dstack-app/builder/shared/kms-pinned-packages.txt", @@ -84,6 +86,14 @@ path = [ SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" SPDX-License-Identifier = "Apache-2.0" +# Scripts with SPDX-like content (false positive prevention) + +[[annotations]] +path = "scripts/add-spdx-attribution.py" +SPDX-FileCopyrightText = "© 2025 Phala Network " +SPDX-License-Identifier = "Apache-2.0" +precedence = "override" + # Vendor code [[annotations]] @@ -96,11 +106,13 @@ precedence = "override" path = "kms/auth-eth/lib/openzeppelin-foundry-upgrades/**" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "MIT" +precedence = "override" [[annotations]] path = "kms/auth-eth/lib/forge-std/**" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "Apache-2.0" +precedence = "override" [[annotations]] path = "tdx-attest-sys/csrc/*" diff --git a/ct_monitor/Cargo.toml b/ct_monitor/Cargo.toml index 3828526e..5e7dcc96 100644 --- a/ct_monitor/Cargo.toml +++ b/ct_monitor/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } +hex = { workspace = true, features = ["alloc", "std"] } hex_fmt.workspace = true regex.workspace = true reqwest = { workspace = true, default-features = false, features = ["json", "rustls-tls", "charset", "hickory-dns"] } @@ -21,6 +22,3 @@ tokio = { workspace = true, features = ["full"] } tracing.workspace = true tracing-subscriber.workspace = true x509-parser.workspace = true - -dstack-gateway-rpc.workspace = true -ra-rpc = { workspace = true, default-features = false, features = ["client"] } diff --git a/ct_monitor/src/main.rs b/ct_monitor/src/main.rs index 6a168cce..4622e9f4 100644 --- a/ct_monitor/src/main.rs +++ b/ct_monitor/src/main.rs @@ -4,8 +4,6 @@ use anyhow::{bail, Context, Result}; use clap::Parser; -use dstack_gateway_rpc::gateway_client::GatewayClient; -use ra_rpc::client::RaClient; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -15,6 +13,13 @@ use x509_parser::prelude::*; const BASE_URL: &str = "https://crt.sh"; +#[derive(Debug, Deserialize)] +struct AcmeInfoResponse { + #[allow(dead_code)] + account_uri: String, + hist_keys: Vec, +} + struct Monitor { gateway_uri: String, domain: String, @@ -48,12 +53,35 @@ impl Monitor { } async fn refresh_known_keys(&mut self) -> Result<()> { - info!("fetching known public keys from {}", self.gateway_uri); - // TODO: Use RA-TLS - let tls_no_check = true; - let rpc = GatewayClient::new(RaClient::new(self.gateway_uri.clone(), tls_no_check)?); - let info = rpc.acme_info().await?; - self.known_keys = info.hist_keys.into_iter().collect(); + let acme_info_url = format!("{}/acme-info", self.gateway_uri.trim_end_matches('/')); + info!("fetching known public keys from {}", acme_info_url); + + let client = reqwest::Client::new(); + + let response = client + .get(&acme_info_url) + .send() + .await + .context("failed to fetch acme-info")?; + + if !response.status().is_success() { + bail!( + "failed to fetch acme-info: HTTP {}", + response.status().as_u16() + ); + } + + let info: AcmeInfoResponse = response + .json() + .await + .context("failed to parse acme-info response")?; + + self.known_keys = info + .hist_keys + .into_iter() + .map(|hex_key| hex::decode(&hex_key).context("invalid hex in hist_keys")) + .collect::>>()?; + info!("got {} known public keys", self.known_keys.len()); for key in self.known_keys.iter() { debug!(" {}", hex_fmt::HexFmt(key)); diff --git a/docs/assets/Dstack Logo Kit/01 Horizontal/01 Dstack _Horizontal_primary.png b/docs/assets/dstack-logo-kit/01 Horizontal/01 Dstack _Horizontal_primary.png similarity index 100% rename from docs/assets/Dstack Logo Kit/01 Horizontal/01 Dstack _Horizontal_primary.png rename to docs/assets/dstack-logo-kit/01 Horizontal/01 Dstack _Horizontal_primary.png diff --git a/docs/assets/Dstack Logo Kit/01 Horizontal/01 Dstack _Horizontal_primary.svg b/docs/assets/dstack-logo-kit/01 Horizontal/01 Dstack _Horizontal_primary.svg similarity index 100% rename from docs/assets/Dstack Logo Kit/01 Horizontal/01 Dstack _Horizontal_primary.svg rename to docs/assets/dstack-logo-kit/01 Horizontal/01 Dstack _Horizontal_primary.svg diff --git a/docs/assets/Dstack Logo Kit/01 Horizontal/02 Dstack _Horizontal_dark.png b/docs/assets/dstack-logo-kit/01 Horizontal/02 Dstack _Horizontal_dark.png similarity index 100% rename from docs/assets/Dstack Logo Kit/01 Horizontal/02 Dstack _Horizontal_dark.png rename to docs/assets/dstack-logo-kit/01 Horizontal/02 Dstack _Horizontal_dark.png diff --git a/docs/assets/Dstack Logo Kit/01 Horizontal/02 Dstack _Horizontal_dark.svg b/docs/assets/dstack-logo-kit/01 Horizontal/02 Dstack _Horizontal_dark.svg similarity index 100% rename from docs/assets/Dstack Logo Kit/01 Horizontal/02 Dstack _Horizontal_dark.svg rename to docs/assets/dstack-logo-kit/01 Horizontal/02 Dstack _Horizontal_dark.svg diff --git a/docs/assets/Dstack Logo Kit/02 Vertical/01 Dstack_Vertical_primary.png b/docs/assets/dstack-logo-kit/02 Vertical/01 Dstack_Vertical_primary.png similarity index 100% rename from docs/assets/Dstack Logo Kit/02 Vertical/01 Dstack_Vertical_primary.png rename to docs/assets/dstack-logo-kit/02 Vertical/01 Dstack_Vertical_primary.png diff --git a/docs/assets/Dstack Logo Kit/02 Vertical/01 Dstack_Vertical_primary.svg b/docs/assets/dstack-logo-kit/02 Vertical/01 Dstack_Vertical_primary.svg similarity index 100% rename from docs/assets/Dstack Logo Kit/02 Vertical/01 Dstack_Vertical_primary.svg rename to docs/assets/dstack-logo-kit/02 Vertical/01 Dstack_Vertical_primary.svg diff --git a/docs/assets/Dstack Logo Kit/02 Vertical/02 Dstack_Vertical_dark.png b/docs/assets/dstack-logo-kit/02 Vertical/02 Dstack_Vertical_dark.png similarity index 100% rename from docs/assets/Dstack Logo Kit/02 Vertical/02 Dstack_Vertical_dark.png rename to docs/assets/dstack-logo-kit/02 Vertical/02 Dstack_Vertical_dark.png diff --git a/docs/assets/Dstack Logo Kit/02 Vertical/02 Dstack_Vertical_dark.svg b/docs/assets/dstack-logo-kit/02 Vertical/02 Dstack_Vertical_dark.svg similarity index 100% rename from docs/assets/Dstack Logo Kit/02 Vertical/02 Dstack_Vertical_dark.svg rename to docs/assets/dstack-logo-kit/02 Vertical/02 Dstack_Vertical_dark.svg diff --git a/docs/assets/Dstack Logo Kit/03 icon/01 Dstack icon_primary.png b/docs/assets/dstack-logo-kit/03 icon/01 Dstack icon_primary.png similarity index 100% rename from docs/assets/Dstack Logo Kit/03 icon/01 Dstack icon_primary.png rename to docs/assets/dstack-logo-kit/03 icon/01 Dstack icon_primary.png diff --git a/docs/assets/Dstack Logo Kit/03 icon/01 Dstack icon_primary.svg b/docs/assets/dstack-logo-kit/03 icon/01 Dstack icon_primary.svg similarity index 100% rename from docs/assets/Dstack Logo Kit/03 icon/01 Dstack icon_primary.svg rename to docs/assets/dstack-logo-kit/03 icon/01 Dstack icon_primary.svg diff --git a/docs/assets/Dstack Logo Kit/03 icon/02 Dstack icon_dark.png b/docs/assets/dstack-logo-kit/03 icon/02 Dstack icon_dark.png similarity index 100% rename from docs/assets/Dstack Logo Kit/03 icon/02 Dstack icon_dark.png rename to docs/assets/dstack-logo-kit/03 icon/02 Dstack icon_dark.png diff --git a/docs/assets/Dstack Logo Kit/03 icon/02 Dstack icon_dark.svg b/docs/assets/dstack-logo-kit/03 icon/02 Dstack icon_dark.svg similarity index 100% rename from docs/assets/Dstack Logo Kit/03 icon/02 Dstack icon_dark.svg rename to docs/assets/dstack-logo-kit/03 icon/02 Dstack icon_dark.svg diff --git a/docs/security-guide/assets/prelaunch-script.png b/docs/assets/prelaunch-script.png similarity index 100% rename from docs/security-guide/assets/prelaunch-script.png rename to docs/assets/prelaunch-script.png diff --git a/docs/security-guide/assets/token-env.png b/docs/assets/token-env.png similarity index 100% rename from docs/security-guide/assets/token-env.png rename to docs/assets/token-env.png diff --git a/docs/auth-simple-operations.md b/docs/auth-simple-operations.md new file mode 100644 index 00000000..8b253b9c --- /dev/null +++ b/docs/auth-simple-operations.md @@ -0,0 +1,298 @@ +# auth-simple Operations Guide + +This guide covers day-to-day operations for managing apps and devices with auth-simple. + +For initial deployment setup, see [Deployment Guide](./deployment.md). + +## Overview + +auth-simple uses a JSON config file to whitelist: +- **OS images** - Which guest OS versions can boot +- **KMS nodes** - Which KMS instances can onboard (mrAggregated, devices) +- **Apps** - Which applications can boot (appId, composeHash, devices) + +The config is re-read on each request, so changes take effect immediately without restart. + +--- + +## Config File Structure + +```json +{ + "osImages": ["0x..."], + "gatewayAppId": "0x...", + "kms": { + "mrAggregated": ["0x..."], + "devices": ["0x..."], + "allowAnyDevice": true + }, + "apps": { + "0x": { + "composeHashes": ["0x..."], + "devices": ["0x..."], + "allowAnyDevice": true + } + } +} +``` + +> **Note:** Only `osImages` is required. Add `gatewayAppId` after deploying the Gateway. Add `apps` entries as you deploy applications. + +--- + +## Adding an App + +### Step 1: Generate App ID + +App IDs are typically the contract address (for on-chain) or a unique identifier you choose. + +For auth-simple, you can use any unique hex string (40 characters / 20 bytes): + +```bash +# Generate a random app ID +openssl rand -hex 20 +# Output: 7a3b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b +``` + +### Step 2: Get Compose Hash + +The compose hash is computed from the normalized docker-compose file. The VMM displays it when deploying: + +``` +Docker compose file: +... +Compose hash: 0x700a50336df7c07c82457b116e144f526c29f6d8... +``` + +Or query from a running CVM: + +```bash +curl -s --unix-socket /var/run/dstack.sock http://localhost/Info | \ + jq -r '"0x" + (.tcb_info | fromjson | .compose_hash)' +``` + +> **Note:** The VMM normalizes YAML to JSON before hashing. For exact hash, use the value shown during deployment. + +### Step 3: Add to Config + +Edit your `auth-config.json`: + +```json +{ + "apps": { + "0x7a3b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b": { + "composeHashes": [ + "0x700a50336df7c07c82457b116e144f526c29f6d8..." + ], + "devices": [], + "allowAnyDevice": true + } + } +} +``` + +### Step 4: Deploy via VMM + +Use the App ID when deploying through VMM: + +```bash +./vmm-cli.py deploy \ + --app-id 7a3b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b \ + --compose docker-compose.yaml \ + ... +``` + +--- + +## Updating an App + +When you change your docker-compose.yaml, a new compose hash is generated. + +### Add New Hash (Keep Old Running) + +Add the new hash to the `composeHashes` array: + +```json +{ + "apps": { + "0x": { + "composeHashes": [ + "0x", + "0x" + ] + } + } +} +``` + +### Replace Hash (Force Upgrade) + +Remove the old hash to prevent old versions from booting: + +```json +{ + "apps": { + "0x": { + "composeHashes": [ + "0x" + ] + } + } +} +``` + +--- + +## Device Management + +Devices are identified by their TDX device ID (hardware-specific). + +### Get Device ID + +The `deviceId` is sent by the booting app/KMS in its auth request. Check auth-simple logs: + +``` +app boot auth request: { appId: '0x...', deviceId: '0x...', ... } +``` + +> **Tip:** Use `allowAnyDevice: true` initially, then restrict to specific devices after capturing IDs from logs. + +### Restrict App to Specific Devices + +```json +{ + "apps": { + "0x": { + "composeHashes": ["0x..."], + "devices": [ + "0xe5a0c70bb6503de2d31c11d85914fe3776ed5b33a078ed856327c371a60fe0fd" + ], + "allowAnyDevice": false + } + } +} +``` + +### Allow Any Device + +For initial testing or when device restriction isn't needed: + +```json +{ + "apps": { + "0x": { + "allowAnyDevice": true + } + } +} +``` + +--- + +## Removing an App + +Delete the app entry from the `apps` object: + +```json +{ + "apps": { + // "0x": { ... } <- deleted + } +} +``` + +Running instances will continue until restarted. New boot requests will be rejected. + +--- + +## Setting the Gateway App (After Gateway Deployment) + +The `gatewayAppId` field is **optional** during initial KMS deployment. Add it after deploying the Gateway. + +The Gateway is a special app that routes traffic to other apps. Once deployed, add its App ID to your config: + +```json +{ + "gatewayAppId": "0x75537828f2ce51be7289709686A69CbFDbB714F1", + "apps": { + "0x75537828f2ce51be7289709686A69CbFDbB714F1": { + "composeHashes": ["0x..."], + "allowAnyDevice": true + } + } +} +``` + +The `gatewayAppId` is returned in boot responses and used by KMS for key derivation. + +--- + +## KMS Onboarding (Multi-Node) + +To allow additional KMS nodes to onboard (receive root keys from the primary KMS), whitelist their `mrAggregated` value. + +### Get mrAggregated + +The `mrAggregated` is sent by the booting KMS in its auth request. To get this value: + +1. **From auth-simple logs**: When a KMS boots, auth-simple logs the mrAggregated: + ``` + KMS boot auth request: { osImageHash: '0x...', mrAggregated: '0x...', ... } + ``` + +2. **Initial setup**: Leave `kms.mrAggregated` empty for the first KMS (empty array allows any). After it boots, check the logs and add the value. + +### Add to Config + +```json +{ + "kms": { + "mrAggregated": ["0x"], + "allowAnyDevice": true + } +} +``` + +> **Note:** All KMS nodes using the same OS image and compose will have the same mrAggregated, so you only need to capture it once. + +--- + +## Verification + +### Check Config is Valid + +```bash +curl -s http://localhost:3001/ | jq +``` + +Returns current config status including `gatewayAppId`. + +### Test Boot Authorization + +```bash +# Test app boot +curl -s -X POST http://localhost:3001/bootAuth/app \ + -H "Content-Type: application/json" \ + -d '{ + "appId": "0x", + "composeHash": "0x", + "osImageHash": "0x", + "deviceId": "0x", + "mrAggregated": "0x...", + "instanceId": "0x...", + "tcbStatus": "UpToDate" + }' | jq +``` + +Expected responses: +- `{"isAllowed": true, ...}` - App is authorized +- `{"isAllowed": false, "reason": "app not registered", ...}` - App ID not in config +- `{"isAllowed": false, "reason": "compose hash not allowed", ...}` - Hash not whitelisted + +--- + +## See Also + +- [Deployment Guide](./deployment.md) - Initial setup +- [auth-simple README](../kms/auth-simple/README.md) - Developer reference +- [On-Chain Governance](./onchain-governance.md) - Smart contract-based alternative diff --git a/docs/confidential-ai.md b/docs/confidential-ai.md new file mode 100644 index 00000000..8d4e2f8a --- /dev/null +++ b/docs/confidential-ai.md @@ -0,0 +1,259 @@ +# Confidential AI + +Run AI workloads where the infrastructure operator can't see your data. dstack uses Intel TDX and NVIDIA Confidential Computing to encrypt everything in memory—your prompts, model weights, and intermediate computations stay private. + +## What You Can Build + +- **Private inference** - Users verify their prompts never leave encrypted memory +- **Training on sensitive data** - Fine-tune models without exposing training data to operators +- **Trustworthy agents** - Prove your agent code can't exfiltrate user data + +The key difference from self-hosting: users don't have to trust you. They can cryptographically verify what code runs and that the hardware is genuine. + +## What Makes It Confidential + +Four things need to be true for the system to be actually confidential: + +**TLS terminates inside the VM.** Your HTTPS connection ends inside the Confidential VM, not at some load balancer outside. The operator never sees plaintext traffic. + +**CPU memory is encrypted.** Intel TDX encrypts all RAM with hardware keys. The hypervisor can't read it, the host OS can't read it, even physical access to the DIMM won't help. + +**GPU memory is encrypted.** On H100/H200/Blackwell with NVIDIA CC mode, GPU memory is encrypted too. Model weights and activations stay protected during inference and training. + +**Disk is encrypted.** Anything written to storage uses keys derived from the TEE. The storage backend only sees ciphertext. + +When all four are in place, data stays encrypted from network ingress to egress. + +## Private Inference + +Deploy vLLM behind a signing proxy. Users can verify responses came from your TEE, not a compromised server. + +```yaml +services: + vllm: + image: vllm/vllm-openai:latest + command: --model Qwen/Qwen2.5-7B-Instruct --host 0.0.0.0 + deploy: + resources: + reservations: + devices: + - driver: nvidia + capabilities: [gpu] + + proxy: + image: phalanetwork/vllm-proxy:latest + ports: + - "8000:8000" + volumes: + - /var/run/dstack.sock:/var/run/dstack.sock + environment: + - VLLM_BASE_URL=http://vllm:8000 + - MODEL_NAME=Qwen/Qwen2.5-7B-Instruct + - TOKEN=${TOKEN} +``` + +The data flow looks like this: + +```mermaid +graph LR + User -->|TLS| Proxy + subgraph TEE[Confidential VM] + Proxy[vllm-proxy] + Proxy -->|HTTP| vLLM + vLLM --> GPU[GPU Memory] + Proxy -.->|signs with| Key[TEE-derived key] + end + Proxy -->|Response + Signature| User +``` + +The proxy terminates TLS, forwards to vLLM, and signs responses with a key derived from the TEE's identity. That signature proves the response came from this specific environment. + +Deploy it: + +```bash +phala deploy -n my-llm -c docker-compose.yaml \ + --instance-type h200.small \ + -e TOKEN=your-secret +``` + +Verify a response signature: + +```python +import requests + +sig = requests.get( + f"https://your-endpoint/v1/signature/{chat_id}", + headers={"Authorization": "Bearer your-token"} +).json() + +# sig contains signing_address, response_hash, signature +``` + +Try it live at [chat.redpill.ai](https://chat.redpill.ai). Full example: [dstack-examples/ai/inference](https://github.com/Dstack-TEE/dstack-examples/tree/main/ai/inference). + +## Confidential Training + +Fine-tune on sensitive data without exposing it to the operator. The training data and resulting weights stay in encrypted memory. + +```yaml +services: + trainer: + image: unsloth/unsloth:latest + command: python train.py --model meta-llama/Llama-3.2-3B --data /data/dataset.jsonl + volumes: + - /var/run/dstack.sock:/var/run/dstack.sock + - training-data:/data + environment: + - HF_TOKEN=${HF_TOKEN} + - WANDB_API_KEY=${WANDB_API_KEY} + deploy: + resources: + reservations: + devices: + - driver: nvidia + capabilities: [gpu] + +volumes: + training-data: +``` + +With NVIDIA CC on H100/H200, GPU memory is encrypted. Weights, gradients, and activations stay confidential throughout training. + +```mermaid +graph TB + subgraph TEE[Confidential VM] + Data[Training Data] --> Trainer + Trainer --> GPU[Confidential GPU Memory] + GPU --> Weights[Model Weights] + end + External[Data Provider] -->|encrypted upload| Data + Weights -->|encrypted download| External +``` + +Before a data provider shares sensitive data, they should: + +1. Review your docker-compose.yaml—check for unexpected network access or volume mounts +2. Get the attestation quote and verify the compose hash matches +3. Confirm it's running on genuine TDX + NVIDIA CC hardware + +The compose hash is a hash of your docker-compose.yaml. It's included in the attestation quote, so data providers can verify exactly what code will process their data. + +```python +from dstack_sdk import DstackClient + +client = DstackClient() +info = client.info() + +print(f"Compose hash: {info.compose_hash}") +# Data provider compares this against the docker-compose they reviewed +``` + +Full example: [dstack-examples/ai/training](https://github.com/Dstack-TEE/dstack-examples/tree/main/ai/training). + +## Trustworthy AI Agents + +Agents do complex things with user data—RAG lookups, database queries, LLM calls. Users want guarantees: their data won't be logged, won't be used for training, won't be exfiltrated. + +For true end-to-end privacy, the agent should call a confidential LLM endpoint (like the vllm-proxy from the inference section) instead of a third-party API. This keeps user prompts encrypted the entire way. + +```yaml +services: + agent: + image: your-agent:latest + volumes: + - /var/run/dstack.sock:/var/run/dstack.sock + environment: + # Point to a confidential LLM endpoint, not OpenAI + - LLM_BASE_URL=https://api.redpill.ai/v1 + - LLM_API_KEY=${LLM_API_KEY} + - DATABASE_URL=${DATABASE_URL} + ports: + - "8080:8080" +``` + +The architecture looks like this: + +```mermaid +graph TB + User -->|TLS| Agent + subgraph TEE1[Agent CVM] + Agent[Agent Code] + Agent --> RAG[RAG Pipeline] + Agent --> DB[(Database)] + end + Agent -->|TLS| LLM + subgraph TEE2[LLM CVM] + LLM[vllm-proxy + vLLM] + end + Agent -->|response| User + Operator -.->|cannot access| TEE1 + Operator -.->|cannot access| TEE2 +``` + +Both the agent and the LLM run in separate TEEs. User queries stay encrypted from browser to agent to LLM and back. The operator sees nothing. + +**Data stays private.** RAG retrievals, database queries, LLM calls, and agent state all run in encrypted memory. Using a confidential LLM means prompts never leave the encrypted environment. + +**No hidden behavior.** The compose hash in the attestation proves what code is running. Users can audit the code and verify it matches—no secret logging, no unauthorized data collection. + +**Provable constraints.** Want to prove your agent doesn't send user data to training pipelines? The auditable code + attestation makes that verifiable. + +For agents that need persistent keys (signing, encryption), derive them from the TEE: + +```python +from dstack_sdk import DstackClient + +client = DstackClient() +key = client.get_key('agent/signing-key') +# Same deployment = same key, but key never leaves TEE +``` + +Full example: [dstack-examples/ai/agents](https://github.com/Dstack-TEE/dstack-examples/tree/main/ai/agents). + +## Verification + +Attestation proves the hardware is genuine and shows what code is running. But it doesn't automatically prove the code is safe—you need to audit it yourself. + +```mermaid +graph LR + App[Your App] -->|quote| Verifier + Verifier -->|valid + compose_hash| You + You -->|compare| Compose[docker-compose.yaml] + Compose -->|audit| Code[Source Code] +``` + +The verification flow: + +```python +import requests + +# Get attestation with a nonce to prevent replay +attestation = requests.get( + "https://your-app/attestation", + params={"nonce": "random-challenge"} +).json() + +# Compare compose hash against what you reviewed +expected_hash = "sha256:abc123..." # From your audited docker-compose +assert attestation["compose_hash"] == expected_hash +``` + +For visual verification, paste the quote into [proof.t16z.com](https://proof.t16z.com), Phala's TEE attestation explorer that parses TDX quotes and displays the verification status, measurements, and TCB info in a readable format. + +The compose hash only tells you *what* is running. You still need to verify that code does what you expect—no secret logging, no data exfiltration, proper access controls. + +## Performance + +GPU inference runs at 99% of bare-metal speed on H100/H200. The memory encryption happens in hardware, so the overhead is minimal. Larger models actually perform better in TEE—the encryption cost is fixed while compute scales with model size. + +See the [full benchmark report](https://docs.phala.network/dstack/phala-cloud/references/performance-report) for CPU and storage numbers. + +## Getting Started + +1. Try [chat.redpill.ai](https://chat.redpill.ai) to see private inference in action +2. Deploy your own with the [inference example](https://github.com/Dstack-TEE/dstack-examples/tree/main/ai/inference) +3. Read the [Security Model](./security/security-model.md) for the full threat model + +## Production + +redpill.ai and NEAR AI run confidential AI on dstack in production. diff --git a/docs/deployment.md b/docs/deployment.md index e8466b52..463e1aeb 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -1,35 +1,145 @@ -# Deployment of dstack +# Deploying dstack -This document describes the deployment of dstack components on bare metal TDX hosts. -It contains steps to deploy dstack-kms and dstack-gateway into CVMs. +This guide covers deploying dstack on bare metal TDX hosts. + +## Overview + +dstack can be deployed in two ways: + +- **Dev Deployment**: All components run directly on the host. For local development and testing only - no security guarantees. +- **Production Deployment**: KMS and Gateway run as CVMs with hardware-rooted security. Uses auth server for authorization and OS image whitelisting. Required for any deployment where security matters. ## Prerequisites -- Follow the [TDX setup guide](https://github.com/canonical/tdx) to setup the TDX host. -- Install `cargo` and `rustc` +**Hardware:** +- Bare metal TDX server ([setup guide](https://github.com/canonical/tdx)) +- At least 16GB RAM, 100GB free disk space +- Public IPv4 address +- Optional: NVIDIA H100 or Blackwell GPU for [Confidential Computing](https://www.nvidia.com/en-us/data-center/solutions/confidential-computing/) workloads + +**Network:** +- Domain with DNS access (for Gateway TLS) + +> **Note:** See [Hardware Requirements](https://docs.phala.network/dstack/hardware-requirements) for server recommendations. + +--- + +## Dev Deployment + +This approach runs all components directly on the host for local development and testing. + +> **Warning:** Dev deployment uses KMS in dev mode with no security guarantees. Do NOT use for production. + +### Install Dependencies -## Clone the dstack repository ```bash -git clone https://github.com/Dstack-TEE/dstack +# Ubuntu 24.04 +sudo apt install -y build-essential chrpath diffstat lz4 wireguard-tools xorriso + +# Install Rust +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -## Compile and Run dstack-vmm + +### Build Configuration + ```bash +git clone https://github.com/Dstack-TEE/meta-dstack.git --recursive +cd meta-dstack/ +mkdir build && cd build +../build.sh hostcfg +``` + +Edit the generated `build-config.sh` for your environment. The minimal required changes are: + +| Variable | Description | +|----------|-------------| +| `KMS_DOMAIN` | DNS domain for KMS RPC (e.g., `kms.example.com`) | +| `GATEWAY_DOMAIN` | DNS domain for Gateway RPC (e.g., `gateway.example.com`) | +| `GATEWAY_PUBLIC_DOMAIN` | Public base domain for app routing (e.g., `apps.example.com`) | + +**TLS Certificates:** + +The Gateway requires TLS certificates. Configure Certbot with Cloudflare: + +```bash +CERTBOT_ENABLED=true +CF_API_TOKEN= +``` + +The certificates will be obtained automatically via ACME DNS-01 challenge. The KMS auto-generates its own certificates during bootstrap. + +Other variables like ports and CID pool settings have sensible defaults. + +```bash +vim ./build-config.sh +../build.sh hostcfg +``` + +### Download Guest Image + +```bash +../build.sh dl 0.5.5 +``` + +### Run Components + +Start in separate terminals: + +1. **KMS**: `./dstack-kms -c kms.toml` +2. **Gateway**: `sudo ./dstack-gateway -c gateway.toml` +3. **VMM**: `./dstack-vmm -c vmm.toml` + +> **Note:** This deployment uses KMS in dev mode without an auth server. For production deployments with proper security, see [Production Deployment](#production-deployment) below. + +--- + +## Production Deployment + +For production, deploy KMS and Gateway as CVMs with hardware-rooted security. Production deployments require: +- KMS running in a CVM (not on the host) +- Auth server for authorization (webhook mode) + +### Production Checklist + +**Required:** + +1. Set up TDX host with dstack-vmm +2. Deploy KMS as CVM (with auth server) +3. Deploy Gateway as CVM + +**Optional Add-ons:** + +4. [Zero Trust HTTPS](#4-zero-trust-https-optional) +5. [Certificate Transparency monitoring](#5-certificate-transparency-monitoring-optional) +6. [Multi-node deployment](#6-multi-node-deployment-optional) +7. [On-chain governance](./onchain-governance.md) - Smart contract-based authorization + +--- + +### 1. Set Up TDX Host + +Clone and build dstack-vmm: + +```bash +git clone https://github.com/Dstack-TEE/dstack cd dstack cargo build --release -p dstack-vmm -p supervisor mkdir -p vmm-data cp target/release/dstack-vmm vmm-data/ cp target/release/supervisor vmm-data/ cd vmm-data/ +``` + +Create `vmm.toml`: -# create vmm.toml. Edit the config as needed. -cat < vmm.toml -address = "unix:./vmm.sock" +```toml +address = "tcp:0.0.0.0:9080" reuse = true image_path = "./images" run_path = "./run/vm" [cvm] -kms_urls = ["https://kms.test2.dstack.phala.network:9201"] +kms_urls = [] gateway_urls = [] cid_start = 30000 cid_pool_size = 1000 @@ -43,333 +153,381 @@ range = [ ] [host_api] -port = 9300 -EOF +address = "vsock:2" +port = 10000 +``` + +Download guest images from [meta-dstack releases](https://github.com/Dstack-TEE/meta-dstack/releases) and extract to `./images/`. + +> For reproducible builds and verification, see the [Security Model](./security/security-model.md). -# Download Guest OS images -DSTACK_VERSION=0.5.2 -wget "https://github.com/Dstack-TEE/meta-dstack/releases/download/v${DSTACK_VERSION}/dstack-${DSTACK_VERSION}.tar.gz" -mkdir -p images/ -tar -xvf dstack-${DSTACK_VERSION}.tar.gz -C images/ -rm -f dstack-${DSTACK_VERSION}.tar.gz +Start VMM: -# run dstack-vmm +```bash ./dstack-vmm -c vmm.toml ``` -## Deploy the DstackKms contract +--- -A KMS node requires a DstackKms contract to be deployed on the Ethereum-compatible network. +### 2. Deploy KMS as CVM -```bash -cd dstack/kms/auth-eth -npm install -npx hardhat compile -PRIVATE_KEY= npx hardhat kms:deploy --with-app-impl --network phala +Production KMS requires: +- **KMS**: The key management service inside a CVM +- **Auth server**: Webhook server that validates boot requests and returns authorization decisions + +#### Auth Server Options + +| Server | Use Case | Configuration | +|--------|----------|---------------| +| [auth-simple](../kms/auth-simple/) | Config-file-based whitelisting | JSON config file | +| [auth-eth](../kms/auth-eth/) | On-chain governance via smart contracts | Ethereum RPC + contract | +| Custom | Your own authorization logic | Implement webhook interface | + +All auth servers implement the same webhook interface: +- `GET /` - Health check +- `POST /bootAuth/app` - App boot authorization +- `POST /bootAuth/kms` - KMS boot authorization + +#### Using auth-simple (Config-Based) + +auth-simple validates boot requests against a JSON config file. + +Create `auth-config.json` for initial KMS deployment: + +```json +{ + "osImages": ["0x"], + "kms": { "allowAnyDevice": true }, + "apps": {} +} ``` -It will deploy both the DstackApp implementation and DstackKms contract to the Phala network and print the contract addresses: +Run auth-simple: + +```bash +cd kms/auth-simple +bun install +PORT=3001 AUTH_CONFIG_PATH=/path/to/auth-config.json bun run start ``` -Step 1: Deploying DstackApp implementation... -✅ DstackApp implementation deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 -Step 2: Deploying DstackKms... -Deploying proxy... -Waiting for deployment... -DstackKms Proxy deployed to: 0xFE6C45aE66344CAEF5E5D7e2cbD476286D651875 -Implementation deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 -Deployment completed successfully -Transaction hash: 0xd413d01a0640b6193048b0e98afb7c173abe58c74d9cf01f368166bc53f4fefe -✅ Complete KMS setup deployed successfully! -- DstackApp implementation: 0x5FbDB2315678afecb367f032d93F642f64180aa3 -- DstackKms proxy: 0xFE6C45aE66344CAEF5E5D7e2cbD476286D651875 -🚀 Ready for factory app deployments! + +For adding Gateway, apps, and other config fields, see [auth-simple Operations Guide](./auth-simple-operations.md). + +#### Using auth-eth (On-Chain) + +For decentralized governance via smart contracts, see [On-Chain Governance](./onchain-governance.md). + +#### Getting OS Image Hash + +The OS image hash is in the `digest.txt` file inside the guest image tarball: + +```bash +# Extract hash from release tarball +tar -xzf dstack-0.5.5.tar.gz +cat dstack-0.5.5/digest.txt +# Output: 0b327bcd642788b0517de3ff46d31ebd3847b6c64ea40bacde268bb9f1c8ec83 ``` -## Deploy KMS into CVM -The dstack-vmm is running now. Open another terminal and go to the `kms/dstack-app/` directory and run the following command: +Add `0x` prefix for auth-simple config: `0x0b327bcd...` + +#### Deploy KMS CVM + +Choose the deployment script based on your auth server: + +**For auth-simple (external webhook):** + +auth-simple runs on your infrastructure, outside the CVM. ```bash cd dstack/kms/dstack-app/ -./deploy-to-vmm.sh ``` -It will create a template `.env` file. Edit the `.env` file and set the required variables. -Especially the `KMS_CONTRACT_ADDR` variable set to the address of the DstackKms Proxy contract deployed in the previous step. -The `IMAGE_DOWNLOAD_URL` variable should be set to the URL of the dstack OS image used to verify the os_image_hash. -``` -# .env -VMM_RPC=unix:../../vmm-data/vmm.sock -KMS_CONTRACT_ADDR=0xFE6C45aE66344CAEF5E5D7e2cbD476286D651875 + +Edit `.env.simple`: + +```bash +VMM_RPC=http://127.0.0.1:9080 +AUTH_WEBHOOK_URL=http://your-auth-server:3001 KMS_RPC_ADDR=0.0.0.0:9201 GUEST_AGENT_ADDR=127.0.0.1:9205 -ETH_RPC_URL=https://rpc.phala.network -GIT_REV=HEAD -OS_IMAGE=dstack-0.5.2 -IMAGE_DOWNLOAD_URL=https://download.dstack.org/os-images/mr_{OS_IMAGE_HASH}.tar.gz +OS_IMAGE=dstack-0.5.5 +IMAGE_DOWNLOAD_URL=https://github.com/Dstack-TEE/meta-dstack/releases/download/v0.5.5/dstack-0.5.5.tar.gz ``` -Then run the script again. - -Then it will deploy the KMS CVM to the dstack-vmm. Outputs: +Then run: +```bash +./deploy-simple.sh ``` -App compose file created at: .app-compose.json -Compose hash: ec3d427f62bd60afd520fce0be3b368aba4516434f2ff761f74775f871f5b6e3 -Deploying KMS to dstack-vmm... -App ID: ec3d427f62bd60afd520fce0be3b368aba451643 -Created VM with ID: f5299298-bf4f-43c0-839c-88c755391f3c -``` -Go back to the vmm-data directory and check the status of the KMS CVM: +**For auth-eth (on-chain governance):** + +> See [On-Chain Governance Guide](./onchain-governance.md) for deploying KMS with smart contract-based authorization. + +**Monitor startup:** + ```bash -cd ../../vmm-data/ -tail -f run/vm/f5299298-bf4f-43c0-839c-88c755391f3c/serial.log +tail -f ../../vmm-data/run/vm//serial.log ``` -Wait until the KMS CVM is ready: -``` -br-1df48b1c448a: port 2(veth36ab5cb) entered forwarding state -app-compose.sh[882]: Container dstack-kms-1 Started -app-compose.sh[688]: Pruning unused images -app-compose.sh[8347]: Total reclaimed space: 0B -app-compose.sh[688]: Pruning unused volumes -app-compose.sh[8356]: Total reclaimed space: 0B -[ OK ] Finished App Compose Service. -[ OK ] Reached target Multi-User System. - Starting Record Runlevel Change in UTMP... -[ OK ] Finished Record Runlevel Change in UTMP. -``` +Wait for `[ OK ] Finished App Compose Service.` -Now open your browser and go to the KMS listening address: -``` -http://127.0.0.1:9201/ -``` -Click Bootstrap button then fill in the domain serving the KMS. For example: `kms.test2.dstack.phala.network`. -![alt text](assets/kms-bootstrap.png) -You should use the domain name that you will use to access the KMS. -Then click [Bootstrap] -> [Finish setup]. -It will display the public key and corresponding TDX quote of the KMS as shown below: -![alt text](assets/kms-bootstrap-result.png) -The KMS info should be then set to the kms-auth-contract [here for this example](https://explorer.phala.network/address/0xFE6C45aE66344CAEF5E5D7e2cbD476286D651875?tab=write_proxy&source_address=0xa1b5B4Eb15E9366D2e11943F5fE319b62a6E9Da3#0x7d025352): -![alt text](assets/kms-auth-set-info.png) -The KMS instance is now ready to use. +#### Bootstrap KMS + +Open `http://127.0.0.1:9201/` in your browser. + +1. Click **Bootstrap** +2. Enter the domain for your KMS (e.g., `kms.example.com`) +3. Click **Finish setup** + +![KMS Bootstrap](assets/kms-bootstrap.png) + +The KMS will display its public key and TDX quote: + +![KMS Bootstrap Result](assets/kms-bootstrap-result.png) -## Deploy dstack-gateway in CVM -dstack-gateway can be deployed as a dstack app in the same host as the KMS or in a different host. +--- -### Add OS image hash to the KMS whitelist -In order to run user workloads that use the KMS, the OS image hash must be added to the KMS whitelist. +### 3. Deploy Gateway as CVM -The `os_image_hash` is generated during the image build process. It is stored in the `digest.txt` file. +#### Prerequisites -After you get the `os_image_hash`, you can register it to the KMS whitelist by running the following command: +Before deploying Gateway: +1. Register the Gateway app in your auth server config (add to `apps` section in `auth-config.json`) +2. Note the App ID you assign - you'll need it for the `.env` file + +For on-chain governance, see [On-Chain Governance](./onchain-governance.md#register-gateway-app) for registration steps. + +#### Deploy Gateway CVM ```bash -cd dstack/kms/auth-eth -npx hardhat kms:add-image --network phala 0x +cd dstack/gateway/dstack-app/ +./deploy-to-vmm.sh ``` -### Register dstack-gateway in KMS -As a normal dstack app, it requires the app to be registered in the DstackKms contract first. +Edit `.env` with required variables: ```bash -cd dstack/kms/auth-eth -npx hardhat kms:create-app --network phala --allow-any-device -``` +# VMM connection (use TCP if VMM is on same host, or remote URL) +VMM_RPC=http://127.0.0.1:9080 + +# Cloudflare (for DNS-01 ACME challenge) +CF_API_TOKEN=your_cloudflare_api_token -This will deploy an DstackApp contract in the DstackKms contract and print the app ID: +# Domain configuration +SRV_DOMAIN=example.com +PUBLIC_IP=$(curl -s ifconfig.me) -``` -Deploying with account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 -Account balance: 9999.988063729279315546 -Initial device: 0xda2d377e04b7133ec1287a18d465fa44ae9dbb08d929166c6bdb414f38a2acd3 -Initial compose hash: none -Using factory method for single-transaction deployment... -Waiting for transaction 0x46cf1959abf309fcde86bcab2518dcf28dd9eec70c74214f0562e7bf847c50de to be confirmed... -✅ App deployed and registered successfully! -Proxy Address (App Id): 0x32467b43BFa67273FC7dDda0999Ee9A12F2AaA08 -Owner: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 -Transaction hash: 0x46cf1959abf309fcde86bcab2518dcf28dd9eec70c74214f0562e7bf847c50de -Deployed with 1 initial device and 0 initial compose hash -``` +# Gateway app ID (from registration above) +GATEWAY_APP_ID=32467b43BFa67273FC7dDda0999Ee9A12F2AaA08 -Register the app ID to the kms as the gateway app ID: -```bash -npx hardhat kms:set-gateway --network phala 0x32467b43BFa67273FC7dDda0999Ee9A12F2AaA08 +# Gateway URLs +MY_URL=https://gateway.example.com:9202 +BOOTNODE_URL=https://gateway.example.com:9202 + +# WireGuard (uses same port as RPC) +WG_ADDR=0.0.0.0:9202 + +# Network settings +SUBNET_INDEX=0 +ACME_STAGING=no # Set to 'yes' for testing +OS_IMAGE=dstack-0.5.5 ``` -Now go to the `gateway/dstack-app/` directory and run the following command: +**Note on hex formats:** +- Gateway `.env` file: Use raw hex without `0x` prefix (e.g., `GATEWAY_APP_ID=32467b43...`) +- auth-simple config: Use `0x` prefix (e.g., `"0x32467b43..."`). The server normalizes both formats. + +Run the script again: + ```bash -cd ../../gateway/dstack-app/ -./deploy-to-vmm.sh +./deploy-to-vmm.sh ``` -It will create a template .env file. Edit the .env file and set the required variables. +The script will display the compose file and compose hash, then prompt for confirmation: ``` -# .env -VMM_RPC=unix:../../vmm-data/vmm.sock +Docker compose file: +... +Compose hash: 0x700a50336df7c07c82457b116e144f526c29f6d8... +Configuration: +... +Continue? [y/N] +``` -# Cloudflare API token for DNS challenge used to get the SSL certificate. -CF_API_TOKEN=your_cloudflare_api_token +**Before pressing 'y'**, add the compose hash to your auth server whitelist: +- For auth-simple: Add to `composeHashes` array in `auth-config.json` +- For auth-eth: Use `app:add-hash` (see [On-Chain Governance](./onchain-governance.md#register-gateway-app)) -# Service domain -SRV_DOMAIN=test2.dstack.phala.network +Then return to the first terminal and press 'y' to deploy. -# Public IP address -PUBLIC_IP=$(curl -s ifconfig.me) +#### Update VMM Configuration -# Gateway application ID. Register the app in DstackKms first to get the app ID. -GATEWAY_APP_ID=0x31884c4b7775affe4c99735f6c2aff7d7bc6cfcd +After Gateway is running, update `vmm.toml` with KMS and Gateway URLs: -# Whether to use ACME staging (yes/no) -ACME_STAGING=yes +```toml +[cvm] +kms_urls = ["https://kms.example.com:9201"] +gateway_urls = ["https://gateway.example.com:9202"] +``` -# Subnet index. 0~15 -SUBNET_INDEX=0 +Restart dstack-vmm to apply changes. -# My URL. The URL will be synced to other nodes in the cluster so that each node can discover other nodes. -MY_URL=https://gateway.test2.dstack.phala.network:9202 +--- -# Bootnode URL. If you want to deploy a multi-node dstack-gateway cluster, set the bootnode URL to the URL of another node already deployed or planed to be deployed later. -BOOTNODE_URL=https://gateway.test2.dstack.phala.network:9202 +### 4. Zero Trust HTTPS (Optional) -# dstack OS image name -OS_IMAGE=dstack-0.5.2 +Generate TLS certificates inside the TEE with automatic CAA record management. -# Set defaults for variables that might not be in .env -GIT_REV=HEAD +Configure in `build-config.sh`: -# Port configurations -GATEWAY_RPC_ADDR=0.0.0.0:9202 -GATEWAY_ADMIN_RPC_ADDR=127.0.0.1:9203 -GATEWAY_SERVING_ADDR=0.0.0.0:9204 -GUEST_AGENT_ADDR=127.0.0.1:9206 -WG_ADDR=0.0.0.0:9202 +```bash +GATEWAY_CERT=${CERTBOT_WORKDIR}/live/cert.pem +GATEWAY_KEY=${CERTBOT_WORKDIR}/live/key.pem +CF_API_TOKEN= +ACME_URL=https://acme-v02.api.letsencrypt.org/directory ``` -Then run the script again. -It should show the prompt to confirm the deployment: -``` -App compose file created at: .app-compose.json -Compose hash: 700a50336df7c07c82457b116e144f526c29f6d8f4a0946b3e88065c9beba0f4 -Configuration: -VMM_RPC: unix:../../build/vmm.sock -SRV_DOMAIN: test5.dstack.phala.network -PUBLIC_IP: 66.220.6.113 -GATEWAY_APP_ID: 31884c4b7775affe4c99735f6c2aff7d7bc6cfcd -MY_URL: https://gateway.test5.dstack.phala.network:9202 -BOOTNODE_URL: https://gateway.test2.dstack.phala.network:9202 -SUBNET_INDEX: 0 -WG_ADDR: 0.0.0.0:9202 -GATEWAY_RPC_ADDR: 0.0.0.0:9202 -GATEWAY_ADMIN_RPC_ADDR: 127.0.0.1:9203 -GATEWAY_SERVING_ADDR: 0.0.0.0:9204 -GUEST_AGENT_ADDR: 127.0.0.1:9206 -Continue? [y/N] +Run certbot: + +```bash +RUST_LOG=info,certbot=debug ./certbot renew -c certbot.toml ``` -Don't press `y` yet. We need to add the compose hash to the DstackApp contract first. Go back to the `kms/auth-eth` directory and run the following command: +This will: +- Create an ACME account +- Set CAA DNS records on Cloudflare +- Request and auto-renew certificates + +--- + +### 5. Certificate Transparency Monitoring (Optional) + +Monitor for unauthorized certificates issued to your domain. ```bash -npx hardhat app:add-hash --network phala --app-id 0x31884c4b7775affe4c99735f6c2aff7d7bc6cfcd 0x700a50336df7c07c82457b116e144f526c29f6d8f4a0946b3e88065c9beba0f4 +cargo build --release -p ct_monitor +./target/release/ct_monitor \ + --gateway-uri https:// \ + --domain ``` -After the transaction is confirmed, you can press `y` to continue the deployment. +**How it works:** +1. Fetches known public keys from Gateway (`/acme-info` endpoint) +2. Queries crt.sh for certificates issued to your domain +3. Verifies each certificate's public key matches the known keys +4. Logs errors (❌) when certificates are issued to unknown public keys -Similar to the KMS deployment, it will deploy the dstack-gateway CVM to the dstack-vmm and it will start serving later. +The monitor runs in a loop, checking every 60 seconds. Integrate with your alerting system by monitoring stderr for error messages. -## Deploy dstack-vmm on other TDX hosts to serve user workloads -After the KMS and dstack-gateway are deployed, you can deploy dstack-vmm on other TDX hosts to serve user workloads. -You can follow the steps at the beginning of this document to deploy dstack-vmm on other TDX hosts. -Edit the vmm.toml file to set the KMS and dstack-gateway URLs. +--- -``` -# vmm.toml +### 6. Multi-Node Deployment (Optional) + +Scale by adding VMM nodes and KMS replicas for high availability. + +#### Adding VMM Nodes + +On each additional TDX host: +1. Set up dstack-vmm (see step 1) +2. Configure `vmm.toml` with existing KMS/Gateway URLs +3. Start VMM + +```toml [cvm] -kms_urls = ["https://kms.test2.dstack.phala.network:9201"] -gateway_urls = ["https://gateway.test2.dstack.phala.network:9202"] +kms_urls = ["https://kms.example.com:9201"] +gateway_urls = ["https://gateway.example.com:9202"] ``` -Then restart the dstack-vmm. +#### Adding KMS Replicas (Onboarding) -## Deploy app on the dstack-vmm +Additional KMS instances can onboard from an existing KMS to share the same root keys. This enables: +- High availability (multiple KMS nodes) +- Geographic distribution +- Load balancing -After the dstack-vmm is ready, you can deploy an app on it following the steps below. +**How it works:** -### 1. On-chain Registration +1. New KMS starts in onboard mode (empty `auto_bootstrap_domain`) +2. New KMS calls `GetTempCaCert` on source KMS +3. New KMS generates RA-TLS certificate with TDX quote +4. New KMS calls `GetKmsKey` with mTLS authentication +5. Source KMS verifies attestation via `bootAuth/kms` webhook +6. If approved, source KMS returns root keys +7. Both KMS instances now derive identical keys -The on-chain registration process includes two steps: +**Configure new KMS for onboarding:** -1. Deploy an App's control contract DstackApp. Developers can develop their own or choose the reference contract from the dstack repository. Custom contracts need to implement the IAppAuth interface. -2. Call DstackKms.registerApp(appContractAddress) to register the contract. +```toml +[core.onboard] +enabled = true +auto_bootstrap_domain = "" # Empty = onboard mode +quote_enabled = true # Require TDX attestation +address = "0.0.0.0" +port = 9203 # HTTP port for onboard UI +``` -The dstack repository provides scripts to complete these two steps: +**Trigger onboard via API:** -**Option 1: Traditional deployment (2 transactions)** ```bash -git clone https://github.com/Dstack-TEE/dstack -cd dstack/kms/auth-eth -npm install -npx hardhat compile -export PRIVATE_KEY= -export KMS_CONTRACT_ADDRESS=0xFE6C45aE66344CAEF5E5D7e2cbD476286D651875 -npx hardhat app:deploy --allow-any-device --network phala +curl -X POST http://:9203/prpc/Onboard.Onboard?json \ + -H "Content-Type: application/json" \ + -d '{"source_url": "https://:9201/prpc", "domain": "kms2.example.com"}' ``` -**Option 2: Factory deployment (1 transaction, recommended)** +**Finish and restart:** + ```bash -npx hardhat kms:create-app --allow-any-device --network phala +curl http://:9203/finish +# Restart KMS - it will now serve as a full KMS with shared keys ``` -Command output: -``` -Deploying with account: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 -Account balance: 9999.995278992293365404 -App ID: 0xA35b434eE853fdf9c2Bf48Fa1583Ac1332d50255 -Starting DstackApp deployment process... -Deploying proxy... -Waiting for deployment... -DstackApp deployed to: 0xD4a546B1C7e63CD4CeD314b2C90108e49191A915 -Implementation deployed to: 0x5aC1671E1Df54994D023F0B05806821d6D84e086 -Deployment completed successfully -Transaction hash: 0xceac2ac6d56a40fef903b947d3a05df42ccce66da7f356c5d54afda68277f9a9 -Waiting for transaction 0xe144e9007208079e5e82c04f727d2383c58184e74d4f860e62557b5f330ab832 to be confirmed... -✅ App deployed and registered successfully! -App ID: 0xA35b434eE853fdf9c2Bf48Fa1583Ac1332d50255 -Proxy Address: 0xD4a546B1C7e63CD4CeD314b2C90108e49191A915 -Owner: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 -Transaction hash: 0xe144e9007208079e5e82c04f727d2383c58184e74d4f860e62557b5f330ab832 -``` +> **Note:** For KMS onboarding with `quote_enabled = true`, add the KMS mrAggregated hash to your auth server's `kms.mrAggregated` whitelist. -Note the AppId, which needs to be filled in when deploying cvm. +--- -**Additional options:** -- Add initial device ID: `--device 0x1234...` -- Add initial compose hash: `--hash 0x5678...` -- Both deployment methods support these optional parameters for pre-configuration during deployment. +## Deploying Apps -If you need to upgrade the contract in the future, please backup the `.openzeppelin/unknown-2035.json` file. +After setup, deploy apps via the VMM dashboard or CLI. -### 2. Add the App compose hash to the whitelist +### Register App -Build app-compose.json and calculate its sha256 to get compose-hash. The compose hash can also be previewed in the dstack-vmm UI. +Before deploying, register your app in your auth server: +- For auth-simple: See [auth-simple Operations Guide](./auth-simple-operations.md#adding-an-app) +- For auth-eth: See [On-Chain Governance](./onchain-governance.md#register-apps-on-chain) -Call the hardhat command to add it to the whitelist (using DstackApp as an example here; custom DstackApp follows its own custom permission control logic). +### Deploy via UI -```bash -export PRIVATE_KEY= -npx hardhat app:add-hash --network phala --app-id 0xA35b434eE853fdf9c2Bf48Fa1583Ac1332d50255 0x44d9cb98aaa6ab11f5729fc7d6fd58117585e0e3fbec621612dcee6b2dfbcde5 -``` +Open `http://localhost:9080`: -### 3. Deploy instances using dstack-vmm +![App Deploy](assets/app-deploy.png) -![app deploy](assets/app-deploy.png) -- Select image `dstack-0.4.2` -- Fill in the AppId applied in the contract during deployment -- Currently, test DstackKms has set a whitelist for Base image, requiring instance memory to be `≥ 3G` or exactly `= 2G` +- Select the OS image +- Enter the App ID (from registration above) +- Upload your `docker-compose.yaml` -After the app starts normally, click [Board] to access. +After startup, click **Dashboard** to view: -You can find the connections to dstack-gateway nodes, meaning that the app is now reachable from the internet: +![App Board](assets/app-board.png) -![app board](assets/app-board.png) +--- + +## Troubleshooting + +### Error: vhost-vsock: unable to set guest cid: Address already in use + +The CID range conflicts with existing VMs. + +1. Find used CIDs: `ps aux | grep 'guest-cid='` +2. Update `vmm.toml`: + ```toml + [cvm] + cid_start = 33000 + cid_pool_size = 1000 + ``` + +### Error: Operation not permitted when building guest image + +Ubuntu 23.10+ restricts unprivileged user namespaces: + +```bash +sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0 +``` diff --git a/docs/onchain-governance.md b/docs/onchain-governance.md new file mode 100644 index 00000000..67497f8b --- /dev/null +++ b/docs/onchain-governance.md @@ -0,0 +1,189 @@ +# On-Chain Governance + +This guide covers setting up on-chain governance for dstack using smart contracts on Ethereum. + +## Overview + +On-chain governance adds: +- **Smart contract-based authorization**: App registration and whitelisting managed by smart contracts +- **Decentralized trust**: No single operator controls keys +- **Transparent policies**: Anyone can verify authorization rules on-chain + +## Prerequisites + +- Production dstack deployment with KMS and Gateway as CVMs (see [Deployment Guide](./deployment.md)) +- Ethereum wallet with funds on Sepolia testnet (or your target network) +- Node.js and npm installed +- Alchemy API key (for Sepolia) - get one at https://www.alchemy.com/ + +## Deploy DstackKms Contract + +```bash +cd dstack/kms/auth-eth +npm install +npx hardhat compile +PRIVATE_KEY= ALCHEMY_API_KEY= npx hardhat kms:deploy --with-app-impl --network sepolia +``` + +The command will prompt for confirmation. Sample output: + +``` +✅ DstackApp implementation deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 +DstackKms Proxy deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 +Implementation deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 +``` + +Note the proxy address (e.g., `0x9fE4...`). + +Set environment variables for subsequent commands: + +```bash +export KMS_CONTRACT_ADDRESS="" +export PRIVATE_KEY="" +export ALCHEMY_API_KEY="" +``` + +## Configure KMS for On-Chain Auth + +The KMS CVM includes an auth-api service that connects to your DstackKms contract. Configure it via environment variables in the KMS CVM: + +```bash +KMS_CONTRACT_ADDR= +ETH_RPC_URL= +``` + +Note: The auth-api uses `KMS_CONTRACT_ADDR`, while Hardhat tasks use `KMS_CONTRACT_ADDRESS`. + +The auth-api validates boot requests against the smart contract. See [Deployment Guide](./deployment.md#2-deploy-kms-as-cvm) for complete setup instructions. + +## Whitelist OS Image + +```bash +npx hardhat kms:add-image --network sepolia 0x +``` + +Output: `Image added successfully` + +The `os_image_hash` is in the `digest.txt` file from the guest OS image build (see [Building Guest Images](./deployment.md#building-guest-images)). + +## Register Gateway App + +```bash +npx hardhat kms:create-app --network sepolia --allow-any-device +``` + +Sample output: + +``` +✅ App deployed and registered successfully! +Proxy Address (App Id): 0x75537828f2ce51be7289709686A69CbFDbB714F1 +``` + +Note the App ID (Proxy Address) from the output. + +Set it as the gateway app: + +```bash +npx hardhat kms:set-gateway --network sepolia +``` + +Output: `Gateway App ID set successfully` + +Add the gateway's compose hash to the whitelist. To compute the compose hash: + +```bash +sha256sum /path/to/gateway-compose.json | awk '{print "0x"$1}' +``` + +Then add it: + +```bash +npx hardhat app:add-hash --network sepolia --app-id +``` + +Output: `Compose hash added successfully` + +## Register Apps On-Chain + +For each app you want to deploy: + +### Create App + +```bash +npx hardhat kms:create-app --network sepolia --allow-any-device +``` + +Note the App ID from the output. + +### Add Compose Hash + +Compute your app's compose hash: + +```bash +sha256sum /path/to/your-app-compose.json | awk '{print "0x"$1}' +``` + +Then add it: + +```bash +npx hardhat app:add-hash --network sepolia --app-id +``` + +### Deploy via VMM + +Use the App ID when deploying through the VMM dashboard or [VMM CLI](./vmm-cli-user-guide.md). + +## Smart Contract Reference + +### DstackKms (Main Contract) + +The central governance contract that manages OS image whitelisting, app registration, and KMS authorization. + +| Function | Description | +|----------|-------------| +| `addOsImageHash(bytes32)` | Whitelist an OS image hash | +| `removeOsImageHash(bytes32)` | Remove an OS image from whitelist | +| `setGatewayAppId(string)` | Set the trusted Gateway app ID | +| `registerApp(address)` | Register an app contract | +| `deployAndRegisterApp(...)` | Deploy and register app in one transaction | +| `isAppAllowed(AppBootInfo)` | Check if an app is allowed to boot | +| `isKmsAllowed(AppBootInfo)` | Check if KMS is allowed to boot | + +### DstackApp (Per-App Contract) + +Each app has its own contract controlling which compose hashes and devices are allowed. + +| Function | Description | +|----------|-------------| +| `addComposeHash(bytes32)` | Whitelist a compose hash | +| `removeComposeHash(bytes32)` | Remove a compose hash from whitelist | +| `addDevice(bytes32)` | Whitelist a device ID | +| `removeDevice(bytes32)` | Remove a device from whitelist | +| `setAllowAnyDevice(bool)` | Allow any device to run this app | +| `isAppAllowed(AppBootInfo)` | Check if app can boot with given config | +| `disableUpgrades()` | Permanently disable contract upgrades | + +### AppBootInfo Structure + +Both `isAppAllowed` and `isKmsAllowed` take an `AppBootInfo` struct: + +```solidity +struct AppBootInfo { + address appId; // Unique app identifier (contract address) + bytes32 composeHash; // Hash of docker-compose configuration + address instanceId; // Unique instance identifier + bytes32 deviceId; // Hardware device identifier + bytes32 mrAggregated; // Aggregated measurement register + bytes32 mrSystem; // System measurement register + bytes32 osImageHash; // OS image hash + string tcbStatus; // TCB status (e.g., "UpToDate") + string[] advisoryIds; // Security advisory IDs +} +``` + +Source: [`kms/auth-eth/contracts/`](../kms/auth-eth/contracts/) + +## See Also + +- [Deployment Guide](./deployment.md) - Setting up dstack infrastructure +- [Security Best Practices](./security/security-best-practices.md) diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 00000000..bc82eded --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,17 @@ +# Security Documentation + +dstack security resources for auditors, researchers, and operators. + +## Audit + +dstack has been audited by zkSecurity. See the [full audit report](./dstack-audit.pdf). + +## Documentation + +- [Security Model](./security-model.md) - Threat model, trust boundaries, and verification checklist +- [Security Best Practices](./security-best-practices.md) - Production hardening guide +- [CVM Boundaries](./cvm-boundaries.md) - Information exchange and isolation details + +## Responsible Disclosure + +To report a security vulnerability, email security@phala.network. We will respond within 48 hours. diff --git a/docs/security-guide/cvm-boundaries.md b/docs/security/cvm-boundaries.md similarity index 98% rename from docs/security-guide/cvm-boundaries.md rename to docs/security/cvm-boundaries.md index b15c844c..06b8e0cb 100644 --- a/docs/security-guide/cvm-boundaries.md +++ b/docs/security/cvm-boundaries.md @@ -117,7 +117,7 @@ dstack uses encrypted environment variables to allow app developers to securely - CVM performs basic regex validation on values - Final result is stored as /dstack/.hostshared/.decrypted-env and loaded system-wide via app-compose.service -This file is not measured to RTMRs. But it is highly recommended to add application-specific integrity checks on encrypted environment variables at the application layer. See [security-guide.md](security-guide.md) for more details. +This file is not measured to RTMRs. But it is highly recommended to add application-specific integrity checks on encrypted environment variables at the application layer. See [security-best-practices.md](./security-best-practices.md) for more details. ### .user-config This is an optional application-specific configuration file that applications inside the CVM can access. dstack OS simply stores it at /dstack/user-config without any measurement or additional processing. diff --git a/docs/security-guide/security-guide.md b/docs/security/security-best-practices.md similarity index 92% rename from docs/security-guide/security-guide.md rename to docs/security/security-best-practices.md index f0e42f83..e93ffb68 100644 --- a/docs/security-guide/security-guide.md +++ b/docs/security/security-best-practices.md @@ -2,6 +2,10 @@ This document describes security considerations for deploying dstack apps in production. +## Security Audit + +dstack has been audited by [zkSecurity](https://www.zksecurity.xyz/). The audit covered the KMS, guest agent, and attestation verification components. See the [full audit report](./dstack-audit.pdf) for findings and remediation status. + ## Always pin image hash in your docker-compose.yaml When deploying applications in a TEE environment, it's critical to ensure the integrity and immutability of your container images. Using image digests (SHA256 hashes) instead of tags cryptographically ensures that the exact same image is always pulled, preventing supply chain attacks. This proves to users that your App is anchored to a specific code version. @@ -38,11 +42,11 @@ dstack provides encrypted environment variable functionality. Although the CVM p If you use dstack-vmm's built-in UI, the prelaunch script has already been automatically filled in for you: -![alt text](assets/prelaunch-script.png) +![Prelaunch Script](../assets/prelaunch-script.png) You only need to add the `APP_LAUNCH_TOKEN` environment variable to enable LAUNCH_TOKEN checking. -![alt text](assets/token-env.png) +![Token Environment Variable](../assets/token-env.png) user_config is not encrypted, and similarly requires integrity checks at the application layer. For example, you can store a USER_CONFIG_HASH in encrypted environment variables and verify it in the prelaunch script. diff --git a/docs/security/security-model.md b/docs/security/security-model.md new file mode 100644 index 00000000..b07e6c05 --- /dev/null +++ b/docs/security/security-model.md @@ -0,0 +1,131 @@ +# dstack Security Model + +dstack protects your code and data from infrastructure operators. Using TEE hardware isolation, your workloads run in encrypted memory that the host cannot read or modify. You can cryptographically verify that your exact code runs in genuine TEE hardware. + +This document helps you evaluate whether dstack's security model fits your needs. + +## Trust Boundaries + +dstack removes the need to trust infrastructure operators. The cloud provider cannot read your memory, modify your code, or access your secrets. Network attackers cannot intercept your traffic because TLS terminates inside the TEE with keys fully controlled by the TEE (Zero Trust HTTPS). Docker registries cannot serve malicious images because the TEE verifies SHA256 digests before pulling. + +The only thing you must trust is **TEE hardware** (currently Intel TDX, with AMD SEV support planned). You trust that the TEE provides genuine memory encryption and that quotes are signed by real hardware. For GPU workloads, you also trust **NVIDIA GPU hardware** and NVIDIA's Remote Attestation Service (NRAS). These are hardware-level trust assumptions. + +Everything else is verifiable. + +**The dstack OS** is measured during boot and recorded in the attestation quote. You verify it by rebuilding from [meta-dstack](https://github.com/Dstack-TEE/meta-dstack) source and comparing measurements, or by checking that the OS hash is whitelisted in a governance contract you trust. + +**The KMS** runs in its own TEE with its own attestation quote. You verify it the same way you verify any dstack workload. + +### What dstack Cannot Protect + +TEE technology has inherent limitations. Side-channel attacks against TEE hardware are researched actively, and microarchitectural vulnerabilities are discovered periodically. Hardware vendors release TCB updates to address these, so keep your TCB version current. + +dstack protects the execution environment, not your application code. Bugs in your application remain exploitable. Secrets that you log or transmit insecurely can still leak. Your code must follow secure development practices. + +Infrastructure operators can still deny service. They can shut down your workload, throttle resources, or block network access. If availability matters, plan for redundancy across providers. + +## Security Guarantees + +### Confidentiality + +| Layer | Protection | Mechanism | +|-------|------------|-----------| +| Memory | Encrypted at runtime | TEE hardware encryption | +| Disk | Encrypted at rest | Per-app keys from KMS (AES-256-GCM) | +| Environment | Encrypted in transit | X25519 ECDH + AES-256-GCM | +| Network | Encrypted end-to-end | Zero Trust HTTPS (TLS terminates in TEE) | + +### Integrity + +| Component | Verification | Measurement | +|-----------|--------------|-------------| +| Hardware | TEE signature | Attestation quote | +| Firmware | Boot measurement | MRTD | +| OS | Boot measurement | RTMR0-2 | +| Application | Runtime measurement | RTMR3 (compose-hash) | + +### Isolation + +Each application derives unique keys from the KMS based on its identity. Instance-level secrets use the instance ID to create unique disk encryption keys. No keys are shared between different applications. + +## GPU Security for AI Workloads + +dstack supports NVIDIA H100, H200, and B200 GPUs in confidential compute mode for AI inference and training workloads. + +### How It Works + +GPUs are passed through via VFIO directly to the TEE-protected CVM. The GPU operates in confidential compute mode, encrypting data during computation. Both the CPU TEE and NVIDIA GPU provide hardware isolation together. If either component fails verification, the security model breaks. + +### Dual Attestation + +GPU workloads require verification of both hardware components. The CPU TEE provides the quote that verifies CPU and memory isolation. NVIDIA's Remote Attestation Service (NRAS) independently verifies the GPU is genuine and running in confidential mode. Both attestations must pass for complete verification. + +### AI Workload Protection + +Models and training data stay within the hardware-protected environment. The infrastructure operator cannot access model weights, training data, or inference inputs/outputs. Response integrity is provable through cryptographic signatures generated inside the TEE. Performance overhead is minimal, achieving approximately 99% efficiency compared to native execution. + +## Chain of Trust + +dstack implements layered verification from hardware to application. Each layer is measured and included in the attestation quote, which TEE hardware cryptographically signs. + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Attestation Quote (signed by TEE hardware) │ +│ ├── Hardware: TEE signature proves genuine hardware │ +│ ├── MRTD: Virtual firmware measurement │ +│ ├── RTMR0-2: OS kernel and boot parameters │ +│ ├── RTMR3: Application (compose-hash) + KMS binding │ +│ └── reportData: Your challenge (replay protection) │ +├─────────────────────────────────────────────────────────────────┤ +│ Event Log (RTMR3 breakdown) │ +│ ├── compose-hash: SHA256 of your docker-compose │ +│ ├── key-provider: KMS root CA public key hash │ +│ └── instance-id: Unique per deployment │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Hardware layer.** The TEE provides the root of trust. The attestation quote is cryptographically signed by TEE hardware, and verification confirms the signature chain. The TCB status shows whether firmware is patched against known vulnerabilities. + +**OS layer.** The dstack OS is measured during boot into MRTD and RTMR0-2. MRTD captures the virtual firmware. RTMR0 captures firmware configuration. RTMR1 captures the Linux kernel. RTMR2 captures kernel command-line parameters. You verify integrity by computing expected measurements from meta-dstack source and comparing them to the quote. + +**Application layer.** Your application is measured into RTMR3 as the compose-hash, which is the SHA256 hash of your normalized docker-compose configuration. Each image must use SHA256 digest pinning. This proves exactly which container images are running and that no code substitution happened after measurement. + +**Key management layer.** The KMS root CA public key hash is recorded in RTMR3 as the key-provider event. This binds your workload to a specific KMS instance. The KMS itself runs in a TEE with its own attestation quote, so you can verify the KMS the same way you verify any workload. + +## Verification Checklist + +Use this checklist to verify a workload running in a dstack CVM. + +**Platform verification:** +- [ ] Attestation quote signature is valid +- [ ] TCB status is up-to-date (no unpatched vulnerabilities) +- [ ] OS measurements match expected values (MRTD, RTMR0-2) +- [ ] OS image hash is whitelisted (if using governance) + +**Application verification:** +- [ ] compose-hash matches your docker-compose +- [ ] All images use SHA256 digests (no mutable tags) +- [ ] RTMR3 event log replays correctly +- [ ] reportData contains your challenge (replay protection) + +**Key management verification:** +- [ ] key-provider matches expected KMS identity +- [ ] KMS attestation is valid + +## Limitations + +### Attestation proves identity, not correctness + +Attestation proves which code is running, not that the code is bug-free. It proves the environment is isolated, not that your application handles secrets correctly. You still need to audit your application code and follow secure development practices. + +### Environment variables need application-layer authentication + +Encrypted environment variables prevent the host from reading your secrets. However, the host can replace encrypted values with different ones. Your application should verify authenticity using patterns like LAUNCH_TOKEN. See [security-best-practices.md](./security-best-practices.md) for details. + +### KMS root key security + +All keys derive from the KMS root key, which is protected by TEE isolation. Like all TEE-based systems, a TEE compromise could expose the root key. We are developing MPC-based KMS where the root key is distributed across multiple parties, eliminating this single point of failure. + +## Further Reading + +For production deployment guidance, see [security-best-practices.md](./security-best-practices.md). For smart contract authorization details, see [onchain-governance.md](../onchain-governance.md). For technical details about CVM boundaries and APIs, see [cvm-boundaries.md](./cvm-boundaries.md). diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 00000000..64684c6c --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,119 @@ +# dstack Usage Guide + +This guide covers deploying and managing applications on dstack. For infrastructure setup and self-hosting, see the [Deployment Guide](./deployment.md). + +You can manage VMs via the dashboard or [CLI](./vmm-cli-user-guide.md). + +## Deploy an App + +Open the dstack-vmm webpage [http://localhost:9080](http://localhost:9080) (change the port according to your configuration) on your local machine to deploy a `docker-compose.yaml` file: + +
+VMM Interface +
+ +After the container is deployed, it should take some time to start the CVM and the containers. Time would vary depending on your workload. + +- **[Logs]**: Click this button to view the CVM logs and monitor container startup progress +- **[Dashboard]**: Once the container is running, click this button to view container information and logs. (Note: This button is only visible when dstack-gateway is enabled. If disabled, you'll need to add a port mapping to port 8090 to access the CVM dashboard) + +
+Guest Agent Dashboard +
+ +## Pass Secrets to Apps + +When deploying a new App, you can pass private data via Encrypted Environment Variables. These variables can be referenced in the docker-compose.yaml file as shown below: + +
+Secret Management +
+ +The environment variables will be encrypted on the client-side and decrypted in the CVM before being passed to the containers. + +## Access the App + +Once your app is deployed and listening on an HTTP port, you can access it through dstack-gateway's public domain using these ingress mapping rules: + +- `[-[][s|g]].` maps to port `` in the CVM + +**Examples:** + +- `3327603e03f5bd1f830812ca4a789277fc31f577-8080.test0.dstack.org` - port `8080` (TLS termination to any TCP) +- `3327603e03f5bd1f830812ca4a789277fc31f577-8080g.test0.dstack.org` - port `8080` (TLS termination with HTTP/2 negotiation) +- `3327603e03f5bd1f830812ca4a789277fc31f577-8080s.test0.dstack.org` - port `8080` (TLS passthrough to any TCP) + +The `` can be either the app ID or instance ID. When using the app ID, the load balancer will select one of the available instances. Adding an `s` suffix enables TLS passthrough to the app instead of terminating at dstack-gateway. Adding a `g` suffix enables HTTPS/2 with TLS termination for gRPC applications. + +**Note:** If dstack-gateway is disabled, you'll need to use port mappings configured during deployment to access your application via the host's IP address and mapped ports. + +For development images (`dstack-x.x.x-dev`), you can SSH into the CVM for inspection: + +```bash +# Find the CVM wg IP address in the dstack-vmm dashboard +ssh root@10.0.3.2 +``` + +## Getting TDX Quote in Docker Container + +To get a TDX quote within app containers: + +**1. Mount the socket in `docker-compose.yaml`** + +```yaml +version: '3' +services: + nginx: + image: nginx:latest + volumes: + - /var/run/dstack.sock:/var/run/dstack.sock + ports: + - "8080:80" + restart: always +``` + +**2. Execute the quote request command** + +```bash +# The argument report_data accepts binary data encoding in hex string. +# The actual report_data passing to the underlying TDX driver is sha2_256(report_data). +curl --unix-socket /var/run/dstack.sock http://localhost/GetQuote?report_data=0x1234deadbeef | jq . +``` + +## Container Logs + +Container logs can be obtained from the CVM's `dashboard` page or by curl: + +```bash +curl 'http://.:9090/logs/?since=0&until=0&follow=true&text=true×tamps=true&bare=true' +``` + +Replace `` and `` with actual values. Available parameters: + +| Parameter | Description | +|-----------|-------------| +| `since=0` | Starting Unix timestamp for log retrieval | +| `until=0` | Ending Unix timestamp for log retrieval | +| `follow` | Enables continuous log streaming | +| `text` | Returns human-readable text instead of base64 encoding | +| `timestamps` | Adds timestamps to each log line | +| `bare` | Returns the raw log lines without json format | + +**Example response:** +```bash +$ curl 'http://0.0.0.0:9190/logs/zk-provider-server?text×tamps' +{"channel":"stdout","message":"2024-09-29T03:05:45.209507046Z Initializing Rust backend...\n"} +{"channel":"stdout","message":"2024-09-29T03:05:45.209543047Z Calling Rust function: init\n"} +{"channel":"stdout","message":"2024-09-29T03:05:45.209544957Z [2024-09-29T03:05:44Z INFO rust_prover] Initializing...\n"} +{"channel":"stdout","message":"2024-09-29T03:05:45.209546381Z [2024-09-29T03:05:44Z INFO rust_prover::groth16] Starting setup process\n"} +``` + +## TLS Passthrough with Custom Domain + +dstack-gateway supports TLS passthrough for custom domains. + +See the example [here](https://github.com/Dstack-TEE/dstack-examples/tree/main/custom-domain/dstack-ingress) for more details. + +## Upgrade an App + +Go to the dstack-vmm webpage, click the **[Upgrade]** button, select or paste the compose file you want to upgrade to, and click the **[Upgrade]** button again. The app id does not change after the upgrade. Stop and start the app to apply the upgrade. diff --git a/docs/verification.md b/docs/verification.md new file mode 100644 index 00000000..17c5170c --- /dev/null +++ b/docs/verification.md @@ -0,0 +1,33 @@ +# Verification + +Attestation is cryptographic proof that your app runs in genuine TEE hardware with exactly the code you expect. No one can fake it. + +## What Attestation Proves + +When you verify a dstack deployment, you're checking three things: + +1. **Genuine hardware** - Hardware vendor signatures confirm real TEE hardware generated the proof (Intel TDX, NVIDIA CC, or AMD SEV) +2. **Correct code** - The compose-hash matches your docker-compose configuration +3. **Secure environment** - OS and firmware measurements show no tampering + +If any of these fail, the cryptographic proof won't verify. + +## How to Verify + +**Phala Cloud users**: Every deployment gets an automatic [Trust Center](https://trust.phala.com) report. This verifies hardware, code, and environment without manual steps. + +**Programmatic verification**: dstack provides several tools: + +- [dstack-verifier](https://github.com/Dstack-TEE/dstack/tree/master/verifier) - HTTP service with `/verify` endpoint, also runs as CLI +- [dcap-qvl](https://github.com/Phala-Network/dcap-qvl) - Open source quote verification library (Rust, Python, JS/WASM, CLI) +- [SDKs](../sdk/) - JavaScript and Python SDKs include `replayRtmrs()` for local RTMR verification + +## Learn More + +- [Attestation Documentation](https://docs.phala.com/phala-cloud/attestation/overview) - Generating quotes, programmatic verification, RTMR3 replay +- [Confidential AI Verification](https://docs.phala.com/phala-cloud/confidential-ai/verify/overview) - GPU TEE attestation for AI workloads +- [Domain Attestation](https://docs.phala.com/phala-cloud/networking/domain-attestation) - TLS certificates managed in TEE + +## See It Live + +Visit [chat.redpill.ai](https://chat.redpill.ai) and click the shield icon next to any response. This shows attestation verification from a real confidential AI deployment. diff --git a/kms/auth-eth/lib/forge-std b/kms/auth-eth/lib/forge-std new file mode 160000 index 00000000..77041d2c --- /dev/null +++ b/kms/auth-eth/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 77041d2ce690e692d6e03cc812b57d1ddaa4d505 diff --git a/kms/auth-eth/lib/openzeppelin-contracts-upgradeable b/kms/auth-eth/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 00000000..e725abdd --- /dev/null +++ b/kms/auth-eth/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit e725abddf1e01cf05ace496e950fc8e243cc7cab diff --git a/kms/auth-eth/lib/openzeppelin-foundry-upgrades b/kms/auth-eth/lib/openzeppelin-foundry-upgrades new file mode 160000 index 00000000..cbce1e00 --- /dev/null +++ b/kms/auth-eth/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit cbce1e00305e943aa1661d43f41e5ac72c662b07 diff --git a/kms/auth-simple/.oxlintrc.json b/kms/auth-simple/.oxlintrc.json new file mode 100644 index 00000000..f7d7b8e3 --- /dev/null +++ b/kms/auth-simple/.oxlintrc.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/crates/oxc_linter/src/config/schema.json", + "rules": { + "typescript": "warn", + "unicorn": "warn", + "react": "off", + "jsx-a11y": "off", + "nextjs": "off" + }, + "env": { + "node": true, + "es2022": true + }, + "globals": { + "globalThis": "readonly", + "console": "readonly" + }, + "ignore_patterns": [ + "node_modules/**", + "dist/**", + "build/**", + "coverage/**", + "*.min.js", + "*.d.ts", + "*.test.ts" + ] +} diff --git a/kms/auth-simple/README.md b/kms/auth-simple/README.md new file mode 100644 index 00000000..e13c9459 --- /dev/null +++ b/kms/auth-simple/README.md @@ -0,0 +1,204 @@ +# dstack auth-simple + +A config-based auth server for dstack KMS webhook authorization. Validates boot requests against a JSON configuration file. + +## When to Use + +| Auth Server | Use Case | +|-------------|----------| +| **auth-simple** | Production deployments with config-file-based whitelisting | +| auth-eth | Production deployments with on-chain governance | +| auth-mock | Development and testing only | + +## Installation + +```bash +bun install +``` + +## Configuration + +Create `auth-config.json` (see `auth-config.example.json`). + +For initial KMS deployment, you only need the OS image hash: + +```json +{ + "osImages": ["0x0b327bcd642788b0517de3ff46d31ebd3847b6c64ea40bacde268bb9f1c8ec83"], + "kms": { + "allowAnyDevice": true + }, + "apps": {} +} +``` + +Add more fields as you deploy Gateway and apps: + +```json +{ + "osImages": ["0x..."], + "gatewayAppId": "0x...", + "kms": { + "mrAggregated": [], + "devices": [], + "allowAnyDevice": true + }, + "apps": { + "0xYourAppId": { + "composeHashes": ["0xabc...", "0xdef..."], + "devices": [], + "allowAnyDevice": true + } + } +} +``` + +### Configuration Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `osImages` | Yes | Allowed OS image hashes (from `digest.txt`) | +| `gatewayAppId` | No | Gateway app ID (add after Gateway deployment) | +| `kms.mrAggregated` | No | Allowed KMS aggregated MR values | +| `kms.devices` | No | Allowed KMS device IDs | +| `kms.allowAnyDevice` | No | If true, skip device ID check for KMS | +| `apps..composeHashes` | No | Allowed compose hashes for this app | +| `apps..devices` | No | Allowed device IDs for this app | +| `apps..allowAnyDevice` | No | If true, skip device ID check for this app | + +### Getting Hash Values + +**OS Image Hash:** +```bash +# From meta-dstack build output +cat images/digest.txt +``` + +**Compose Hash:** +```bash +sha256sum .app-compose.json | awk '{print "0x"$1}' +``` + +## Usage + +### Development + +```bash +# Run with hot reload +bun run dev +``` + +### Production + +```bash +# Run directly +bun run start + +# Or build first +bun run build +``` + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | 3000 | Server port | +| `AUTH_CONFIG_PATH` | ./auth-config.json | Path to config file | + +## API Endpoints + +### GET / + +Health check and server info. + +**Response:** +```json +{ + "status": "ok", + "configPath": "./auth-config.json", + "gatewayAppId": "0x..." +} +``` + +### POST /bootAuth/app + +App boot authorization. + +**Request:** +```json +{ + "mrAggregated": "0x...", + "osImageHash": "0x...", + "appId": "0x...", + "composeHash": "0x...", + "instanceId": "0x...", + "deviceId": "0x...", + "tcbStatus": "UpToDate" +} +``` + +**Response:** +```json +{ + "isAllowed": true, + "reason": "", + "gatewayAppId": "0x..." +} +``` + +### POST /bootAuth/kms + +KMS boot authorization. + +**Request:** Same as `/bootAuth/app` + +**Response:** Same as `/bootAuth/app` + +## Validation Logic + +### KMS Boot Validation + +1. `tcbStatus` must be "UpToDate" +2. `osImageHash` must be in `osImages` array +3. `mrAggregated` must be in `kms.mrAggregated` (if non-empty) +4. `deviceId` must be in `kms.devices` (unless `allowAnyDevice` is true) + +### App Boot Validation + +1. `tcbStatus` must be "UpToDate" +2. `osImageHash` must be in `osImages` array +3. `appId` must exist in `apps` object +4. `composeHash` must be in app's `composeHashes` array +5. `deviceId` must be in app's `devices` (unless `allowAnyDevice` is true) + +## Hot Reload + +The config file is re-read on every request. No restart required after config changes. + +## Integration with KMS + +Configure KMS to use webhook auth pointing to this server: + +```toml +[core.auth_api] +type = "webhook" + +[core.auth_api.webhook] +url = "http://localhost:3000" +``` + +## Testing + +```bash +# Run tests +bun run test + +# Run once +bun run test:run +``` + +## See Also + +- [auth-eth](../auth-eth/) - On-chain governance auth server +- [auth-mock](../auth-mock/) - Development/testing auth server (always allows) +- [Deployment Guide](../../docs/deployment.md) - Full deployment instructions diff --git a/kms/auth-simple/auth-config.example.json b/kms/auth-simple/auth-config.example.json new file mode 100644 index 00000000..40b3d00d --- /dev/null +++ b/kms/auth-simple/auth-config.example.json @@ -0,0 +1,19 @@ +{ + "osImages": [ + "0x0b327bcd642788b0517de3ff46d31ebd3847b6c64ea40bacde268bb9f1c8ec83" + ], + "kms": { + "mrAggregated": [], + "devices": [], + "allowAnyDevice": true + }, + "apps": { + "0xYourAppId": { + "composeHashes": [ + "0xYourComposeHash" + ], + "devices": [], + "allowAnyDevice": true + } + } +} diff --git a/kms/auth-simple/bun.lock b/kms/auth-simple/bun.lock new file mode 100644 index 00000000..acd5f8a5 --- /dev/null +++ b/kms/auth-simple/bun.lock @@ -0,0 +1,352 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "auth-simple", + "dependencies": { + "@hono/zod-validator": "0.2.2", + "hono": "4.10.3", + "zod": "3.25.76", + }, + "devDependencies": { + "@types/bun": "1.2.18", + "@types/node": "22.10.8", + "@vitest/ui": "1.6.1", + "openapi-types": "12.1.3", + "oxlint": "0.9.10", + "typescript": "5.8.3", + "vitest": "1.6.1", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@hono/zod-validator": ["@hono/zod-validator@0.2.2", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-dSDxaPV70Py8wuIU2QNpoVEIOSzSXZ/6/B/h4xA7eOMz7+AarKTSGV8E6QwrdcCbBLkpqfJ4Q2TmBO0eP1tCBQ=="], + + "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.9.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eOXKZYq5bnCSgDefgM5bzAg+4Fc//Rc4yjgKN8iDWUARweCaChiQXb6TXX8MfEfs6qayEMy6yVj0pqoFz0B1aw=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.9.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-UeYICDvLUaUOcY+0ugZUEmBMRLP+x8iTgL7TeY6BlpGw2ahbtUOTbyIIRWtr/0O++TnjZ+v8TzhJ9crw6Ij6dg=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.9.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-0Zn+vqHhrZyufFBfq9WOgiIool0gCR14BLsdS+0Dwd9o+kNxPGA5q7erQFkiC4rpkxtfBHeD3iIKMMt7d29Kyw=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.9.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-tkQcWpYwF42bA/uRaV2iMFePHkBjTTgomOgeEaiw6XOSJX4nBEqGIIboqqLBWT4JnKCf/L+IG3y/e1MflhKByw=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.9.10", "", { "os": "linux", "cpu": "x64" }, "sha512-JHbkMUnibqaSMBvLHyqTL5cWxcGW+jw+Ppt2baLISpvo34a6fBR+PI7v/A92sEDWe0W1rPhypzCwA8mKpkQ3DA=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.9.10", "", { "os": "linux", "cpu": "x64" }, "sha512-aBBwN7bQzidwHwEXr7BAdVvMTLWstCy5gikerjLnGDeCSXX9r+o6+yUzTOqZvOo66E+XBgOJaVbY8rsL1MLE0g=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.9.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-LXDnk7vKHT3IY6G1jq0O7+XMhtcHOYuxLGIx4KP+4xS6vKgBY+Bsq4xV3AtmtKlvnXkP5FxHpfLmcEtm5AWysA=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@0.9.10", "", { "os": "win32", "cpu": "x64" }, "sha512-w5XRAV4bhgwenjjpGYZGglqzG9Wv/sI+cjQWJBQsvfDXsr2w4vOBXzt1j3/Z3EcSqf4KtkCa/IIuAhQyeShUbA=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.54.0", "", { "os": "android", "cpu": "arm" }, "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.54.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.54.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.54.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.54.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.54.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.54.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.54.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.54.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.54.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.54.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.54.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.54.0", "", { "os": "none", "cpu": "arm64" }, "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.54.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.54.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], + + "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/node": ["@types/node@22.10.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-rk+QvAEGsbX/ZPiiyel6hJHNUS9cnSbPWVaZLvE+Er3tLqQFzWMz9JOfWW7XUmKvRPfxJfbl3qYWve+RGXncFw=="], + + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], + + "@vitest/expect": ["@vitest/expect@1.6.1", "", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="], + + "@vitest/runner": ["@vitest/runner@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="], + + "@vitest/snapshot": ["@vitest/snapshot@1.6.1", "", { "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" } }, "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ=="], + + "@vitest/spy": ["@vitest/spy@1.6.1", "", { "dependencies": { "tinyspy": "^2.2.0" } }, "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw=="], + + "@vitest/ui": ["@vitest/ui@1.6.1", "", { "dependencies": { "@vitest/utils": "1.6.1", "fast-glob": "^3.3.2", "fflate": "^0.8.1", "flatted": "^3.2.9", "pathe": "^1.1.1", "picocolors": "^1.0.0", "sirv": "^2.0.4" }, "peerDependencies": { "vitest": "1.6.1" } }, "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg=="], + + "@vitest/utils": ["@vitest/utils@1.6.1", "", { "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" } }, "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], + + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "assertion-error": ["assertion-error@1.1.0", "", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], + + "check-error": ["check-error@1.0.3", "", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="], + + "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@4.1.4", "", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="], + + "diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-func-name": ["get-func-name@2.0.2", "", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "hono": ["hono@4.10.3", "", {}, "sha512-2LOYWUbnhdxdL8MNbNg9XZig6k+cZXm5IjHn2Aviv7honhBMOHb+jxrKIeJRZJRmn+htUCKhaicxwXuUDlchRA=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], + + "loupe": ["loupe@2.3.7", "", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "oxlint": ["oxlint@0.9.10", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.9.10", "@oxlint/darwin-x64": "0.9.10", "@oxlint/linux-arm64-gnu": "0.9.10", "@oxlint/linux-arm64-musl": "0.9.10", "@oxlint/linux-x64-gnu": "0.9.10", "@oxlint/linux-x64-musl": "0.9.10", "@oxlint/win32-arm64": "0.9.10", "@oxlint/win32-x64": "0.9.10" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-bKiiFN7Hnoaist/rditTRBXz+GXKYuLd53/NB7Q6zHB/bifELJarSoRLkAUGElIJKl4PSr3lTh1g6zehh+rX0g=="], + + "p-limit": ["p-limit@5.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sirv": ["sirv@2.0.4", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-literal": ["strip-literal@2.1.1", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinypool": ["tinypool@0.8.4", "", {}, "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ=="], + + "tinyspy": ["tinyspy@2.2.1", "", {}, "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "type-detect": ["type-detect@4.1.0", "", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@1.6.1", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA=="], + + "vitest": ["vitest@1.6.1", "", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + } +} diff --git a/kms/auth-simple/index.test.ts b/kms/auth-simple/index.test.ts new file mode 100644 index 00000000..cd931431 --- /dev/null +++ b/kms/auth-simple/index.test.ts @@ -0,0 +1,312 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { writeFileSync, unlinkSync, existsSync } from 'fs'; +import app from './index'; + +const TEST_CONFIG_PATH = './test-auth-config.json'; + +const baseBootInfo = { + mrAggregated: '0xabc123', + osImageHash: '0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a', + appId: '0xapp123', + composeHash: '0xcompose456', + instanceId: '0xinstance789', + deviceId: '0xdevice999', + tcbStatus: 'UpToDate', + advisoryIds: [], + mrSystem: '' +}; + +function writeTestConfig(config: object) { + writeFileSync(TEST_CONFIG_PATH, JSON.stringify(config, null, 2)); +} + +describe('auth-simple', () => { + beforeAll(() => { + process.env.AUTH_CONFIG_PATH = TEST_CONFIG_PATH; + }); + + afterAll(() => { + if (existsSync(TEST_CONFIG_PATH)) { + unlinkSync(TEST_CONFIG_PATH); + } + }); + + describe('GET /', () => { + it('returns health check info', async () => { + writeTestConfig({ gatewayAppId: '0xgateway' }); + + const res = await app.fetch(new Request('http://localhost/', { method: 'GET' })); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(json.status).toBe('ok'); + expect(json.gatewayAppId).toBe('0xgateway'); + }); + }); + + describe('POST /bootAuth/kms', () => { + it('allows KMS boot with valid config', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + kms: { + mrAggregated: ['0xabc123'], + devices: ['0xdevice999'], + allowAnyDevice: false + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/kms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(true); + expect(json.reason).toBe(''); + }); + + it('rejects KMS boot with invalid TCB status', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + kms: { allowAnyDevice: true } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/kms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...baseBootInfo, tcbStatus: 'OutOfDate' }) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(false); + expect(json.reason).toContain('TCB status'); + }); + + it('rejects KMS boot with invalid OS image', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0xdifferentimage'], + kms: { allowAnyDevice: true } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/kms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(false); + expect(json.reason).toContain('OS image'); + }); + + it('rejects KMS boot with invalid mrAggregated', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + kms: { + mrAggregated: ['0xdifferentmr'], + allowAnyDevice: true + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/kms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(false); + expect(json.reason).toContain('MR'); + }); + + it('allows KMS boot with allowAnyDevice', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + kms: { + mrAggregated: ['0xabc123'], + devices: ['0xotherdevice'], + allowAnyDevice: true + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/kms', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(true); + }); + }); + + describe('POST /bootAuth/app', () => { + it('allows app boot with valid config', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + apps: { + '0xapp123': { + composeHashes: ['0xcompose456'], + devices: ['0xdevice999'], + allowAnyDevice: false + } + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(true); + expect(json.reason).toBe(''); + }); + + it('rejects app boot with unregistered app', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + apps: {} + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(false); + expect(json.reason).toContain('not registered'); + }); + + it('rejects app boot with invalid compose hash', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + apps: { + '0xapp123': { + composeHashes: ['0xdifferenthash'], + allowAnyDevice: true + } + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(false); + expect(json.reason).toContain('compose hash'); + }); + + it('rejects app boot with invalid device', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + apps: { + '0xapp123': { + composeHashes: ['0xcompose456'], + devices: ['0xotherdevice'], + allowAnyDevice: false + } + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(false); + expect(json.reason).toContain('device'); + }); + + it('allows app boot with allowAnyDevice', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + apps: { + '0xapp123': { + composeHashes: ['0xcompose456'], + devices: [], + allowAnyDevice: true + } + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(true); + }); + }); + + describe('hex normalization', () => { + it('handles uppercase hex', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['0x1FBB0CF9CC6CFBF23D6B779776FABAD2C5403D643BADB9E5E238615E4960A78A'], + apps: { + '0xAPP123': { + composeHashes: ['0xCOMPOSE456'], + allowAnyDevice: true + } + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(true); + }); + + it('handles hex without 0x prefix', async () => { + writeTestConfig({ + gatewayAppId: '0xgateway', + osImages: ['1fbb0cf9cc6cfbf23d6b779776fabad2c5403d643badb9e5e238615e4960a78a'], + apps: { + 'app123': { + composeHashes: ['compose456'], + allowAnyDevice: true + } + } + }); + + const res = await app.fetch(new Request('http://localhost/bootAuth/app', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(baseBootInfo) + })); + const json = await res.json(); + + expect(json.isAllowed).toBe(true); + }); + }); +}); diff --git a/kms/auth-simple/index.ts b/kms/auth-simple/index.ts new file mode 100644 index 00000000..bda04b11 --- /dev/null +++ b/kms/auth-simple/index.ts @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { z } from 'zod'; +import { readFileSync, existsSync } from 'fs'; + +// zod schemas for validation - compatible with auth-eth implementation +const BootInfoSchema = z.object({ + mrAggregated: z.string().describe('aggregated MR measurement'), + osImageHash: z.string().describe('OS Image hash'), + appId: z.string().describe('application ID'), + composeHash: z.string().describe('compose hash'), + instanceId: z.string().describe('instance ID'), + deviceId: z.string().describe('device ID'), + tcbStatus: z.string().optional().default(''), + advisoryIds: z.array(z.string()).optional().default([]), + mrSystem: z.string().optional().default('') +}); + +const BootResponseSchema = z.object({ + isAllowed: z.boolean(), + reason: z.string(), + gatewayAppId: z.string() +}); + +// config schema +const AppConfigSchema = z.object({ + composeHashes: z.array(z.string()).default([]), + devices: z.array(z.string()).default([]), + allowAnyDevice: z.boolean().default(false) +}); + +const KmsConfigSchema = z.object({ + mrAggregated: z.array(z.string()).default([]), + devices: z.array(z.string()).default([]), + allowAnyDevice: z.boolean().default(false) +}); + +const AuthConfigSchema = z.object({ + gatewayAppId: z.string().default(''), + // KMS expects these fields in the health check response + kmsContractAddr: z.string().default('0x0000000000000000000000000000000000000000'), + chainId: z.number().default(0), + appImplementation: z.string().default('0x0000000000000000000000000000000000000000'), + osImages: z.array(z.string()).default([]), + kms: KmsConfigSchema.default({}), + apps: z.record(z.string(), AppConfigSchema).default({}) +}); + +type BootInfo = z.infer; +type BootResponse = z.infer; +type AuthConfig = z.infer; + +// normalize hex string to lowercase with 0x prefix +function normalizeHex(hex: string): string { + hex = hex.toLowerCase(); + if (!hex.startsWith('0x')) { + hex = '0x' + hex; + } + return hex; +} + +// config-based backend +class ConfigBackend { + private getConfigPath(): string { + return process.env.AUTH_CONFIG_PATH || './auth-config.json'; + } + + private loadConfig(): AuthConfig { + const configPath = this.getConfigPath(); + if (!existsSync(configPath)) { + console.error(`config file not found: ${configPath}`); + return AuthConfigSchema.parse({}); + } + + try { + const content = readFileSync(configPath, 'utf-8'); + const parsed = JSON.parse(content); + return AuthConfigSchema.parse(parsed); + } catch (error) { + console.error(`failed to load config: ${error}`); + return AuthConfigSchema.parse({}); + } + } + + async checkBoot(bootInfo: BootInfo, isKms: boolean): Promise { + const config = this.loadConfig(); + const osImageHash = normalizeHex(bootInfo.osImageHash); + const deviceId = normalizeHex(bootInfo.deviceId); + + // check TCB status + if (bootInfo.tcbStatus !== 'UpToDate') { + return { + isAllowed: false, + reason: 'TCB status is not up to date', + gatewayAppId: config.gatewayAppId + }; + } + + // check OS image + const allowedOsImages = config.osImages.map(normalizeHex); + if (!allowedOsImages.includes(osImageHash)) { + return { + isAllowed: false, + reason: 'OS image is not allowed', + gatewayAppId: config.gatewayAppId + }; + } + + if (isKms) { + return this.checkKmsBoot(bootInfo, config, deviceId); + } else { + return this.checkAppBoot(bootInfo, config, deviceId); + } + } + + private checkKmsBoot(bootInfo: BootInfo, config: AuthConfig, deviceId: string): BootResponse { + const mrAggregated = normalizeHex(bootInfo.mrAggregated); + + // check aggregated MR + const allowedMrs = config.kms.mrAggregated.map(normalizeHex); + if (allowedMrs.length > 0 && !allowedMrs.includes(mrAggregated)) { + return { + isAllowed: false, + reason: 'aggregated MR not allowed', + gatewayAppId: config.gatewayAppId + }; + } + + // check device ID + if (!config.kms.allowAnyDevice) { + const allowedDevices = config.kms.devices.map(normalizeHex); + if (allowedDevices.length > 0 && !allowedDevices.includes(deviceId)) { + return { + isAllowed: false, + reason: 'KMS is not allowed to boot on this device', + gatewayAppId: config.gatewayAppId + }; + } + } + + return { + isAllowed: true, + reason: '', + gatewayAppId: config.gatewayAppId + }; + } + + private checkAppBoot(bootInfo: BootInfo, config: AuthConfig, deviceId: string): BootResponse { + const appId = normalizeHex(bootInfo.appId); + const composeHash = normalizeHex(bootInfo.composeHash); + + // check app exists + const appConfig = Object.entries(config.apps).find( + ([id]) => normalizeHex(id) === appId + )?.[1]; + + if (!appConfig) { + return { + isAllowed: false, + reason: 'app not registered', + gatewayAppId: config.gatewayAppId + }; + } + + // check compose hash + const allowedHashes = appConfig.composeHashes.map(normalizeHex); + if (!allowedHashes.includes(composeHash)) { + return { + isAllowed: false, + reason: 'compose hash not allowed', + gatewayAppId: config.gatewayAppId + }; + } + + // check device ID + if (!appConfig.allowAnyDevice) { + const allowedDevices = appConfig.devices.map(normalizeHex); + if (allowedDevices.length > 0 && !allowedDevices.includes(deviceId)) { + return { + isAllowed: false, + reason: 'app is not allowed to boot on this device', + gatewayAppId: config.gatewayAppId + }; + } + } + + return { + isAllowed: true, + reason: '', + gatewayAppId: config.gatewayAppId + }; + } + + async getGatewayAppId(): Promise { + return this.loadConfig().gatewayAppId; + } + + async getInfo(): Promise<{ + gatewayAppId: string; + kmsContractAddr: string; + chainId: number; + appImplementation: string; + }> { + const config = this.loadConfig(); + return { + gatewayAppId: config.gatewayAppId, + kmsContractAddr: config.kmsContractAddr, + chainId: config.chainId, + appImplementation: config.appImplementation, + }; + } +} + +// initialize app +const app = new Hono(); + +// initialize backend +const backend = new ConfigBackend(); + +// health check and info endpoint +app.get('/', async (c) => { + try { + const info = await backend.getInfo(); + return c.json({ + status: 'ok', + kmsContractAddr: info.kmsContractAddr, + gatewayAppId: info.gatewayAppId, + chainId: info.chainId, + appAuthImplementation: info.appImplementation, // backward compat + appImplementation: info.appImplementation, + }); + } catch (error) { + console.error('error in health check:', error); + return c.json({ + status: 'error', + message: error instanceof Error ? error.message : String(error) + }, 500); + } +}); + +// app boot authentication +app.post('/bootAuth/app', + zValidator('json', BootInfoSchema), + async (c) => { + try { + const bootInfo = c.req.valid('json'); + console.log('app boot auth request:', { + appId: bootInfo.appId, + composeHash: bootInfo.composeHash, + instanceId: bootInfo.instanceId, + }); + + const result = await backend.checkBoot(bootInfo, false); + console.log('app boot auth result:', result); + return c.json(result); + } catch (error) { + console.error('error in app boot auth:', error); + return c.json({ + isAllowed: false, + gatewayAppId: '', + reason: error instanceof Error ? error.message : String(error) + }); + } + } +); + +// KMS boot authentication +app.post('/bootAuth/kms', + zValidator('json', BootInfoSchema), + async (c) => { + try { + const bootInfo = c.req.valid('json'); + console.log('KMS boot auth request:', { + osImageHash: bootInfo.osImageHash, + mrAggregated: bootInfo.mrAggregated, + instanceId: bootInfo.instanceId, + }); + + const result = await backend.checkBoot(bootInfo, true); + console.log('KMS boot auth result:', result); + return c.json(result); + } catch (error) { + console.error('error in KMS boot auth:', error); + return c.json({ + isAllowed: false, + gatewayAppId: '', + reason: error instanceof Error ? error.message : String(error) + }); + } + } +); + +// start server +const port = parseInt(process.env.PORT || '3000'); +const defaultConfigPath = process.env.AUTH_CONFIG_PATH || './auth-config.json'; +console.log(`starting auth-simple server on port ${port}`); +console.log(`config path: ${defaultConfigPath}`); + +export default { + port, + fetch: app.fetch, +}; diff --git a/kms/auth-simple/package.json b/kms/auth-simple/package.json new file mode 100644 index 00000000..d5d30ab8 --- /dev/null +++ b/kms/auth-simple/package.json @@ -0,0 +1,32 @@ +{ + "name": "auth-simple", + "version": "1.0.0", + "description": "dstack KMS config-based auth server with bun + hono + zod", + "main": "index.ts", + "scripts": { + "dev": "bun run --watch index.ts", + "start": "bun run index.ts", + "build": "bun build index.ts --outdir ./dist --target bun", + "test": "vitest", + "test:run": "vitest run", + "lint": "oxlint .", + "lint:fix": "oxlint --fix .", + "fmt": "oxlint --fix .", + "check": "bun run lint && bun run test:run" + }, + "dependencies": { + "hono": "4.10.3", + "@hono/zod-validator": "0.2.2", + "zod": "3.25.76" + }, + "devDependencies": { + "@types/bun": "1.2.18", + "@types/node": "22.10.8", + "@vitest/ui": "1.6.1", + "openapi-types": "12.1.3", + "oxlint": "0.9.10", + "typescript": "5.8.3", + "vitest": "1.6.1" + }, + "type": "module" +} diff --git a/kms/auth-simple/vitest.config.ts b/kms/auth-simple/vitest.config.ts new file mode 100644 index 00000000..17597dac --- /dev/null +++ b/kms/auth-simple/vitest.config.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}); diff --git a/kms/dstack-app/compose-simple.yaml b/kms/dstack-app/compose-simple.yaml new file mode 100644 index 00000000..f40e58d3 --- /dev/null +++ b/kms/dstack-app/compose-simple.yaml @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +# KMS deployment with external auth-simple webhook +# auth-simple runs outside the CVM (operator infrastructure) + +services: + kms: + image: ${KMS_IMAGE} + volumes: + - kms-volume:/kms + - /var/run/dstack.sock:/var/run/dstack.sock + ports: + - 8000:8000 + restart: unless-stopped + configs: + - source: kms_config + target: /kms/kms.toml + command: sh -c 'mkdir -p /kms/certs /kms/images && exec dstack-kms -c /kms/kms.toml' + +volumes: + kms-volume: + +configs: + kms_config: + content: | + [rpc] + address = "0.0.0.0" + port = 8000 + + [rpc.tls] + key = "/kms/certs/rpc.key" + certs = "/kms/certs/rpc.crt" + + [rpc.tls.mutual] + ca_certs = "/kms/certs/tmp-ca.crt" + mandatory = false + + [core] + cert_dir = "/kms/certs" + admin_token_hash = "${ADMIN_TOKEN_HASH}" + + [core.image] + verify = true + cache_dir = "/kms/images" + download_url = "${IMAGE_DOWNLOAD_URL}" + download_timeout = "2m" + + [core.auth_api] + type = "webhook" + + [core.auth_api.webhook] + url = "${AUTH_WEBHOOK_URL}" + + [core.onboard] + enabled = true + auto_bootstrap_domain = "" + quote_enabled = true + address = "0.0.0.0" + port = 8000 diff --git a/kms/dstack-app/deploy-simple.sh b/kms/dstack-app/deploy-simple.sh new file mode 100755 index 00000000..f027f24a --- /dev/null +++ b/kms/dstack-app/deploy-simple.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +# Deploy KMS with external auth-simple webhook +# auth-simple runs outside the CVM (operator infrastructure) + +set -e + +# Check if .env.simple exists +if [ -f ".env.simple" ]; then + echo "Loading environment variables from .env.simple file..." + set -a + source .env.simple + set +a +else + # Create a template .env.simple file + echo "Creating template .env.simple file..." + cat >.env.simple < [!NOTE] -> This feature isn't available in the simulator. We recommend sticking with `report_data` for most cases since it's simpler and safer to use. If you're not super familiar with SGX/TDX attestation quotes, it's best to avoid adding data directly into quotes as it could cause verification issues. - -Extend RTMR3 with custom events for audit trails: - -```typescript -// Emit custom events (requires dstack OS 0.5.0+) -await client.emitEvent('user-action', JSON.stringify({ - action: 'transfer', - amount: 1000, - timestamp: Date.now() -})); - -// Events are automatically included in subsequent quotes -const quote = await client.getQuote('audit-data'); -const events = JSON.parse(quote.event_log); -``` +## Core API -## Blockchain Integration +### Derive Keys -### Ethereum with Viem +`getKey()` derives deterministic keys bound to your application's identity (`app_id`). The same path always produces the same key for your app, but different apps get different keys even with the same path. ```typescript -import { toViemAccount, toViemAccountSecure } from '@phala/dstack-sdk/viem'; -import { createWalletClient, http } from 'viem'; -import { mainnet } from 'viem/chains'; - -const keyResult = await client.getKey('ethereum/main', 'wallet'); - -// Standard account creation -const account = toViemAccount(keyResult); - -// Enhanced security with SHA256 hashing (recommended) -const secureAccount = toViemAccountSecure(keyResult); - -const wallet = createWalletClient({ - account: secureAccount, - chain: mainnet, - transport: http() -}); +// Derive keys by path +const ethKey = await client.getKey('wallet/ethereum'); +const btcKey = await client.getKey('wallet/bitcoin'); -// Use wallet for transactions -const hash = await wallet.sendTransaction({ - to: '0x...', - value: parseEther('0.1') -}); +// Use path to separate keys +const mainnetKey = await client.getKey('wallet/eth/mainnet'); +const testnetKey = await client.getKey('wallet/eth/testnet'); ``` -### Solana - -```typescript -import { toKeypair, toKeypairSecure } from '@phala/dstack-sdk/solana'; -import { Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js'; - -const keyResult = await client.getKey('solana/main', 'wallet'); +**Parameters:** +- `path`: Key derivation path (determines the key) +- `purpose` (optional): Included in signature chain message, does not affect the derived key -// Standard keypair creation -const keypair = toKeypair(keyResult); +**Returns:** `GetKeyResponse` +- `key`: Hex-encoded private key +- `signature_chain`: Signatures proving the key was derived in a genuine TEE -// Enhanced security with SHA256 hashing (recommended) -const secureKeypair = toKeypairSecure(keyResult); +### Generate Attestation Quotes -const connection = new Connection('https://api.mainnet-beta.solana.com'); +`getQuote()` creates a TDX quote proving your code runs in a genuine TEE. -// Create and send transaction -const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: secureKeypair.publicKey, - toPubkey: new PublicKey('...'), - lamports: 1000000 - }) -); +```typescript +const quote = await client.getQuote('user:alice:nonce123'); -const signature = await connection.sendTransaction(transaction, [secureKeypair]); +// Replay RTMRs from the event log +const rtmrs = quote.replayRtmrs(); +console.log(rtmrs); ``` -## Environment Variables Encryption - -**Important**: This feature is specifically for **deployment-time security**, not runtime SDK operations. - -The SDK provides end-to-end encryption capabilities for securely transmitting sensitive environment variables during dstack application deployment. When deploying applications to TEE instances, sensitive configuration data (API keys, database credentials, private keys, etc.) needs to be securely transmitted from the deployment client to the TEE application. - -### Deployment Security Problem - -During application deployment, sensitive data must traverse: -1. **Client Environment** → Deployment infrastructure → **TEE Application** -2. **Risk**: Deployment infrastructure could potentially access plaintext secrets -3. **Solution**: Client-side encryption ensures only the target TEE application can decrypt secrets - -### How It Works - -1. **Pre-Deployment**: Client obtains encryption public key from KMS API -2. **Encryption**: Client encrypts environment variables using X25519 + AES-GCM -3. **Transmission**: Encrypted payload is sent through deployment infrastructure -4. **Decryption**: TEE application automatically decrypts and loads environment variables -5. **Runtime**: Application accesses secrets via standard `process.env` - -This ensures **true end-to-end encryption** where deployment infrastructure never sees plaintext secrets. +**Parameters:** +- `reportData`: Exactly 64 bytes recommended. If shorter, pad with zeros. If longer, hash it first (e.g., SHA-256). -### App Configuration for Encrypted Variables +**Returns:** `GetQuoteResponse` +- `quote`: Hex-encoded TDX quote +- `event_log`: JSON string of measured events +- `replayRtmrs()`: Method to compute RTMR values from event log -Your `app-compose.json` should specify which environment variables are allowed: +### Get Instance Info -```json -{ - "manifest_version": 1, - "name": "secure-app", - "runner": "docker-compose", - "docker_compose_file": "services:\n app:\n build: .\n volumes:\n - /var/run/dstack.sock:/var/run/dstack.sock\n environment:\n - API_KEY\n - DATABASE_URL\n - PRIVATE_KEY", - "allowed_envs": ["API_KEY", "DATABASE_URL", "PRIVATE_KEY"], - "kms_enabled": true -} +```typescript +const info = await client.info(); +console.log(info.app_id); +console.log(info.instance_id); +console.log(info.tcb_info); ``` -### Deployment Encryption Workflow +**Returns:** `InfoResponse` +- `app_id`: Application identifier +- `instance_id`: Instance identifier +- `app_name`: Application name +- `tcb_info`: TCB measurements (MRTD, RTMRs, event log) +- `compose_hash`: Hash of the app configuration +- `app_cert`: Application certificate (PEM) -```typescript -import { encryptEnvVars, verifyEnvEncryptPublicKey, type EnvVar } from '@phala/dstack-sdk'; +### Generate TLS Certificates -// 1. Define sensitive environment variables -const envVars: EnvVar[] = [ - { key: 'DATABASE_URL', value: 'postgresql://user:pass@host:5432/db' }, - { key: 'API_SECRET_KEY', value: 'your-secret-key' }, - { key: 'JWT_PRIVATE_KEY', value: '-----BEGIN PRIVATE KEY-----\n...' }, - { key: 'WALLET_MNEMONIC', value: 'abandon abandon abandon...' } -]; +`getTlsKey()` creates fresh TLS certificates. Unlike `getKey()`, each call generates a new random key. -// 2. Obtain encryption public key from KMS API (dstack-vmm or Phala Cloud) -const response = await fetch('/prpc/GetAppEnvEncryptPubKey?json', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ app_id: 'your-app-id-hex' }) +```typescript +const tls = await client.getTlsKey({ + subject: 'api.example.com', + altNames: ['localhost'], + usageRaTls: true // Embed attestation in certificate }); -const { public_key, signature } = await response.json(); - -// 3. Verify KMS API authenticity to prevent man-in-the-middle attacks -const publicKeyBytes = new Uint8Array(Buffer.from(public_key, 'hex')); -const signatureBytes = new Uint8Array(Buffer.from(signature, 'hex')); - -const trustedPubkey = verifyEnvEncryptPublicKey(publicKeyBytes, signatureBytes, 'your-app-id-hex'); -if (!trustedPubkey) { - throw new Error('KMS API provided untrusted encryption key'); -} - -console.log('Verified KMS public key:', trustedPubkey); -// 4. Encrypt environment variables for secure deployment -const encryptedData = await encryptEnvVars(envVars, public_key); -console.log('Encrypted payload:', encryptedData); - -// 5. Deploy with encrypted configuration -await deployDstackApp({ - app_id: 'your-app-id-hex', - encrypted_env: encryptedData, - // ... other deployment parameters -}); +console.log(tls.key); // PEM private key +console.log(tls.certificate_chain); // Certificate chain ``` -### Security Guarantees - -The environment encryption system provides several security guarantees: - -**End-to-End Encryption**: Environment variables are encrypted on the client side and can only be decrypted by the target dstack application inside the TEE. Even the deployment infrastructure cannot access the plaintext values. - -**KMS Authenticity Verification**: The `verifyEnvEncryptPublicKey` function validates that the encryption public key comes from a trusted KMS (Key Management Service), preventing man-in-the-middle attacks during key exchange. - -**Forward Secrecy**: Each encryption operation uses ephemeral X25519 keypairs, ensuring that compromising long-term keys cannot decrypt past communications. - -**Authenticated Encryption**: AES-256-GCM provides both confidentiality and integrity protection, detecting any tampering with encrypted data. - -### Encryption Protocol Details - -The encryption process follows this cryptographic protocol: - -1. **Key Exchange**: Generate ephemeral X25519 keypair and perform ECDH with KMS public key -2. **Shared Secret**: Derive AES-256 key from ECDH shared secret -3. **Authenticated Encryption**: Encrypt JSON payload using AES-256-GCM with random IV -4. **Payload Format**: `ephemeral_pubkey(32) + iv(12) + encrypted_data + auth_tag(16)` - -```typescript -// Detailed encryption example -const envVars = [{ key: 'SECRET', value: 'sensitive-data' }]; -const kmsPublicKey = '0xa1b2c3d4...'; // From trusted KMS API - -const encrypted = await encryptEnvVars(envVars, kmsPublicKey); -// encrypted = "a1b2c3..." (hex string) +**Parameters:** +- `subject` (optional): Certificate common name (e.g., domain name) +- `altNames` (optional): List of subject alternative names +- `usageRaTls` (optional): Embed TDX quote in certificate extension +- `usageServerAuth` (optional): Enable for server authentication (default: `true`) +- `usageClientAuth` (optional): Enable for client authentication (default: `false`) -// Inside dstack application, the encrypted data is automatically decrypted -// and made available as environment variables: -console.log(process.env.SECRET); // "sensitive-data" -``` +**Returns:** `GetTlsKeyResponse` +- `key`: PEM-encoded private key +- `certificate_chain`: List of PEM certificates -### KMS Public Key Verification +### Sign and Verify -The verification function ensures the encryption public key comes from a legitimate KMS: +Sign data using TEE-derived keys (not yet released): ```typescript -import { verifyEnvEncryptPublicKey } from '@phala/dstack-sdk'; - -/** - * Verify KMS-provided encryption public key authenticity - * - * @param publicKey - X25519 public key bytes (32 bytes) - * @param signature - secp256k1 signature from KMS (65 bytes) - * @param appId - Target application ID (hex string) - * @returns Compressed secp256k1 public key of KMS if valid, null if invalid - */ -const kmsIdentity = verifyEnvEncryptPublicKey(publicKeyBytes, signatureBytes, appId); +const result = await client.sign('ed25519', 'message to sign'); +console.log(result.signature); +console.log(result.public_key); -if (kmsIdentity) { - console.log('Trusted KMS identity:', kmsIdentity); - // Proceed with encryption using the verified public key -} else { - throw new Error('KMS signature verification failed - potential MITM attack'); -} +// Verify the signature +const valid = await client.verify('ed25519', 'message to sign', result.signature, result.public_key); +console.log(valid.valid); // true ``` -### Message Format for Verification +**`sign()` Parameters:** +- `algorithm`: `'ed25519'`, `'secp256k1'`, or `'secp256k1_prehashed'` +- `data`: Data to sign (string, Buffer, or Uint8Array) -The KMS signature covers the following message structure: -``` -message = "dstack-env-encrypt-pubkey" + ":" + app_id + public_key -signature = secp256k1.sign(keccak256(message), kms_private_key) -``` +**`sign()` Returns:** `SignResponse` +- `signature`: Hex-encoded signature +- `public_key`: Hex-encoded public key +- `signature_chain`: Signatures proving TEE origin -This binds the encryption key to a specific application ID and prevents key substitution attacks. +**`verify()` Parameters:** +- `algorithm`: Algorithm used for signing +- `data`: Original data +- `signature`: Signature to verify +- `public_key`: Public key to verify against -## Cryptographic Security +**`verify()` Returns:** `VerifyResponse` +- `valid`: Boolean indicating if signature is valid -### Key Derivation Security +### Emit Events -The SDK implements secure key derivation using: - -- **Deterministic Generation**: Keys are derived using HMAC-based Key Derivation Function (HKDF) -- **Application Isolation**: Each path produces unique keys, preventing cross-application access -- **Signature Verification**: All derived keys include cryptographic proof of origin -- **TEE Protection**: Master keys never leave the secure enclave +Extend RTMR3 with custom measurements for your application's boot sequence (requires dstack OS 0.5.0+). These measurements are append-only and become part of the attestation record. ```typescript -// Each path generates a unique, deterministic key -const wallet1 = await client.getKey('app1/wallet', 'ethereum'); -const wallet2 = await client.getKey('app2/wallet', 'ethereum'); -// wallet1.key !== wallet2.key (guaranteed different) - -const sameWallet = await client.getKey('app1/wallet', 'ethereum'); -// wallet1.key === sameWallet.key (guaranteed identical) +await client.emitEvent('config_loaded', 'production'); +await client.emitEvent('plugin_initialized', 'auth-v2'); ``` -### Remote Attestation +**Parameters:** +- `event`: Event name (string identifier) +- `payload`: Event value (string, Buffer, or Uint8Array) -TDX quotes provide cryptographic proof of: +## Blockchain Integration -- **Code Integrity**: Measurement of loaded application code -- **Data Integrity**: Inclusion of application-specific data in quote -- **Environment Authenticity**: Verification of TEE platform and configuration +### Ethereum with Viem ```typescript -const applicationState = JSON.stringify({ - version: '1.0.0', - config_hash: 'sha256:...', - timestamp: Date.now() -}); +import { toViemAccount } from '@phala/dstack-sdk/viem'; +import { createWalletClient, http } from 'viem'; +import { mainnet } from 'viem/chains'; -const quote = await client.getQuote(applicationState); +const key = await client.getKey('wallet/ethereum'); +const account = toViemAccount(key); -// Quote can be verified by external parties to confirm: -// 1. Application is running in genuine TEE -// 2. Application code matches expected measurements -// 3. Application state is authentic and unmodified +const wallet = createWalletClient({ + account, + chain: mainnet, + transport: http() +}); ``` -### Environment Encryption Protocol - -The encryption scheme uses: +### Solana -- **X25519 ECDH**: Elliptic curve key exchange for forward secrecy -- **AES-256-GCM**: Authenticated encryption with 256-bit keys -- **Ephemeral Keys**: New keypair generated for each encryption operation -- **Authenticated Data**: Prevents tampering and ensures integrity +```typescript +import { toKeypair } from '@phala/dstack-sdk/solana'; -## Development and Testing +const key = await client.getKey('wallet/solana'); +const keypair = toKeypair(key); +console.log(keypair.publicKey.toBase58()); +``` -### Local Development +## Development -For development without physical TDX hardware: +For local development without TDX hardware, use the simulator: ```bash -# Clone and build simulator git clone https://github.com/Dstack-TEE/dstack.git cd dstack/sdk/simulator ./build.sh ./dstack-simulator - -# Set environment variable -export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090 ``` -### Testing Connectivity - -```typescript -const client = new DstackClient(); - -// Check if dstack service is available -const isAvailable = await client.isReachable(); -if (!isAvailable) { - console.error('dstack service is not reachable'); - process.exit(1); -} -``` +Then set the endpoint: -## API Reference - -### DstackClient - -#### Constructor - -```typescript -new DstackClient(endpoint?: string) -``` - -**Parameters:** -- `endpoint` (optional): Connection endpoint - - Unix socket path (production): `/var/run/dstack.sock` - - HTTP/HTTPS URL (development): `http://localhost:8090` - - Environment variable: `DSTACK_SIMULATOR_ENDPOINT` - -**Production App Configuration:** - -The Docker Compose configuration is embedded in `app-compose.json`: - -```json -{ - "manifest_version": 1, - "name": "production-app", - "runner": "docker-compose", - "docker_compose_file": "services:\n app:\n image: your-app\n volumes:\n - /var/run/dstack.sock:/var/run/dstack.sock\n environment:\n - NODE_ENV=production", - "public_tcbinfo": true -} -``` - -**Important**: The `docker_compose_file` contains YAML content as a string, ensuring the volume binding for `/var/run/dstack.sock` is included. - -#### Methods - -##### `info(): Promise` - -Retrieves comprehensive information about the TEE instance. - -**Returns:** `InfoResponse` -- `app_id`: Unique application identifier -- `instance_id`: Unique instance identifier -- `app_name`: Application name from configuration -- `device_id`: TEE device identifier -- `tcb_info`: Trusted Computing Base information - - `mrtd`: Measurement of TEE domain - - `rtmr0-3`: Runtime Measurement Registers - - `event_log`: Boot and runtime events - - `os_image_hash`: Operating system measurement - - `compose_hash`: Application configuration hash -- `app_cert`: Application certificate in PEM format -- `key_provider_info`: Key management configuration - -##### `getKey(path: string, purpose?: string): Promise` - -Derives a deterministic secp256k1/K256 private key for blockchain and Web3 applications. This is the primary method for obtaining cryptographic keys for wallets, signing, and other deterministic key scenarios. - -**Parameters:** -- `path`: Unique identifier for key derivation (e.g., `"wallet/ethereum"`, `"signing/solana"`) -- `purpose` (optional): Additional context for key usage (default: `""`) - -**Returns:** `GetKeyResponse` -- `key`: 32-byte secp256k1 private key as `Uint8Array` (suitable for Ethereum, Bitcoin, Solana, etc.) -- `signature_chain`: Array of cryptographic signatures proving key authenticity - -**Key Characteristics:** -- **Deterministic**: Same path + purpose always generates identical key -- **Isolated**: Different paths produce cryptographically independent keys -- **Blockchain-Ready**: Compatible with secp256k1 curve (Ethereum, Bitcoin, Solana) -- **Verifiable**: Signature chain proves key was derived inside genuine TEE - -**Use Cases:** -- Cryptocurrency wallets -- Transaction signing -- DeFi protocol interactions -- NFT operations -- Any scenario requiring consistent, reproducible keys - -```typescript -// Examples of deterministic key derivation -const ethWallet = await client.getKey('wallet/ethereum', 'mainnet'); -const btcWallet = await client.getKey('wallet/bitcoin', 'mainnet'); -const solWallet = await client.getKey('wallet/solana', 'mainnet'); - -// Same path always returns same key -const key1 = await client.getKey('my-app/signing'); -const key2 = await client.getKey('my-app/signing'); -// key1.key === key2.key (guaranteed identical) - -// Different paths return different keys -const userA = await client.getKey('user/alice/wallet'); -const userB = await client.getKey('user/bob/wallet'); -// userA.key !== userB.key (guaranteed different) +```bash +export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090 ``` -##### `getQuote(reportData: string | Buffer | Uint8Array): Promise` - -Generates a TDX attestation quote containing the provided report data. - -**Parameters:** -- `reportData`: Data to include in quote (max 64 bytes) - -**Returns:** `GetQuoteResponse` -- `quote`: TDX quote as hex string -- `event_log`: JSON string of system events -- `replayRtmrs()`: Function returning computed RTMR values - -**Use Cases:** -- Remote attestation of application state -- Cryptographic proof of execution environment -- Audit trail generation - -##### `getTlsKey(options?: TlsKeyOptions): Promise` - -Generates a fresh, random TLS key pair with X.509 certificate for TLS/SSL connections. **Important**: This method generates different keys on each call - use `getKey()` for deterministic keys. - -**Parameters:** `TlsKeyOptions` -- `path` (optional): Path parameter (unused in current implementation) -- `subject` (optional): Certificate subject (Common Name) - typically the domain name (default: `""`) -- `altNames` (optional): Subject Alternative Names - additional domains/IPs for the certificate (default: `[]`) -- `usageRaTls` (optional): Include TDX attestation quote in certificate extension for remote verification (default: `false`) -- `usageServerAuth` (optional): Enable server authentication - allows certificate to authenticate servers (default: `true`) -- `usageClientAuth` (optional): Enable client authentication - allows certificate to authenticate clients (default: `false`) - -**Returns:** `GetTlsKeyResponse` -- `key`: Private key in PEM format (X.509/PKCS#8) -- `certificate_chain`: Certificate chain array - -**Key Characteristics:** -- **Random Generation**: Each call produces a completely different key -- **TLS-Optimized**: Keys and certificates designed for TLS/SSL scenarios -- **RA-TLS Support**: Optional remote attestation extension in certificates -- **TEE-Signed**: Certificates signed by TEE-resident Certificate Authority +--- -**Certificate Usage Scenarios:** +## Deployment Utilities -1. **Standard HTTPS Server** (`usageServerAuth: true`, `usageClientAuth: false`) - - Web servers, API endpoints - - Server authenticates to clients - - Most common TLS use case +These utilities are for deployment scripts, not runtime SDK operations. -2. **Remote Attestation Server** (`usageRaTls: true`) - - TEE-based services requiring proof of execution environment - - Clients can verify the server runs in genuine TEE - - Combines TLS with hardware attestation +### Encrypt Environment Variables -3. **mTLS Client Certificate** (`usageServerAuth: false`, `usageClientAuth: true`) - - Client authentication in mutual TLS - - API clients, service-to-service communication - - Client proves identity to server - -4. **Dual-Purpose Certificate** (`usageServerAuth: true`, `usageClientAuth: true`) - - Services that act as both client and server - - Microservices architectures - - Maximum flexibility for TLS roles +Encrypt secrets before deploying to dstack: ```typescript -// Example 1: Standard HTTPS server certificate -const serverCert = await client.getTlsKey({ - subject: 'api.example.com', - altNames: ['api.example.com', 'www.api.example.com', '10.0.0.1'] - // usageServerAuth: true (default) - allows server authentication - // usageClientAuth: false (default) - no client authentication -}); - -// Example 2: Certificate with remote attestation (RA-TLS) -const attestedCert = await client.getTlsKey({ - subject: 'secure-api.example.com', - usageRaTls: true // Include TDX quote for remote verification - // Clients can verify the TEE environment through the certificate -}); - -// Example 3: Mutual TLS (mTLS) certificate for client authentication -const clientCert = await client.getTlsKey({ - subject: 'client.example.com', - usageServerAuth: false, // This certificate won't authenticate servers - usageClientAuth: true // Enable client authentication -}); +import { encryptEnvVars, verifyEnvEncryptPublicKey, type EnvVar } from '@phala/dstack-sdk'; -// Example 4: Certificate for both server and client authentication -const dualUseCert = await client.getTlsKey({ - subject: 'dual.example.com', - usageServerAuth: true, // Can authenticate as server - usageClientAuth: true // Can authenticate as client -}); +// Get and verify the KMS public key +// (obtain public_key and signature from KMS API) +const kmsIdentity = verifyEnvEncryptPublicKey(publicKeyBytes, signatureBytes, appId); +if (!kmsIdentity) { + throw new Error('Invalid KMS key'); +} -// ⚠️ Each call generates different keys (unlike getKey) -const cert1 = await client.getTlsKey(); -const cert2 = await client.getTlsKey(); -// cert1.key !== cert2.key (always different) - -// Use with Node.js HTTPS server -import https from 'https'; -const server = https.createServer({ - key: serverCert.key, - cert: serverCert.certificate_chain.join('\n') -}, app); +// Encrypt variables +const envVars: EnvVar[] = [ + { key: 'DATABASE_URL', value: 'postgresql://...' }, + { key: 'API_KEY', value: 'secret' } +]; +const encrypted = await encryptEnvVars(envVars, publicKey); ``` -##### `emitEvent(event: string, payload: string | Buffer | Uint8Array): Promise` - -Extends RTMR3 with a custom event for audit logging. - -**Parameters:** -- `event`: Event identifier string -- `payload`: Event data - -**Requirements:** -- dstack OS version 0.5.0 or later -- Events are permanently recorded in TEE measurements - -##### `isReachable(): Promise` - -Tests connectivity to the dstack service. - -**Returns:** `boolean` indicating service availability - -## Utility Functions - -### Compose Hash Calculation +### Calculate Compose Hash ```typescript import { getComposeHash } from '@phala/dstack-sdk'; -const appCompose = { - manifest_version: 1, - name: 'my-app', - runner: 'docker-compose', - docker_compose_file: 'docker-compose.yml' -}; - -const hash = getComposeHash(appCompose); -console.log('Configuration hash:', hash); -``` - -### KMS Public Key Verification - -Verify the authenticity of encryption public keys provided by KMS APIs: - -```typescript -import { verifyEnvEncryptPublicKey } from '@phala/dstack-sdk'; - -// Example: Verify KMS-provided encryption key -const publicKey = Buffer.from('e33a1832c6562067ff8f844a61e51ad051f1180b66ec2551fb0251735f3ee90a', 'hex'); -const signature = Buffer.from('8542c49081fbf4e03f62034f13fbf70630bdf256a53032e38465a27c36fd6bed7a5e7111652004aef37f7fd92fbfc1285212c4ae6a6154203a48f5e16cad2cef00', 'hex'); -const appId = '0000000000000000000000000000000000000000'; - -const kmsIdentity = verifyEnvEncryptPublicKey( - new Uint8Array(publicKey), - new Uint8Array(signature), - appId -); - -if (kmsIdentity) { - console.log('Trusted KMS identity:', kmsIdentity); - // Safe to use the public key for encryption -} else { - console.error('KMS signature verification failed'); - // Potential man-in-the-middle attack -} +const hash = getComposeHash(appComposeObject); ``` -## Security Best Practices - -1. **Key Management** - - Use descriptive, unique paths for key derivation - - Never expose derived keys outside the TEE - - Implement proper access controls in your application - -2. **Remote Attestation** - - Always verify quotes before trusting remote TEE instances - - Include application-specific data in quote generation - - Validate RTMR measurements against expected values - -3. **TLS Configuration** - - Enable RA-TLS for attestation-based authentication - - Use appropriate certificate validity periods - - Implement proper certificate validation - -4. **Error Handling** - - Handle cryptographic operation failures gracefully - - Log security events for monitoring - - Implement fallback mechanisms where appropriate - -## Migration Guide - -### Critical API Changes: Understanding the Separation +--- -The legacy `deriveKey()` method mixed two different use cases that have now been properly separated: +## Migration from TappdClient -1. **`getKey()`**: Deterministic key derivation for Web3/blockchain (secp256k1) -2. **`getTlsKey()`**: Random TLS certificate generation for HTTPS/SSL - -### Method Comparison Table - -| Feature | `getKey()` | `getTlsKey()` | -|---------|------------|---------------| -| **Purpose** | Web3/Blockchain keys | TLS/SSL certificates | -| **Key Generation** | Deterministic (same input = same key) | Random (different every call) | -| **Key Format** | Raw 32-byte secp256k1 private key | PEM-formatted X.509 private key | -| **Use Cases** | Wallets, signing, DeFi, NFT | HTTPS servers, mTLS, secure APIs | -| **Algorithm/Curve** | ECDSA/secp256k1 (k256) | ECDSA/NIST P-256 | -| **Returns** | `{ key: Uint8Array, signature_chain }` | `{ key: string (PEM), certificate_chain }` | -| **Reproducible** | ✅ Yes (same path = same key) | ❌ No (random each time) | -| **Blockchain Ready** | ✅ Yes (Ethereum, Bitcoin, Solana) | ❌ No (TLS-specific format) | -| **Certificate** | ❌ No certificate | ✅ Yes (X.509 certificate chain) | -| **RA-TLS Support** | ❌ No | ✅ Yes (optional) | - -### When to Use Which Method - -**Use `getKey()` when you need:** -- Cryptocurrency wallets -- Transaction signing -- Consistent keys across app restarts -- Web3 integrations (Ethereum, Solana, etc.) -- Any deterministic cryptographic operations - -**Use `getTlsKey()` when you need:** -- HTTPS server certificates -- Client authentication certificates -- Fresh keys for each connection -- Standard TLS/SSL setups -- Remote attestation via RA-TLS - -### From TappdClient to DstackClient - -**⚠️ BREAKING CHANGE**: `TappdClient` is deprecated and will be removed. All users must migrate to `DstackClient`. - -### Complete Migration Reference - -| Component | TappdClient (Old) | DstackClient (New) | Status | -|-----------|-------------------|-------------------|---------| -| **Socket Path** | `/var/run/tappd.sock` | `/var/run/dstack.sock` | ✅ Updated | -| **HTTP URL Format** | `http://localhost/prpc/Tappd.` | `http://localhost/` | ✅ Simplified | -| **K256 Key Method** | `DeriveK256Key(...)` | `GetKey(...)` | ✅ Renamed | -| **TLS Certificate Method** | `DeriveKey(...)` | `GetTlsKey(...)` | ✅ Separated | -| **TDX Quote (Hash)** | `TdxQuote(...)` | ❌ **Removed** | ⚠️ No longer supported | -| **TDX Quote (Raw)** | `RawQuote(...)` | `GetQuote(report_data)` | ✅ Renamed | - -#### For Web3/Blockchain Applications (Most Common Use Case) - -```typescript -// ❌ OLD - TappdClient.deriveKey() (DEPRECATED) -import { TappdClient } from '@phala/dstack-sdk'; -const client = new TappdClient(); -const result = await client.deriveKey('wallet/ethereum', 'mainnet'); -// This actually returned deterministic keys (confusing!) - -// ✅ NEW - DstackClient.getKey() (RECOMMENDED) -import { DstackClient } from '@phala/dstack-sdk'; -const client = new DstackClient(); -const result = await client.getKey('wallet/ethereum', 'mainnet'); -// Clear intent: deterministic key for blockchain usage -``` - -#### For TLS/SSL Certificate Generation - -```typescript -// ❌ OLD - TappdClient.deriveKey() with TLS options (CONFUSING) -import { TappdClient } from '@phala/dstack-sdk'; -const client = new TappdClient(); -const result = await client.deriveKey('server', 'api.example.com', ['localhost']); -// Mixed deterministic keys with TLS concepts - -// ✅ NEW - DstackClient.getTlsKey() (CLEAR PURPOSE) -import { DstackClient } from '@phala/dstack-sdk'; -const client = new DstackClient(); -const result = await client.getTlsKey({ - subject: 'api.example.com', - altNames: ['localhost'] -}); -// Clear intent: random TLS certificate generation -``` - -### Migration Steps - -#### Step 1: Update Imports and Client +Replace `TappdClient` with `DstackClient`: ```typescript // Before import { TappdClient } from '@phala/dstack-sdk'; const client = new TappdClient(); -// After -import { DstackClient } from '@phala/dstack-sdk'; -const client = new DstackClient(); -``` - -#### Step 2: Choose the Right Method - -**For Web3/Blockchain (95% of use cases):** -```typescript -// Before: deriveKey() for wallet/signing -const walletKey = await client.deriveKey('wallet', 'ethereum'); - -// After: getKey() for deterministic keys -const walletKey = await client.getKey('wallet', 'ethereum'); -``` - -**For TLS/SSL Certificates:** -```typescript -// Before: deriveKey() with subject/altNames -const tlsCert = await client.deriveKey('api', 'example.com', ['localhost']); - -// After: getTlsKey() with proper options -const tlsCert = await client.getTlsKey({ - subject: 'example.com', - altNames: ['localhost'] -}); -``` - -#### Step 3: Update Blockchain Integrations - -```typescript -// Viem (Ethereum) -import { toViemAccountSecure } from '@phala/dstack-sdk/viem'; - -// Before -const keyResult = await tappdClient.deriveKey('wallet'); -const account = toViemAccount(keyResult); // Basic security - -// After -const keyResult = await dstackClient.getKey('wallet', 'ethereum'); -const account = toViemAccountSecure(keyResult); // Enhanced security - -// Solana -import { toKeypairSecure } from '@phala/dstack-sdk/solana'; - -// Before -const keyResult = await tappdClient.deriveKey('wallet'); -const keypair = toKeypair(keyResult); // Basic security - // After -const keyResult = await dstackClient.getKey('wallet', 'solana'); -const keypair = toKeypairSecure(keyResult); // Enhanced security -``` - -### Why This Migration is Important - -1. **API Clarity**: Separate methods for separate purposes eliminates confusion -2. **Critical Security Fixes**: Legacy integration functions have security vulnerabilities -3. **Feature Deprecation**: Hash-based TDX quotes are no longer supported -4. **Future Compatibility**: `TappdClient` will be removed in future versions -5. **Better Type Safety**: Clearer interfaces and return types - -### Critical Security Issues - -**⚠️ SECURITY VULNERABILITY**: Legacy blockchain integration functions have security flaws: - -- **`toViemAccount()`**: Uses raw key material without proper hashing - **VULNERABLE** -- **`toKeypair()`**: Uses raw key material without proper hashing - **VULNERABLE** - -**✅ SECURE ALTERNATIVES**: Always use the secure versions: -- **`toViemAccountSecure()`**: Applies SHA256 hashing for enhanced security -- **`toKeypairSecure()`**: Applies SHA256 hashing for enhanced security - -### Compatibility Notes - -- **`TappdClient`** remains functional for now but shows deprecation warnings -- **`DstackClient.deriveKey()`** throws an error - forcing migration to correct methods -- **Legacy integration functions** work but have security vulnerabilities - **MUST MIGRATE** - -### Infrastructure Changes - -**Docker Volume Binding:** -```yaml -# Old (dstack OS 0.3.x) -volumes: - - /var/run/tappd.sock:/var/run/tappd.sock - -# New (dstack OS 0.5.x) -volumes: - - /var/run/dstack.sock:/var/run/dstack.sock -``` - -**Environment Variables:** -```bash -# Old -TAPPD_SIMULATOR_ENDPOINT=http://localhost:8090 - -# New -DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090 -``` - -### API Method Changes - -**⚠️ MAJOR BREAKING CHANGE - TDX Quote Methods:** - -```typescript -// ❌ OLD - TappdClient with hash algorithms (REMOVED) -const client = new TappdClient(); -await client.tdxQuote('user-data', 'sha256'); // Hash applied automatically -await client.tdxQuote('user-data', 'keccak256'); // Multiple algorithms supported -await client.tdxQuote('user-data', 'raw'); // Raw mode - -// ✅ NEW - DstackClient raw data only (BREAKING CHANGE) +import { DstackClient } from '@phala/dstack-sdk'; const client = new DstackClient(); -await client.getQuote('user-data'); // Raw data only, max 64 bytes -``` - -**Critical Changes:** -1. **No Hash Algorithms**: `getQuote()` only accepts raw data, no automatic hashing -2. **Size Limit Change**: Report data must be ≤ 64 bytes (was flexible with hashing) -3. **Manual Hashing**: If you need hashing, do it manually before calling `getQuote()` - -**Migration Example:** -```typescript -// Old approach with automatic hashing -const result = await tappdClient.tdxQuote('my-application-data', 'sha256'); - -// New approach - manual hashing if needed -import { createHash } from 'crypto'; -const hash = createHash('sha256').update('my-application-data').digest(); -const result = await dstackClient.getQuote(hash); // hash is 32 bytes -``` - -**Key Derivation Methods:** -```typescript -// Old internal method names (handled automatically by SDK) -// DeriveK256Key -> GetKey -// DeriveKey -> GetTlsKey -// RawQuote -> GetQuote ``` -### Migration Checklist - -- [ ] **Infrastructure Updates:** - - [ ] Update Docker volume binding to `/var/run/dstack.sock` - - [ ] Change environment variables from `TAPPD_*` to `DSTACK_*` - -- [ ] **Client Code Updates:** - - [ ] Replace `TappdClient` with `DstackClient` - - [ ] Replace `deriveKey()` calls with appropriate method: - - [ ] `getKey()` for Web3/blockchain keys (deterministic) - - [ ] `getTlsKey()` for TLS certificates (random) - - [ ] **CRITICAL**: Migrate TDX quote methods: - - [ ] Replace all `tdxQuote()` calls with `getQuote()` - - [ ] Remove hash algorithm parameters (`'sha256'`, `'keccak256'`, etc.) - - [ ] Add manual hashing if needed (data must be ≤ 64 bytes) - - [ ] Test with smaller data or pre-hash large data - - [ ] **SECURITY CRITICAL**: Update blockchain integration functions: - - [ ] Replace `toViemAccount()` with `toViemAccountSecure()` (Ethereum) - - [ ] Replace `toKeypair()` with `toKeypairSecure()` (Solana) - - [ ] **DO NOT** use legacy functions - they have security vulnerabilities - -- [ ] **Testing:** - - [ ] Test that deterministic keys still work as expected - - [ ] Verify TLS certificate generation works - - [ ] **CRITICAL**: Test quote generation changes: - - [ ] Verify raw data fits in 64 bytes - - [ ] Test manual hashing if using large data - - [ ] Confirm quotes work without algorithm parameters - - [ ] **SECURITY**: Validate blockchain integrations: - - [ ] Test Ethereum with `toViemAccountSecure()` only - - [ ] Test Solana with `toKeypairSecure()` only - - [ ] Verify legacy functions are completely removed from codebase +Method changes: +- `deriveKey()` → `getTlsKey()` for TLS certificates +- `tdxQuote()` → `getQuote()` (raw data only, no hash algorithms) +- Socket path: `/var/run/tappd.sock` → `/var/run/dstack.sock` ## License diff --git a/sdk/js/package.json b/sdk/js/package.json index 9cb6d3d8..01946b89 100644 --- a/sdk/js/package.json +++ b/sdk/js/package.json @@ -87,7 +87,11 @@ "test:ci": "vitest --run", "release": "npm run clean && npm run build && npm publish --access public" }, - "keywords": ["sdk", "dstack", "Phala"], + "keywords": [ + "sdk", + "dstack", + "Phala" + ], "author": "Leechael Yim", "license": "Apache-2.0", "dependencies": { @@ -99,15 +103,13 @@ "vitest": "^3.2.4" }, "optionalDependencies": { - "viem": "^2.21.0 <3.0.0", "@noble/curves": "^1.8.1", - "@solana/web3.js": "^1.98.0" + "@solana/web3.js": "^1.98.4", + "viem": "^2.43.3" }, "peerDependencies": { - "viem": "^2.21.0 <3.0.0", "@noble/curves": "^1.8.1", - "@noble/hashes": "^1.6.1", - "@solana/web3.js": "^1.98.0" + "@noble/hashes": "^1.6.1" }, "peerDependenciesMeta": { "viem": { diff --git a/sdk/python/README.md b/sdk/python/README.md index 8b6d521a..78a3c4f1 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -1,6 +1,6 @@ -# dstack SDK +# dstack SDK for Python -The dstack SDK provides a Python client for secure communication with the dstack Trusted Execution Environment (TEE). This SDK enables applications to derive cryptographic keys, generate remote attestation quotes, and perform other security-critical operations within confidential computing environments. +Access TEE features from your Python application running inside dstack. Derive deterministic keys, generate attestation quotes, create TLS certificates, and sign data—all backed by hardware security. ## Installation @@ -8,833 +8,288 @@ The dstack SDK provides a Python client for secure communication with the dstack pip install dstack-sdk ``` -## Overview - -The dstack SDK enables secure communication with dstack Trusted Execution Environment (TEE) instances. dstack applications are defined using `app-compose.json` (based on the `AppCompose` structure) and deployed as containerized applications using Docker Compose. - -### Client Types - -The Python SDK provides both synchronous and asynchronous clients to accommodate different programming patterns: - -#### Synchronous Clients -- **`DstackClient`**: Main synchronous client for dstack services -- **`TappdClient`**: Deprecated synchronous client (use `DstackClient` instead) - -#### Asynchronous Clients -- **`AsyncDstackClient`**: Async/await compatible client for dstack services -- **`AsyncTappdClient`**: Deprecated async client (use `AsyncDstackClient` instead) - -**Key Differences:** -- **Synchronous clients** use regular method calls and block until completion -- **Asynchronous clients** use `async`/`await` syntax and allow concurrent operations -- **API compatibility**: Both client types provide identical methods and functionality -- **Performance**: Async clients excel in I/O-bound applications with multiple concurrent requests - -### Application Architecture - -dstack applications consist of: -- **App Configuration**: `app-compose.json` defining app metadata, security settings, and Docker Compose content -- **Container Deployment**: Docker Compose configuration embedded within the app definition -- **TEE Integration**: Access to TEE functionality via Unix socket (`/var/run/dstack.sock`) - -### SDK Capabilities - -- **Key Derivation**: Deterministic secp256k1 key generation for blockchain and Web3 applications -- **Remote Attestation**: TDX quote generation providing cryptographic proof of execution environment -- **TLS Certificate Management**: Fresh certificate generation with optional RA-TLS support for secure connections -- **Deployment Security**: Client-side encryption of sensitive environment variables ensuring secrets are only accessible to target TEE applications -- **Blockchain Integration**: Ready-to-use adapters for Ethereum and Solana ecosystems - -### Socket Connection Requirements - -To use the SDK, your Docker Compose configuration must bind-mount the dstack socket: - -```yaml -# docker-compose.yml -services: - your-app: - image: your-app-image - volumes: - - /var/run/dstack.sock:/var/run/dstack.sock # dstack OS 0.5.x - # For dstack OS 0.3.x compatibility (deprecated): - # - /var/run/tappd.sock:/var/run/tappd.sock -``` - -## Basic Usage - -### Application Setup - -First, ensure your dstack application is properly configured: - -**1. App Configuration (`app-compose.json`)** -```json -{ - "manifest_version": 1, - "name": "my-secure-app", - "runner": "docker-compose", - "docker_compose_file": "services:\\n app:\\n build: .\\n volumes:\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n environment:\\n - NODE_ENV=production", - "public_tcbinfo": true, - "kms_enabled": false, - "gateway_enabled": false -} -``` - -**Note**: The `docker_compose_file` field contains the actual Docker Compose YAML content as a string, not a file path. - -### Synchronous Client Usage +## Quick Start ```python -import json -import time from dstack_sdk import DstackClient -# Create synchronous client - automatically connects to /var/run/dstack.sock client = DstackClient() -# For local development with simulator -dev_client = DstackClient('http://localhost:8090') +# Derive a deterministic key for your wallet +key = client.get_key('wallet/eth') +print(key.key) # Same path always returns the same key -# Get TEE instance information -info = client.info() -print('App ID:', info.app_id) -print('Instance ID:', info.instance_id) -print('App Name:', info.app_name) -print('TCB Info:', info.tcb_info) - -# Derive deterministic keys for blockchain applications -wallet_key = client.get_key('wallet/ethereum', 'mainnet') -print('Derived key (32 bytes):', wallet_key.decode_key()) # secp256k1 private key bytes -print('Signature chain:', wallet_key.signature_chain) # Authenticity proof - -# Generate remote attestation quote -application_data = json.dumps({ - "version": "1.0.0", - "timestamp": time.time(), - "user_id": "alice" -}) - -quote = client.get_quote(application_data.encode()) -print('TDX Quote:', quote.quote) -print('Event Log:', quote.event_log) - -# Verify measurement registers -rtmrs = quote.replay_rtmrs() -print('RTMR0-3:', rtmrs) +# Generate an attestation quote +quote = client.get_quote(b'my-app-state') +print(quote.quote) ``` -### Asynchronous Client Usage +The client automatically connects to `/var/run/dstack.sock`. For local development with the simulator, pass the endpoint explicitly: ```python -import json -import time -import asyncio -from dstack_sdk import AsyncDstackClient - -async def main(): - # Create asynchronous client - async_client = AsyncDstackClient() - - # All methods must be awaited - info = await async_client.info() - print('App ID:', info.app_id) - - # Derive keys asynchronously - wallet_key = await async_client.get_key('wallet/ethereum', 'mainnet') - print('Derived key:', wallet_key.decode_key().hex()) - - # Generate quote asynchronously - quote = await async_client.get_quote(b'async-test-data') - print('Quote length:', len(quote.quote)) - - # Multiple concurrent operations - tasks = [ - async_client.get_key('wallet/btc', 'mainnet'), - async_client.get_key('wallet/eth', 'mainnet'), - async_client.get_key('signing/key', 'production') - ] - - # Execute concurrently - keys = await asyncio.gather(*tasks) - for i, key in enumerate(keys): - print(f'Key {i}: {key.decode_key().hex()[:16]}...') - -# Run async code -if __name__ == "__main__": - asyncio.run(main()) +client = DstackClient('http://localhost:8090') ``` -### Choosing Between Sync and Async +## Core API -**Use Synchronous Client (`DstackClient`) when:** -- Building simple scripts or CLI tools -- Making sequential API calls -- Working in synchronous codebases -- Learning or prototyping +### Derive Keys -**Use Asynchronous Client (`AsyncDstackClient`) when:** -- Building web applications (FastAPI, Starlette, etc.) -- Needing concurrent TEE operations -- Integrating with async frameworks -- Optimizing I/O-bound applications +`get_key()` derives deterministic keys bound to your application's identity (`app_id`). The same path always produces the same key for your app, but different apps get different keys even with the same path. ```python -# Example: Concurrent key derivation (async advantage) -async def derive_multiple_keys(): - client = AsyncDstackClient() - - # This is much faster than sequential sync calls - keys = await asyncio.gather( - client.get_key('user/alice', 'eth'), - client.get_key('user/bob', 'eth'), - client.get_key('user/charlie', 'eth') - ) - return keys -``` +# Derive keys by path +eth_key = client.get_key('wallet/ethereum') +btc_key = client.get_key('wallet/bitcoin') -### Version Compatibility +# Use path to separate keys +mainnet_key = client.get_key('wallet/eth/mainnet') +testnet_key = client.get_key('wallet/eth/testnet') -- **dstack OS 0.5.x**: Use `/var/run/dstack.sock` (current) -- **dstack OS 0.3.x**: Use `/var/run/tappd.sock` (deprecated but supported) +# Different algorithm +ed_key = client.get_key('signing/key', algorithm='ed25519') +``` -The SDK automatically detects the correct socket path, but you must ensure the appropriate volume binding in your Docker Compose configuration. +**Parameters:** +- `path`: Key derivation path (determines the key) +- `purpose` (optional): Included in signature chain message, does not affect the derived key +- `algorithm` (optional): `'secp256k1'` (default) or `'ed25519'` -## Advanced Features +**Returns:** `GetKeyResponse` +- `key`: Hex-encoded private key +- `signature_chain`: Signatures proving the key was derived in a genuine TEE -### TLS Certificate Generation +### Generate Attestation Quotes -Generate fresh TLS certificates with optional Remote Attestation support. **Important**: `get_tls_key()` generates random keys on each call - it's designed specifically for TLS/SSL scenarios where fresh keys are required. +`get_quote()` creates a TDX quote proving your code runs in a genuine TEE. ```python -# Generate TLS certificate with different usage scenarios -tls_key = client.get_tls_key( - subject='my-secure-service', # Certificate common name - alt_names=['localhost', '127.0.0.1'], # Additional valid domains/IPs - usage_ra_tls=True, # Include remote attestation - usage_server_auth=True, # Enable server authentication (default) - usage_client_auth=False # Disable client authentication -) - -print('Private Key (PEM):', tls_key.key) -print('Certificate Chain:', tls_key.certificate_chain) +quote = client.get_quote(b'user:alice:nonce123') -# ⚠️ WARNING: Each call generates a different key -tls_key1 = client.get_tls_key() -tls_key2 = client.get_tls_key() -# tls_key1.key != tls_key2.key (always different!) +# Replay RTMRs from the event log +rtmrs = quote.replay_rtmrs() +print(rtmrs) ``` -### Event Logging +**Parameters:** +- `report_data`: Exactly 64 bytes recommended. If shorter, pad with zeros. If longer, hash it first (e.g., SHA-256). -> [!NOTE] -> This feature isn't available in the simulator. We recommend sticking with `report_data` for most cases since it's simpler and safer to use. If you're not super familiar with SGX/TDX attestation quotes, it's best to avoid adding data directly into quotes as it could cause verification issues. +**Returns:** `GetQuoteResponse` +- `quote`: Hex-encoded TDX quote +- `event_log`: JSON string of measured events +- `replay_rtmrs()`: Method to compute RTMR values from event log -Extend RTMR3 with custom events for audit trails: +### Get Instance Info ```python -# Emit custom events (requires dstack OS 0.5.0+) -client.emit_event('user-action', json.dumps({ - "action": "transfer", - "amount": 1000, - "timestamp": time.time() -})) - -# Events are automatically included in subsequent quotes -quote = client.get_quote(b'audit-data') -events = json.loads(quote.event_log) +info = client.info() +print(info.app_id) +print(info.instance_id) +print(info.tcb_info) ``` -## Blockchain Integration - -### Ethereum - -```python -from dstack_sdk.ethereum import to_account - -key_result = client.get_key('ethereum/main', 'wallet') - -# Convert to Ethereum account -account = to_account(key_result) -print(f"Ethereum address: {account.address}") - -# Use with Web3.py -from web3 import Web3 -w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR-PROJECT-ID')) +**Returns:** `InfoResponse` +- `app_id`: Application identifier +- `instance_id`: Instance identifier +- `app_name`: Application name +- `tcb_info`: TCB measurements (MRTD, RTMRs, event log) +- `compose_hash`: Hash of the app configuration +- `app_cert`: Application certificate (PEM) -# Sign transactions, etc. -``` +### Generate TLS Certificates -### Solana +`get_tls_key()` creates fresh TLS certificates. Unlike `get_key()`, each call generates a new random key. ```python -from dstack_sdk.solana import to_keypair - -key_result = client.get_key('solana/main', 'wallet') - -# Convert to Solana keypair -keypair = to_keypair(key_result) -print(f"Solana public key: {keypair.public_key}") - -# Use with solana-py -from solana.rpc.api import Client -from solana.transaction import Transaction -from solana.system_program import transfer, TransferParams - -client_rpc = Client("https://api.mainnet-beta.solana.com") +tls = client.get_tls_key( + subject='api.example.com', + alt_names=['localhost'], + usage_ra_tls=True # Embed attestation in certificate +) -# Create and send transaction -# ... (implement transaction logic) +print(tls.key) # PEM private key +print(tls.certificate_chain) # Certificate chain ``` -## Environment Variables Encryption +**Parameters:** +- `subject` (optional): Certificate common name (e.g., domain name) +- `alt_names` (optional): List of subject alternative names +- `usage_ra_tls` (optional): Embed TDX quote in certificate extension +- `usage_server_auth` (optional): Enable for server authentication (default: `True`) +- `usage_client_auth` (optional): Enable for client authentication (default: `False`) -**Important**: This feature is specifically for **deployment-time security**, not runtime SDK operations. +**Returns:** `GetTlsKeyResponse` +- `key`: PEM-encoded private key +- `certificate_chain`: List of PEM certificates -The SDK provides end-to-end encryption capabilities for securely transmitting sensitive environment variables during dstack application deployment. When deploying applications to TEE instances, sensitive configuration data (API keys, database credentials, private keys, etc.) needs to be securely transmitted from the deployment client to the TEE application. +### Sign and Verify -### Deployment Security Problem +Sign data using TEE-derived keys (not yet released): -During application deployment, sensitive data must traverse: -1. **Client Environment** → Deployment infrastructure → **TEE Application** -2. **Risk**: Deployment infrastructure could potentially access plaintext secrets -3. **Solution**: Client-side encryption ensures only the target TEE application can decrypt secrets +```python +result = client.sign('ed25519', b'message to sign') +print(result.signature) +print(result.public_key) -### How It Works +# Verify the signature +valid = client.verify('ed25519', b'message to sign', result.signature, result.public_key) +print(valid.valid) # True +``` -1. **Pre-Deployment**: Client obtains encryption public key from KMS API -2. **Encryption**: Client encrypts environment variables using X25519 + AES-GCM -3. **Transmission**: Encrypted payload is sent through deployment infrastructure -4. **Decryption**: TEE application automatically decrypts and loads environment variables -5. **Runtime**: Application accesses secrets via standard `os.environ` +**`sign()` Parameters:** +- `algorithm`: `'ed25519'`, `'secp256k1'`, or `'secp256k1_prehashed'` +- `data`: Data to sign (bytes or string) -This ensures **true end-to-end encryption** where deployment infrastructure never sees plaintext secrets. +**`sign()` Returns:** `SignResponse` +- `signature`: Hex-encoded signature +- `public_key`: Hex-encoded public key +- `signature_chain`: Signatures proving TEE origin -### App Configuration for Encrypted Variables +**`verify()` Parameters:** +- `algorithm`: Algorithm used for signing +- `data`: Original data +- `signature`: Signature to verify +- `public_key`: Public key to verify against -Your `app-compose.json` should specify which environment variables are allowed: +**`verify()` Returns:** `VerifyResponse` +- `valid`: Boolean indicating if signature is valid -```json -{ - "manifest_version": 1, - "name": "secure-app", - "runner": "docker-compose", - "docker_compose_file": "services:\\n app:\\n build: .\\n volumes:\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n environment:\\n - API_KEY\\n - DATABASE_URL\\n - PRIVATE_KEY", - "allowed_envs": ["API_KEY", "DATABASE_URL", "PRIVATE_KEY"], - "kms_enabled": true -} -``` +### Emit Events -### Deployment Encryption Workflow +Extend RTMR3 with custom measurements for your application's boot sequence (requires dstack OS 0.5.0+). These measurements are append-only and become part of the attestation record. ```python -from dstack_sdk import encrypt_env_vars, verify_env_encrypt_public_key, EnvVar -import requests -import json - -# 1. Define sensitive environment variables -env_vars = [ - EnvVar(key='DATABASE_URL', value='postgresql://user:pass@host:5432/db'), - EnvVar(key='API_SECRET_KEY', value='your-secret-key'), - EnvVar(key='JWT_PRIVATE_KEY', value='-----BEGIN PRIVATE KEY-----\\n...'), - EnvVar(key='WALLET_MNEMONIC', value='abandon abandon abandon...'), -] - -# 2. Obtain encryption public key from KMS API (dstack-vmm or Phala Cloud) -response = requests.post('/prpc/GetAppEnvEncryptPubKey?json', - headers={'Content-Type': 'application/json'}, - json={'app_id': 'your-app-id-hex'}) -data = response.json() -public_key, signature = data['public_key'], data['signature'] - -# 3. Verify KMS API authenticity to prevent man-in-the-middle attacks -public_key_bytes = bytes.fromhex(public_key) -signature_bytes = bytes.fromhex(signature) - -trusted_pubkey = verify_env_encrypt_public_key(public_key_bytes, signature_bytes, 'your-app-id-hex') -if not trusted_pubkey: - raise RuntimeError('KMS API provided untrusted encryption key') - -print('Verified KMS public key:', trusted_pubkey.hex()) - -# 4. Encrypt environment variables for secure deployment -encrypted_data = encrypt_env_vars(env_vars, public_key) -print('Encrypted payload:', encrypted_data) - -# 5. Deploy with encrypted configuration -# deploy_dstack_app({ -# 'app_id': 'your-app-id-hex', -# 'encrypted_env': encrypted_data, -# # ... other deployment parameters -# }) +client.emit_event('config_loaded', 'production') +client.emit_event('plugin_initialized', 'auth-v2') ``` -### Security Guarantees +**Parameters:** +- `event`: Event name (string identifier) +- `payload`: Event value (bytes or string) -The environment encryption system provides several security guarantees: +## Async Client -**End-to-End Encryption**: Environment variables are encrypted on the client side and can only be decrypted by the target dstack application inside the TEE. Even the deployment infrastructure cannot access the plaintext values. +For async applications, use `AsyncDstackClient`: -**KMS Authenticity Verification**: The `verify_env_encrypt_public_key` function validates that the encryption public key comes from a trusted KMS (Key Management Service), preventing man-in-the-middle attacks during key exchange. +```python +from dstack_sdk import AsyncDstackClient +import asyncio -**Forward Secrecy**: Each encryption operation uses ephemeral X25519 keypairs, ensuring that compromising long-term keys cannot decrypt past communications. +async def main(): + client = AsyncDstackClient() -**Authenticated Encryption**: AES-256-GCM provides both confidentiality and integrity protection, detecting any tampering with encrypted data. + info = await client.info() + key = await client.get_key('wallet/eth') -## Cryptographic Security + # Concurrent operations + keys = await asyncio.gather( + client.get_key('user/alice'), + client.get_key('user/bob'), + ) -### Key Derivation Security +asyncio.run(main()) +``` -The SDK implements secure key derivation using: +## Blockchain Integration -- **Deterministic Generation**: Keys are derived using HMAC-based Key Derivation Function (HKDF) -- **Application Isolation**: Each path produces unique keys, preventing cross-application access -- **Signature Verification**: All derived keys include cryptographic proof of origin -- **TEE Protection**: Master keys never leave the secure enclave +### Ethereum ```python -# Each path generates a unique, deterministic key -wallet1 = client.get_key('app1/wallet', 'ethereum') -wallet2 = client.get_key('app2/wallet', 'ethereum') -# wallet1.key != wallet2.key (guaranteed different) +from dstack_sdk.ethereum import to_account -same_wallet = client.get_key('app1/wallet', 'ethereum') -# wallet1.key == same_wallet.key (guaranteed identical) +key = client.get_key('wallet/ethereum') +account = to_account(key) +print(account.address) ``` -### Remote Attestation - -TDX quotes provide cryptographic proof of: - -- **Code Integrity**: Measurement of loaded application code -- **Data Integrity**: Inclusion of application-specific data in quote -- **Environment Authenticity**: Verification of TEE platform and configuration +### Solana ```python -import json -import time - -application_state = json.dumps({ - "version": "1.0.0", - "config_hash": "sha256:...", - "timestamp": time.time() -}) - -quote = client.get_quote(application_state.encode()) +from dstack_sdk.solana import to_keypair -# Quote can be verified by external parties to confirm: -# 1. Application is running in genuine TEE -# 2. Application code matches expected measurements -# 3. Application state is authentic and unmodified +key = client.get_key('wallet/solana') +keypair = to_keypair(key) +print(keypair.public_key) ``` -### Environment Encryption Protocol - -The encryption scheme uses: - -- **X25519 ECDH**: Elliptic curve key exchange for forward secrecy -- **AES-256-GCM**: Authenticated encryption with 256-bit keys -- **Ephemeral Keys**: New keypair generated for each encryption operation -- **Authenticated Data**: Prevents tampering and ensures integrity - -## Development and Testing - -### Local Development +## Development -For development without physical TDX hardware: +For local development without TDX hardware, use the simulator: ```bash -# Clone and build simulator git clone https://github.com/Dstack-TEE/dstack.git cd dstack/sdk/simulator ./build.sh ./dstack-simulator - -# Set environment variable -export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090 -``` - -### Testing Connectivity - -```python -client = DstackClient() - -# Check if dstack service is available -is_available = client.is_reachable() -if not is_available: - print('dstack service is not reachable') - exit(1) -``` - -## API Reference - -### Client Classes Overview - -The Python SDK provides four client classes with identical functionality but different execution models: - -| Client Class | Type | Status | Use Case | -|-------------|------|--------|----------| -| `DstackClient` | Synchronous | ✅ Active | General use, scripts, synchronous code | -| `AsyncDstackClient` | Asynchronous | ✅ Active | Web apps, concurrent operations | -| `TappdClient` | Synchronous | ⚠️ Deprecated | Legacy compatibility only | -| `AsyncTappdClient` | Asynchronous | ⚠️ Deprecated | Legacy compatibility only | - -### DstackClient (Synchronous) - -#### Constructor - -```python -DstackClient(endpoint: str | None = None) ``` -**Parameters:** -- `endpoint` (optional): Connection endpoint - - Unix socket path (production): `/var/run/dstack.sock` - - HTTP/HTTPS URL (development): `http://localhost:8090` - - Environment variable: `DSTACK_SIMULATOR_ENDPOINT` - -### AsyncDstackClient (Asynchronous) +Then set the endpoint: -#### Constructor - -```python -AsyncDstackClient(endpoint: str | None = None) +```bash +export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090 ``` -**Parameters:** Same as `DstackClient` - -**Key Differences from Sync Client:** -- All methods are `async` and must be awaited -- Supports concurrent operations with `asyncio.gather()` -- Better performance for multiple I/O operations -- Integrates with async frameworks (FastAPI, aiohttp, etc.) - -**Production App Configuration:** +Run tests with PDM: -The Docker Compose configuration is embedded in `app-compose.json`: - -```json -{ - "manifest_version": 1, - "name": "production-app", - "runner": "docker-compose", - "docker_compose_file": "services:\\n app:\\n image: your-app\\n volumes:\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n environment:\\n - NODE_ENV=production", - "public_tcbinfo": true -} +```bash +pdm install -d +pdm run pytest -s ``` -**Important**: The `docker_compose_file` contains YAML content as a string, ensuring the volume binding for `/var/run/dstack.sock` is included. - -### Shared Methods +--- -All methods below are available in both synchronous and asynchronous clients with identical signatures and functionality: +## Deployment Utilities -| Sync Method | Async Method | Description | -|------------|--------------|-------------| -| `client.info()` | `await async_client.info()` | Get TEE instance information | -| `client.get_key(...)` | `await async_client.get_key(...)` | Derive deterministic keys | -| `client.get_quote(...)` | `await async_client.get_quote(...)` | Generate attestation quote | -| `client.get_tls_key(...)` | `await async_client.get_tls_key(...)` | Generate TLS certificate | -| `client.emit_event(...)` | `await async_client.emit_event(...)` | Log custom events | -| `client.is_reachable()` | `await async_client.is_reachable()` | Test connectivity | +These utilities are for deployment scripts, not runtime SDK operations. -#### Methods +### Encrypt Environment Variables -##### `info() -> InfoResponse` / `async info() -> InfoResponse` - -Retrieves comprehensive information about the TEE instance. - -**Returns:** `InfoResponse` -- `app_id`: Unique application identifier -- `instance_id`: Unique instance identifier -- `app_name`: Application name from configuration -- `device_id`: TEE device identifier -- `tcb_info`: Trusted Computing Base information - - `mrtd`: Measurement of TEE domain - - `rtmr0-3`: Runtime Measurement Registers - - `event_log`: Boot and runtime events - - `os_image_hash`: Operating system measurement - - `compose_hash`: Application configuration hash -- `app_cert`: Application certificate in PEM format -- `key_provider_info`: Key management configuration - -##### `get_key(path: str | None = None, purpose: str | None = None, algorithm: str = "secp256k1") -> GetKeyResponse` - -Derives a deterministic secp256k1/K256 private key for blockchain and Web3 applications. This is the primary method for obtaining cryptographic keys for wallets, signing, and other deterministic key scenarios. - -**Parameters:** -- `path`: Unique identifier for key derivation (e.g., `"wallet/ethereum"`, `"signing/solana"`) -- `purpose` (optional): Additional context for key usage (default: `""`) -- `algorithm` (optional): Key algorithm (e.g., "secp256k1", "ed25519"). Defaults to "secp256k1". - -**Returns:** `GetKeyResponse` -- `key`: 32-byte secp256k1 private key as hex string (suitable for Ethereum, Bitcoin, Solana, etc.) -- `signature_chain`: Array of cryptographic signatures proving key authenticity - -**Key Characteristics:** -- **Deterministic**: Same path + purpose always generates identical key -- **Isolated**: Different paths produce cryptographically independent keys -- **Blockchain-Ready**: Compatible with secp256k1 curve (Ethereum, Bitcoin, Solana) -- **Verifiable**: Signature chain proves key was derived inside genuine TEE - -**Use Cases:** -- Cryptocurrency wallets -- Transaction signing -- DeFi protocol interactions -- NFT operations -- Any scenario requiring consistent, reproducible keys +Encrypt secrets before deploying to dstack: ```python -# Examples of deterministic key derivation -eth_wallet = client.get_key('wallet/ethereum', 'mainnet') -btc_wallet = client.get_key('wallet/bitcoin', 'mainnet') -sol_wallet = client.get_key('wallet/solana', 'mainnet') - -# Same path always returns same key -key1 = client.get_key('my-app/signing') -key2 = client.get_key('my-app/signing') -# key1.decode_key() == key2.decode_key() (guaranteed identical) - -# Different paths return different keys -user_a = client.get_key('user/alice/wallet') -user_b = client.get_key('user/bob/wallet') -# user_a.decode_key() != user_b.decode_key() (guaranteed different) -``` - -##### `get_quote(report_data: str | bytes) -> GetQuoteResponse` - -Generates a TDX attestation quote containing the provided report data. - -**Parameters:** -- `report_data`: Data to include in quote (max 64 bytes) - -**Returns:** `GetQuoteResponse` -- `quote`: TDX quote as hex string -- `event_log`: JSON string of system events -- `replay_rtmrs()`: Method returning computed RTMR values - -**Use Cases:** -- Remote attestation of application state -- Cryptographic proof of execution environment -- Audit trail generation - -##### `get_tls_key(subject: str | None = None, alt_names: List[str] | None = None, usage_ra_tls: bool = False, usage_server_auth: bool = True, usage_client_auth: bool = False) -> GetTlsKeyResponse` - -Generates a fresh, random TLS key pair with X.509 certificate for TLS/SSL connections. **Important**: This method generates different keys on each call - use `get_key()` for deterministic keys. - -**Parameters:** -- `subject` (optional): Certificate subject (Common Name) - typically the domain name (default: `""`) -- `alt_names` (optional): Subject Alternative Names - additional domains/IPs for the certificate (default: `[]`) -- `usage_ra_tls` (optional): Include TDX attestation quote in certificate extension for remote verification (default: `False`) -- `usage_server_auth` (optional): Enable server authentication - allows certificate to authenticate servers (default: `True`) -- `usage_client_auth` (optional): Enable client authentication - allows certificate to authenticate clients (default: `False`) - -**Returns:** `GetTlsKeyResponse` -- `key`: Private key in PEM format (X.509/PKCS#8) -- `certificate_chain`: Certificate chain list - -**Key Characteristics:** -- **Random Generation**: Each call produces a completely different key -- **TLS-Optimized**: Keys and certificates designed for TLS/SSL scenarios -- **RA-TLS Support**: Optional remote attestation extension in certificates -- **TEE-Signed**: Certificates signed by TEE-resident Certificate Authority - -##### `sign(algorithm: str, data: str | bytes) -> SignResponse` - -Signs data using a derived key. - -**Parameters**: -- `algorithm`: The algorithm to use (e.g., "ed25519", "secp256k1", "secp256k1_prehashed"). -- `data`: The data to sign. If using "secp256k1_prehashed", this must be a 32-byte hash (bytes). - -**Returns**: `SignResponse` -- `signature`: The resulting signature as hex string. -- `signature_chain`: List of hex strings proving key authenticity. -- `public_key`: The public key corresponding to the derived signing key as hex string. - -##### `verify(algorithm: str, data: str | bytes, signature: str | bytes, public_key: str | bytes) -> VerifyResponse` - -Verifies a payload signature. - -**Parameters**: -- `algorithm`: The algorithm used for signing. -- `data`: The original data that was signed. -- `signature`: The signature to verify (hex string or bytes). -- `public_key`: The public key to use for verification (hex string or bytes). - -**Returns**: `VerifyResponse` -- `valid`: A bool indicating if the signature is valid. - -##### `emit_event(event: str, payload: str | bytes) -> None` - -Extends RTMR3 with a custom event for audit logging. - -**Parameters:** -- `event`: Event identifier string -- `payload`: Event data - -**Requirements:** -- dstack OS version 0.5.0 or later -- Events are permanently recorded in TEE measurements - -##### `is_reachable() -> bool` - -Tests connectivity to the dstack service. - -**Returns:** `bool` indicating service availability - -## Utility Functions - -### Compose Hash Calculation - -```python -from dstack_sdk import get_compose_hash +from dstack_sdk import encrypt_env_vars, verify_env_encrypt_public_key, EnvVar -app_compose = { - "manifest_version": 1, - "name": "my-app", - "runner": "docker-compose", - "docker_compose_file": "docker-compose.yml" -} +# Get and verify the KMS public key +# (obtain public_key and signature from KMS API) +kms_identity = verify_env_encrypt_public_key(public_key_bytes, signature_bytes, app_id) +if not kms_identity: + raise RuntimeError('Invalid KMS key') -hash_value = get_compose_hash(app_compose) -print('Configuration hash:', hash_value) +# Encrypt variables +env_vars = [ + EnvVar(key='DATABASE_URL', value='postgresql://...'), + EnvVar(key='API_KEY', value='secret'), +] +encrypted = encrypt_env_vars(env_vars, public_key) ``` -### KMS Public Key Verification - -Verify the authenticity of encryption public keys provided by KMS APIs: +### Calculate Compose Hash ```python -from dstack_sdk import verify_env_encrypt_public_key - -# Example: Verify KMS-provided encryption key -public_key = bytes.fromhex('e33a1832c6562067ff8f844a61e51ad051f1180b66ec2551fb0251735f3ee90a') -signature = bytes.fromhex('8542c49081fbf4e03f62034f13fbf70630bdf256a53032e38465a27c36fd6bed7a5e7111652004aef37f7fd92fbfc1285212c4ae6a6154203a48f5e16cad2cef00') -app_id = '0000000000000000000000000000000000000000' - -kms_identity = verify_env_encrypt_public_key(public_key, signature, app_id) +from dstack_sdk import get_compose_hash -if kms_identity: - print('Trusted KMS identity:', kms_identity.hex()) - # Safe to use the public key for encryption -else: - print('KMS signature verification failed') - # Potential man-in-the-middle attack +hash_value = get_compose_hash(app_compose_dict) ``` -## Security Best Practices - -1. **Key Management** - - Use descriptive, unique paths for key derivation - - Never expose derived keys outside the TEE - - Implement proper access controls in your application - -2. **Remote Attestation** - - Always verify quotes before trusting remote TEE instances - - Include application-specific data in quote generation - - Validate RTMR measurements against expected values - -3. **TLS Configuration** - - Enable RA-TLS for attestation-based authentication - - Use appropriate certificate validity periods - - Implement proper certificate validation - -4. **Error Handling** - - Handle cryptographic operation failures gracefully - - Log security events for monitoring - - Implement fallback mechanisms where appropriate - -## Migration Guide - -### Critical API Changes: Understanding the Separation - -The legacy client mixed two different use cases that have now been properly separated: - -1. **`get_key()`**: Deterministic key derivation for Web3/blockchain (secp256k1) -2. **`get_tls_key()`**: Random TLS certificate generation for HTTPS/SSL - -### From TappdClient to DstackClient - -**⚠️ BREAKING CHANGE**: `TappdClient` and `AsyncTappdClient` are deprecated and will be removed. All users must migrate to `DstackClient` and `AsyncDstackClient`. - -### Complete Migration Reference +--- -| Component | TappdClient (Old) | DstackClient (New) | Status | -|-----------|-------------------|-------------------|---------| -| **Socket Path** | `/var/run/tappd.sock` | `/var/run/dstack.sock` | ✅ Updated | -| **HTTP URL Format** | `http://localhost/prpc/Tappd.` | `http://localhost/` | ✅ Simplified | -| **K256 Key Method** | `get_key(...)` | `get_key(...)` | ✅ Same | -| **TLS Certificate Method** | `derive_key(...)` | `get_tls_key(...)` | ✅ Separated | -| **TDX Quote (Raw)** | `tdx_quote(...)` | `get_quote(report_data)` | ✅ Renamed | +## Migration from TappdClient -#### Migration Steps - -**Step 1: Update Imports and Client** +Replace `TappdClient` with `DstackClient`: ```python # Before -from dstack_sdk import TappdClient, AsyncTappdClient +from dstack_sdk import TappdClient client = TappdClient() -async_client = AsyncTappdClient() -# After -from dstack_sdk import DstackClient, AsyncDstackClient +# After +from dstack_sdk import DstackClient client = DstackClient() -async_client = AsyncDstackClient() ``` -**Step 2: Update Method Calls** - -```python -# For deterministic keys (most common) -# Before: TappdClient methods -key_result = client.get_key('wallet', 'ethereum') - -# After: DstackClient methods (same!) -key_result = client.get_key('wallet', 'ethereum') - -# For TLS certificates -# Before: derive_key with TLS options -tls_cert = client.derive_key('api', 'example.com', ['localhost']) - -# After: get_tls_key with proper options -tls_cert = client.get_tls_key( - subject='example.com', - alt_names=['localhost'] -) -``` - -### Migration Checklist - -- [ ] **Infrastructure Updates:** - - [ ] Update Docker volume binding to `/var/run/dstack.sock` - - [ ] Change environment variables from `TAPPD_*` to `DSTACK_*` - -- [ ] **Client Code Updates:** - - [ ] Replace `TappdClient` with `DstackClient` - - [ ] Replace `AsyncTappdClient` with `AsyncDstackClient` - - [ ] Replace `derive_key()` calls with `get_tls_key()` for TLS certificates - - [ ] Replace `tdx_quote()` calls with `get_quote()` - -- [ ] **Testing:** - - [ ] Test that deterministic keys still work as expected - - [ ] Verify TLS certificate generation works - - [ ] Test quote generation with new interface - -## Development - -We use [PDM](https://pdm-project.org/en/latest/) for local development and creating an isolated environment. - -To initiate development: - -```bash -pdm install -d -``` - -To run tests: - -```bash -DSTACK_SIMULATOR_ENDPOINT=/path/to/dstack/sdk/simulator/dstack.sock pdm run pytest -s -``` +Method changes: +- `derive_key()` → `get_tls_key()` for TLS certificates +- `tdx_quote()` → `get_quote()` +- Socket path: `/var/run/tappd.sock` → `/var/run/dstack.sock` ## License diff --git a/sdk/rust/README.md b/sdk/rust/README.md index 2ae47ad1..a6b0056a 100644 --- a/sdk/rust/README.md +++ b/sdk/rust/README.md @@ -1,6 +1,6 @@ -# dstack Crate +# dstack SDK for Rust -This crate provides rust clients for communicating with both the current dstack server and the legacy tappd service, which are available inside dstack. +Access TEE features from your Rust application running inside dstack. Derive deterministic keys, generate attestation quotes, create TLS certificates, and sign data—all backed by hardware security. ## Installation @@ -9,161 +9,191 @@ This crate provides rust clients for communicating with both the current dstack dstack-sdk = { git = "https://github.com/Dstack-TEE/dstack.git" } ``` -## Basic Usage - -### DstackClient (Current API) +## Quick Start ```rust use dstack_sdk::dstack_client::DstackClient; #[tokio::main] async fn main() -> Result<(), Box> { - let client = DstackClient::new(None); // Uses env var or default to Unix socket - - // Get system info - let info = client.info().await?; - println!("Instance ID: {}", info.instance_id); - - // Derive a key - let key_resp = client.get_key(Some("my-app".to_string()), None).await?; - println!("Key: {}", key_resp.key); - println!("Signature Chain: {:?}", key_resp.signature_chain); + let client = DstackClient::new(None); - // Generate TDX quote - let quote_resp = client.get_quote(b"test-data".to_vec()).await?; - println!("Quote: {}", quote_resp.quote); - let rtmrs = quote_resp.replay_rtmrs()?; - println!("Replayed RTMRs: {:?}", rtmrs); + // Derive a deterministic key for your wallet + let key = client.get_key(Some("wallet/eth".to_string()), None).await?; + println!("{}", key.key); // Same path always returns the same key - // Emit an event - client.emit_event("BootComplete".to_string(), b"payload-data".to_vec()).await?; + // Generate an attestation quote + let quote = client.get_quote(b"my-app-state".to_vec()).await?; + println!("{}", quote.quote); Ok(()) } ``` -### TappdClient (Legacy API) +The client automatically connects to `/var/run/dstack.sock`. For local development with the simulator: ```rust -use dstack_sdk::tappd_client::TappdClient; +let client = DstackClient::new(Some("http://localhost:8090".to_string())); +``` -#[tokio::main] -async fn main() -> Result<(), Box> { - let client = TappdClient::new(None); // Uses env var or default to Unix socket - - // Get system info - let info = client.info().await?; - println!("Instance ID: {}", info.instance_id); - println!("App Name: {}", info.app_name); - - // Derive a key - let key_resp = client.derive_key("my-app").await?; - println!("Key: {}", key_resp.key); - println!("Certificate Chain: {:?}", key_resp.certificate_chain); - - // Decode the key to bytes (extracts raw ECDSA P-256 private key - 32 bytes) - let key_bytes = key_resp.to_bytes()?; - println!("ECDSA P-256 private key bytes (32 bytes): {:?}", key_bytes.len()); - - // Generate quote (exactly 64 bytes of report data required) - let mut report_data = b"test-data".to_vec(); - report_data.resize(64, 0); // Pad to 64 bytes - let quote_resp = client.get_quote(report_data).await?; - println!("Quote: {}", quote_resp.quote); - let rtmrs = quote_resp.replay_rtmrs()?; - println!("Replayed RTMRs: {:?}", rtmrs); +## Core API - Ok(()) -} +### Derive Keys + +`get_key()` derives deterministic keys bound to your application's identity (`app_id`). The same path always produces the same key for your app, but different apps get different keys even with the same path. + +```rust +// Derive keys by path +let eth_key = client.get_key(Some("wallet/ethereum".to_string()), None).await?; +let btc_key = client.get_key(Some("wallet/bitcoin".to_string()), None).await?; + +// Use path to separate keys +let mainnet_key = client.get_key(Some("wallet/eth/mainnet".to_string()), None).await?; +let testnet_key = client.get_key(Some("wallet/eth/testnet".to_string()), None).await?; ``` -## Features +**Parameters:** +- `path`: Key derivation path (determines the key) +- `purpose` (optional): Included in signature chain message, does not affect the derived key + +**Returns:** `GetKeyResponse` +- `key`: Hex-encoded private key +- `signature_chain`: Signatures proving the key was derived in a genuine TEE -### DstackClient Initialization +### Generate Attestation Quotes + +`get_quote()` creates a TDX quote proving your code runs in a genuine TEE. ```rust -let client = DstackClient::new(Some("http://localhost:8000")); +let quote = client.get_quote(b"user:alice:nonce123".to_vec()).await?; + +// Replay RTMRs from the event log +let rtmrs = quote.replay_rtmrs()?; +println!("{:?}", rtmrs); ``` -- `endpoint`: Optional HTTP URL or Unix socket path (`/var/run/dstack.sock` by default) -- Will use the `DSTACK_SIMULATOR_ENDPOINT` environment variable if set -### TappdClient Initialization (Legacy API) +**Parameters:** +- `report_data`: Exactly 64 bytes recommended. If shorter, pad with zeros. If longer, hash it first (e.g., SHA-256). + +**Returns:** `GetQuoteResponse` +- `quote`: Hex-encoded TDX quote +- `event_log`: JSON string of measured events +- `replay_rtmrs()`: Method to compute RTMR values from event log + +### Get Instance Info ```rust -let client = TappdClient::new(Some("/var/run/tappd.sock")); +let info = client.info().await?; +println!("{}", info.app_id); +println!("{}", info.instance_id); +println!("{}", info.tcb_info); ``` -- `endpoint`: Optional HTTP URL or Unix socket path (`/var/run/tappd.sock` by default) -- Will use the `TAPPD_SIMULATOR_ENDPOINT` environment variable if set -- Supports the legacy tappd.sock API for backwards compatibility -## API Methods +**Returns:** `InfoResponse` +- `app_id`: Application identifier +- `instance_id`: Instance identifier +- `app_name`: Application name +- `tcb_info`: TCB measurements (JSON string) +- `compose_hash`: Hash of the app configuration +- `app_cert`: Application certificate (PEM) + +### Generate TLS Certificates -### DstackClient Methods +`get_tls_key()` creates fresh TLS certificates. Unlike `get_key()`, each call generates a new random key. + +```rust +use dstack_sdk_types::dstack::TlsKeyConfig; -#### `info(): InfoResponse` -Fetches metadata and measurements about the CVM instance. +let tls_config = TlsKeyConfig::builder() + .subject("api.example.com") + .alt_names(vec!["localhost".to_string()]) + .usage_ra_tls(true) // Embed attestation in certificate + .usage_server_auth(true) + .build(); -#### `get_key(path: Option, purpose: Option) -> GetKeyResponse` -Derives a key for a specified path and optional purpose. -- `key`: Private key in hex format -- `signature_chain`: Vec of X.509 certificate chain entries +let tls = client.get_tls_key(tls_config).await?; -#### `get_quote(report_data: Vec) -> GetQuoteResponse` -Generates a TDX quote with a custom 64-byte payload. -- `quote`: Hex-encoded quote -- `event_log`: Serialized list of events -- `replay_rtmrs()`: Reconstructs RTMR values from the event log +println!("{}", tls.key); // PEM private key +println!("{:?}", tls.certificate_chain); // Certificate chain +``` -#### `emit_event(event: String, payload: Vec)` -Sends an event log with associated binary payload to the runtime. +**TlsKeyConfig Options:** +- `.subject(name)`: Certificate common name (e.g., domain name) +- `.alt_names(names)`: List of subject alternative names +- `.usage_ra_tls(bool)`: Embed TDX quote in certificate extension +- `.usage_server_auth(bool)`: Enable for server authentication +- `.usage_client_auth(bool)`: Enable for client authentication -#### `get_tls_key(...) -> GetTlsKeyResponse` -Requests a key and X.509 certificate chain for RA-TLS or server/client authentication. +**Returns:** `GetTlsKeyResponse` +- `key`: PEM-encoded private key +- `certificate_chain`: List of PEM certificates -#### sign(algorithm: &str, data: Vec) -> SignResponse -Signs a payload using a derived key. +### Sign and Verify -#### verify(algorithm: &str, data: Vec, signature: Vec, public_key: Vec) -> VerifyResponse -Verifies a payload signature. +Sign data using TEE-derived keys (not yet released): -### TappdClient Methods (Legacy API) +```rust +let result = client.sign("ed25519", b"message to sign".to_vec()).await?; +println!("{:?}", result.signature); +println!("{:?}", result.public_key); + +// Verify the signature +let valid = client.verify( + "ed25519", + b"message to sign".to_vec(), + result.signature.clone(), + result.public_key.clone() +).await?; +println!("{}", valid.valid); // true +``` -#### `info(): TappdInfoResponse` -Fetches metadata and measurements about the CVM instance. +**`sign()` Parameters:** +- `algorithm`: `"ed25519"`, `"secp256k1"`, or `"secp256k1_prehashed"` +- `data`: Data to sign -#### `derive_key(path: &str) -> DeriveKeyResponse` -Derives a key for a specified path. -- `key`: ECDSA P-256 private key in PEM format -- `certificate_chain`: Vec of X.509 certificate chain entries -- `to_bytes()`: Extracts and returns the raw ECDSA P-256 private key bytes (32 bytes) +**`sign()` Returns:** `SignResponse` +- `signature`: Signature bytes +- `public_key`: Public key bytes +- `signature_chain`: Signatures proving TEE origin -#### `derive_key_with_subject(path: &str, subject: &str) -> DeriveKeyResponse` -Derives a key with a custom certificate subject. +**`verify()` Parameters:** +- `algorithm`: Algorithm used for signing +- `data`: Original data +- `signature`: Signature to verify +- `public_key`: Public key to verify against -#### `derive_key_with_subject_and_alt_names(path: &str, subject: Option<&str>, alt_names: Option>) -> DeriveKeyResponse` -Derives a key with full certificate customization. +**`verify()` Returns:** `VerifyResponse` +- `valid`: Boolean indicating if signature is valid -#### `get_quote(report_data: Vec) -> TdxQuoteResponse` -Generates a TDX quote with exactly 64 bytes of raw report data. +### Emit Events -### Structures -- `GetKeyResponse`: Holds derived key and signature chain +Extend RTMR3 with custom measurements for your application's boot sequence (requires dstack OS 0.5.0+). These measurements are append-only and become part of the attestation record. -- `GetQuoteResponse`: Contains the TDX quote and event log, with RTMR replay support +```rust +client.emit_event("config_loaded".to_string(), b"production".to_vec()).await?; +client.emit_event("plugin_initialized".to_string(), b"auth-v2".to_vec()).await?; +``` -- `InfoResponse`: CVM instance metadata, including image and runtime measurements +**Parameters:** +- `event`: Event name (string identifier) +- `payload`: Event value (bytes) -- `SignResponse`: Holds a signature, signature chain, and public key +## Blockchain Integration -- `VerifyResponse`: Holds a boolean valid result +### Ethereum with Alloy -## API Reference +```rust +use dstack_sdk::dstack_client::DstackClient; +use dstack_sdk::ethereum::to_account; -### Running the Simulator +let key = client.get_key(Some("wallet/ethereum".to_string()), None).await?; +let signer = to_account(&key)?; +println!("Ethereum address: {}", signer.address()); +``` -For local development without TDX devices, you can use the simulator under `sdk/simulator`. +## Development -Run the simulator with: +For local development without TDX hardware, use the simulator: ```bash git clone https://github.com/Dstack-TEE/dstack.git @@ -171,25 +201,39 @@ cd dstack/sdk/simulator ./build.sh ./dstack-simulator ``` -Set the endpoint in your environment: +Then set the endpoint: + +```bash +export DSTACK_SIMULATOR_ENDPOINT=http://localhost:8090 ``` -export DSTACK_SIMULATOR_ENDPOINT=/path/to/dstack-simulator/dstack.sock + +Run examples: + +```bash +cargo run --example dstack_client_usage ``` -## Examples +--- -See the `examples/` directory for comprehensive usage examples: +## Migration from TappdClient -- `examples/dstack_client_usage.rs` - Complete example using the current DstackClient API -- `examples/tappd_client_usage.rs` - Complete example using the legacy TappdClient API +Replace `TappdClient` with `DstackClient`: -Run examples with: -```bash -cargo run --example dstack_client_usage -cargo run --example tappd_client_usage +```rust +// Before +use dstack_sdk::tappd_client::TappdClient; +let client = TappdClient::new(None); + +// After +use dstack_sdk::dstack_client::DstackClient; +let client = DstackClient::new(None); ``` +Method changes: +- `derive_key()` → `get_tls_key()` for TLS certificates +- Socket path: `/var/run/tappd.sock` → `/var/run/dstack.sock` + ## License -Apache License +Apache License 2.0 diff --git a/sdk/rust/src/dstack_client.rs b/sdk/rust/src/dstack_client.rs index 40a5242a..f1f206a1 100644 --- a/sdk/rust/src/dstack_client.rs +++ b/sdk/rust/src/dstack_client.rs @@ -104,7 +104,7 @@ impl DstackClient { .send_request_json::<_, _, Value>( path, Method::POST, - &[("Content-Type", "application/json")], + &[("Content-Type", "application/json"), ("Host", "dstack")], Some(&payload), ) .await?;