From 3b27f142748157e3a1c8ca3ca7fb45999383055a Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:55:26 +0800 Subject: [PATCH 001/264] Update serde-human-bytes to 0.1.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 98bc4551c..364cc4221 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ scale = { version = "3.7.4", package = "parity-scale-codec", features = [ "derive", ] } serde = { version = "1.0.219", features = ["derive"], default-features = false } -serde-human-bytes = "0.1.0" +serde-human-bytes = "0.1.2" serde_json = { version = "1.0.140", default-features = false } serde_ini = "0.2.0" toml = "0.8.20" From e533464107f5e8cedb742421d806372e1c6fe3d1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:56:21 +0800 Subject: [PATCH 002/264] Update dcap-qvl 0.3.4 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 364cc4221..e7d186360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,7 +158,7 @@ default-net = "0.22.0" # Cryptography/Security aes-gcm = "0.10.3" curve25519-dalek = "4.1.3" -dcap-qvl = "0.3.0" +dcap-qvl = "0.3.4" elliptic-curve = { version = "0.13.8", features = ["pkcs8"] } getrandom = "0.3.1" hkdf = "0.12.4" From b53de0471ac51227144b1f8e43cc066773e30b68 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 04:04:28 +0000 Subject: [PATCH 003/264] dstack-util: Refactor remove orphans --- Cargo.toml | 2 +- basefiles/app-compose.sh | 4 - basefiles/dstack-prepare.sh | 13 +- dstack-util/Cargo.toml | 2 +- dstack-util/src/docker_compose.rs | 417 ++++++++++++++++++ dstack-util/src/main.rs | 115 +---- .../fixtures/key-provider-docker-compose.yaml | 47 ++ dstack-util/tests/test_remove_orphans.sh | 232 ++++++++++ 8 files changed, 729 insertions(+), 103 deletions(-) create mode 100644 dstack-util/src/docker_compose.rs create mode 100644 dstack-util/tests/fixtures/key-provider-docker-compose.yaml create mode 100755 dstack-util/tests/test_remove_orphans.sh diff --git a/Cargo.toml b/Cargo.toml index e7d186360..45652434a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -217,7 +217,7 @@ uuid = { version = "1.15.1", features = ["v4"] } which = "7.0.2" smallvec = "1.14.0" cmd_lib = "1.9.5" -serde_yaml2 = "0.1.2" +yaml-rust2 = "0.10.4" luks2 = "0.5.0" scopeguard = "1.2.0" diff --git a/basefiles/app-compose.sh b/basefiles/app-compose.sh index c3768ea4c..0387ed348 100644 --- a/basefiles/app-compose.sh +++ b/basefiles/app-compose.sh @@ -23,9 +23,6 @@ case "$RUNNER" in if ! [ -f docker-compose.yaml ]; then jq -r '.docker_compose_file' app-compose.json >docker-compose.yaml fi - dstack-util remove-orphans -f docker-compose.yaml || true - chmod +x /usr/bin/containerd-shim-runc-v2 - systemctl restart docker if ! docker compose up --remove-orphans -d --build; then dstack-util notify-host -e "boot.error" -d "failed to start containers" @@ -37,7 +34,6 @@ case "$RUNNER" in docker volume prune -f ;; "bash") - chmod +x /usr/bin/containerd-shim-runc-v2 echo "Running main script" dstack-util notify-host -e "boot.progress" -d "running main script" || true jq -r '.bash_script' app-compose.json | bash diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index dfa92b9b0..a4704e344 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -24,9 +24,6 @@ mount_overlay /etc/docker $OVERLAY_TMP mount_overlay /usr/bin $OVERLAY_TMP mount_overlay /home/root $OVERLAY_TMP -# Disable the containerd-shim-runc-v2 temporarily to prevent the containers from starting -# before docker compose removal orphans. It will be enabled in app-compose.sh -chmod -x /usr/bin/containerd-shim-runc-v2 # Make sure the system time is synchronized echo "Syncing system time..." @@ -53,3 +50,13 @@ if [ $(jq 'has("init_script")' app-compose.json) == true ]; then dstack-util notify-host -e "boot.progress" -d "init-script" || true source <(jq -r '.init_script' app-compose.json) fi + +RUNNER=$(jq -r '.runner' app-compose.json) +case "$RUNNER" in +docker-compose) + if [[ ! -f docker-compose.yaml ]]; then + jq -r '.docker_compose_file' app-compose.json >docker-compose.yaml + fi + dstack-util remove-orphans --no-dockerd -f docker-compose.yaml || true + ;; +esac diff --git a/dstack-util/Cargo.toml b/dstack-util/Cargo.toml index f02664876..2e7731417 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -45,7 +45,7 @@ rand.workspace = true sha3.workspace = true cert-client.workspace = true x509-parser.workspace = true -serde_yaml2.workspace = true +yaml-rust2.workspace = true bollard.workspace = true sodiumbox.workspace = true libc.workspace = true diff --git a/dstack-util/src/docker_compose.rs b/dstack-util/src/docker_compose.rs new file mode 100644 index 000000000..d20170506 --- /dev/null +++ b/dstack-util/src/docker_compose.rs @@ -0,0 +1,417 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context, Result}; +use bollard::container::{ListContainersOptions, RemoveContainerOptions}; +use bollard::Docker; +use fs_err as fs; +use serde::Deserialize; +use std::collections::HashMap; +use std::path::Path; +use yaml_rust2::{Yaml, YamlLoader}; + +/// Holds parsed information from a docker-compose file +#[derive(Debug)] +pub struct ComposeInfo { + pub project_name: String, + pub service_names: std::collections::HashSet, +} + +/// Parse a docker-compose file and extract project name and service names +pub fn parse_docker_compose_file(compose_file: impl AsRef) -> Result { + let compose_content = + fs::read_to_string(compose_file.as_ref()).context("failed to read docker-compose file")?; + + let yaml_docs = YamlLoader::load_from_str(&compose_content).context("failed to parse YAML")?; + let yaml_doc = yaml_docs.first().context("empty YAML document")?; + + // Extract project name + let project_name = if let Some(name) = yaml_doc["name"].as_str() { + name.to_string() + } else { + get_project_name(compose_file.as_ref())? + }; + + // Extract service names + let services = match &yaml_doc["services"] { + Yaml::Hash(m) => m, + _ => anyhow::bail!("missing or invalid 'services' field"), + }; + + let service_names = services + .keys() + .filter_map(|k| k.as_str().map(|s| s.to_string())) + .collect(); + + Ok(ComposeInfo { + project_name, + service_names, + }) +} + +fn get_project_name(compose_file: impl AsRef) -> Result { + let project_name = fs::canonicalize(compose_file) + .context("failed to canonicalize compose file")? + .parent() + .context("failed to get parent directory of compose file")? + .file_name() + .context("failed to get file name of compose file")? + .to_string_lossy() + .into_owned(); + Ok(project_name) +} + +/// Remove orphaned containers using Docker daemon API +pub async fn remove_orphans(compose_file: impl AsRef, dry_run: bool) -> Result<()> { + // Connect to Docker daemon + let docker = + Docker::connect_with_local_defaults().context("Failed to connect to Docker daemon")?; + + // Parse compose file to extract project name and service names + let compose_info = parse_docker_compose_file(&compose_file)?; + let project_name = compose_info.project_name; + let service_names = compose_info.service_names; + + // List all containers + let options = ListContainersOptions:: { + all: true, + ..Default::default() + }; + + let containers = docker + .list_containers(Some(options)) + .await + .context("Failed to list containers")?; + + // Find and remove orphaned containers + for container in containers { + let Some(labels) = container.labels else { + continue; + }; + + // Check if container belongs to current project + let Some(container_project) = labels.get("com.docker.compose.project") else { + continue; + }; + + if container_project != &project_name { + continue; + } + // Check if service still exists in compose file + let Some(service_name) = labels.get("com.docker.compose.service") else { + continue; + }; + if service_names.contains(service_name) { + continue; + } + // Service no longer exists in compose file, remove the container + let Some(container_id) = container.id else { + continue; + }; + + if dry_run { + println!("would remove orphaned container {service_name} {container_id}"); + } else { + println!("removing orphaned container {service_name} {container_id}"); + docker + .remove_container( + &container_id, + Some(RemoveContainerOptions { + v: true, + force: true, + ..Default::default() + }), + ) + .await + .with_context(|| format!("Failed to remove container {}", container_id))?; + } + } + + Ok(()) +} + +/// Docker container config.v2.json structure +#[derive(Deserialize)] +struct ContainerConfig { + #[serde(rename = "Config")] + config: Option, +} + +#[derive(Deserialize)] +struct ContainerConfigInner { + #[serde(rename = "Labels")] + labels: Option>, +} + +/// Remove orphaned containers without requiring Docker daemon (offline mode) +/// +/// This function directly reads Docker's data directory to find and remove +/// orphaned containers. It should be run BEFORE dockerd starts to prevent +/// orphaned containers from starting. +pub fn remove_orphans_direct( + compose_file: impl AsRef, + docker_root: impl AsRef, + dry_run: bool, +) -> Result<()> { + // Parse compose file to extract project name and service names + let compose_info = parse_docker_compose_file(&compose_file)?; + let project_name = &compose_info.project_name; + let service_names = &compose_info.service_names; + + let containers_dir = docker_root.as_ref().join("containers"); + if !containers_dir.exists() { + println!( + "Docker containers directory does not exist: {}", + containers_dir.display() + ); + return Ok(()); + } + + // Iterate through all container directories + let entries = fs::read_dir(&containers_dir).with_context(|| { + format!( + "Failed to read containers directory: {}", + containers_dir.display() + ) + })?; + + for entry in entries { + let entry = entry.context("Failed to read directory entry")?; + let container_dir = entry.path(); + + if !container_dir.is_dir() { + continue; + } + + let container_id = container_dir + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_string(); + + // Read config.v2.json + let config_path = container_dir.join("config.v2.json"); + if !config_path.exists() { + continue; + } + + let config_content = match fs::read_to_string(&config_path) { + Ok(content) => content, + Err(e) => { + eprintln!("Warning: Failed to read {}: {}", config_path.display(), e); + continue; + } + }; + + let config: ContainerConfig = match serde_json::from_str(&config_content) { + Ok(config) => config, + Err(e) => { + eprintln!("Warning: Failed to parse {}: {}", config_path.display(), e); + continue; + } + }; + + let Some(inner_config) = config.config else { + continue; + }; + + let Some(labels) = inner_config.labels else { + continue; + }; + + // Check if container belongs to current project + let Some(container_project) = labels.get("com.docker.compose.project") else { + continue; + }; + + if container_project != project_name { + continue; + } + + // Check if service still exists in compose file + let Some(service_name) = labels.get("com.docker.compose.service") else { + continue; + }; + + if service_names.contains(service_name) { + continue; + } + + // Service no longer exists in compose file, remove the container directory + let short_id = &container_id[..12.min(container_id.len())]; + + if dry_run { + println!("would remove orphaned container {service_name} {short_id}"); + } else { + println!("removing orphaned container {service_name} {short_id}"); + fs::remove_dir_all(&container_dir).with_context(|| { + format!( + "Failed to remove container directory: {}", + container_dir.display() + ) + })?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_yaml_anchor_parsing() { + // Test that yaml-rust2 can parse YAML anchors and aliases + let yaml_with_anchors = r#" +name: test-project +services: + common: &common-config + image: ubuntu:latest + restart: unless-stopped + + service1: + <<: *common-config + container_name: service1 + + service2: + <<: *common-config + container_name: service2 + + service3: + image: nginx:latest +"#; + + let yaml_docs = YamlLoader::load_from_str(yaml_with_anchors).unwrap(); + let yaml_doc = yaml_docs.first().unwrap(); + + // Extract project name + let project_name = yaml_doc["name"].as_str().unwrap(); + assert_eq!(project_name, "test-project"); + + // Extract service names + let services = match &yaml_doc["services"] { + Yaml::Hash(m) => m, + _ => panic!("services should be a hash"), + }; + + let service_names: std::collections::HashSet = services + .keys() + .filter_map(|k| k.as_str().map(|s| s.to_string())) + .collect(); + + // Verify all services are parsed including the anchor definition + assert_eq!(service_names.len(), 4); + assert!(service_names.contains("common")); + assert!(service_names.contains("service1")); + assert!(service_names.contains("service2")); + assert!(service_names.contains("service3")); + + // Verify that anchors are resolved + // Note: yaml-rust2 parses anchors but doesn't auto-expand merge keys + // The merge key "<<" will contain the referenced hash + let service1 = &yaml_doc["services"]["service1"]; + assert_eq!(service1["container_name"].as_str().unwrap(), "service1"); + + // Verify the merge key contains the anchor content + if let Yaml::Hash(merge_content) = &service1["<<"] { + assert_eq!( + merge_content[&Yaml::String("image".to_string())] + .as_str() + .unwrap(), + "ubuntu:latest" + ); + assert_eq!( + merge_content[&Yaml::String("restart".to_string())] + .as_str() + .unwrap(), + "unless-stopped" + ); + } else { + panic!("merge key should contain hash"); + } + } + + #[test] + fn test_yaml_simple_anchor_alias() { + // Test simple anchor and alias without merge keys + let yaml_simple_anchor = r#" +defaults: &defaults + timeout: 30 + retries: 3 + +service1: + name: web + config: *defaults + +service2: + name: api + config: *defaults +"#; + + let yaml_docs = YamlLoader::load_from_str(yaml_simple_anchor).unwrap(); + let yaml_doc = yaml_docs.first().unwrap(); + + // Verify alias points to the same content + let service1_config = &yaml_doc["service1"]["config"]; + let service2_config = &yaml_doc["service2"]["config"]; + + assert_eq!(service1_config["timeout"].as_i64().unwrap(), 30); + assert_eq!(service1_config["retries"].as_i64().unwrap(), 3); + assert_eq!(service2_config["timeout"].as_i64().unwrap(), 30); + assert_eq!(service2_config["retries"].as_i64().unwrap(), 3); + } + + #[test] + fn test_yaml_without_anchors() { + let yaml_simple = r#" +services: + web: + image: nginx:latest + db: + image: postgres:14 +"#; + + let yaml_docs = YamlLoader::load_from_str(yaml_simple).unwrap(); + let yaml_doc = yaml_docs.first().unwrap(); + + let services = match &yaml_doc["services"] { + Yaml::Hash(m) => m, + _ => panic!("services should be a hash"), + }; + + let service_names: std::collections::HashSet = services + .keys() + .filter_map(|k| k.as_str().map(|s| s.to_string())) + .collect(); + + assert_eq!(service_names.len(), 2); + assert!(service_names.contains("web")); + assert!(service_names.contains("db")); + } + + #[test] + fn test_parse_real_compose_file() { + // Test with real docker-compose.yaml from key-provider-build + let compose_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/key-provider-docker-compose.yaml" + ); + + let compose_info = parse_docker_compose_file(compose_path).unwrap(); + + // Verify service names are correctly extracted + assert_eq!(compose_info.service_names.len(), 2); + assert!(compose_info.service_names.contains("aesmd")); + assert!(compose_info + .service_names + .contains("gramine-sealing-key-provider")); + + // Note: x-common is an anchor definition, not a service, so it should not be in service_names + assert!(!compose_info.service_names.contains("x-common")); + + // Project name should be "fixtures" (the parent directory name) + assert_eq!(compose_info.project_name, "fixtures"); + } +} diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index f5f2f7308..ade229e68 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -3,8 +3,6 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::{Context, Result}; -use bollard::container::{ListContainersOptions, RemoveContainerOptions}; -use bollard::Docker; use clap::{Parser, Subcommand}; use dstack_types::KeyProvider; use fs_err as fs; @@ -29,6 +27,7 @@ use tdx_attest as att; use utils::AppKeys; mod crypto; +mod docker_compose; mod host_api; mod parse_env_file; mod system_setup; @@ -185,16 +184,19 @@ struct RemoveOrphansArgs { /// path to the docker-compose.yaml file #[arg(short = 'f', long)] compose: String, -} -#[derive(Debug, Deserialize)] -struct ComposeConfig { - name: Option, - services: HashMap, -} + /// show what would be removed without actually removing + #[arg(short = 'n', long)] + dry_run: bool, + + /// Offline mode: operate without Docker daemon by directly reading Docker data directory + #[arg(long)] + no_dockerd: bool, -#[derive(Debug, Deserialize)] -struct ComposeService {} + /// Docker data root directory for offline mode (default: /var/lib/docker) + #[arg(short = 'd', long, default_value = "/var/lib/docker")] + docker_root: String, +} fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; @@ -417,89 +419,6 @@ fn sha256(data: &[u8]) -> [u8; 32] { sha256.finalize().into() } -fn get_project_name(compose_file: impl AsRef) -> Result { - let project_name = fs::canonicalize(compose_file) - .context("Failed to canonicalize compose file")? - .parent() - .context("Failed to get parent directory of compose file")? - .file_name() - .context("Failed to get file name of compose file")? - .to_string_lossy() - .into_owned(); - Ok(project_name) -} - -async fn cmd_remove_orphans(compose_file: impl AsRef) -> Result<()> { - // Connect to Docker daemon - let docker = - Docker::connect_with_local_defaults().context("Failed to connect to Docker daemon")?; - - // Read and parse docker-compose.yaml to get project name - let compose_content = - fs::read_to_string(compose_file.as_ref()).context("Failed to read docker-compose.yaml")?; - let docker_compose: ComposeConfig = - serde_yaml2::from_str(&compose_content).context("Failed to parse docker-compose.yaml")?; - - // Get current project name from compose file or directory name - let project_name = match docker_compose.name { - Some(name) => name, - None => get_project_name(compose_file)?, - }; - - // List all containers - let options = ListContainersOptions:: { - all: true, - ..Default::default() - }; - - let containers = docker - .list_containers(Some(options)) - .await - .context("Failed to list containers")?; - - // Find and remove orphaned containers - for container in containers { - let Some(labels) = container.labels else { - continue; - }; - - // Check if container belongs to current project - let Some(container_project) = labels.get("com.docker.compose.project") else { - continue; - }; - - if container_project != &project_name { - continue; - } - // Check if service still exists in compose file - let Some(service_name) = labels.get("com.docker.compose.service") else { - continue; - }; - if docker_compose.services.contains_key(service_name) { - continue; - } - // Service no longer exists in compose file, remove the container - let Some(container_id) = container.id else { - continue; - }; - - println!("Removing orphaned container {service_name} {container_id}"); - docker - .remove_container( - &container_id, - Some(RemoveContainerOptions { - v: true, - force: true, - ..Default::default() - }), - ) - .await - .with_context(|| format!("Failed to remove container {}", container_id))?; - } - - Ok(()) -} - #[tokio::main] async fn main() -> Result<()> { { @@ -542,7 +461,15 @@ async fn main() -> Result<()> { cmd_notify_host(args).await?; } Commands::RemoveOrphans(args) => { - cmd_remove_orphans(args.compose).await?; + if args.no_dockerd { + docker_compose::remove_orphans_direct( + args.compose, + args.docker_root, + args.dry_run, + )?; + } else { + docker_compose::remove_orphans(args.compose, args.dry_run).await?; + } } } diff --git a/dstack-util/tests/fixtures/key-provider-docker-compose.yaml b/dstack-util/tests/fixtures/key-provider-docker-compose.yaml new file mode 100644 index 000000000..25e7e4ef1 --- /dev/null +++ b/dstack-util/tests/fixtures/key-provider-docker-compose.yaml @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: © 2024-2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +x-common: &common-config + restart: always + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + +services: + aesmd: + <<: *common-config + container_name: aesmd + build: + context: . + dockerfile: Dockerfile.aesmd + privileged: true + devices: + - "/dev/sgx_enclave:/dev/sgx_enclave" + - "/dev/sgx_provision:/dev/sgx_provision" + volumes: + - "./sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" + - "aesmd:/var/run/aesmd/" + network_mode: "host" + + gramine-sealing-key-provider: + <<: *common-config + container_name: gramine-sealing-key-provider + build: + context: . + dockerfile: Dockerfile.key-provider + privileged: true + devices: + - "/dev/sgx_enclave:/dev/sgx_enclave" + - "/dev/sgx_provision:/dev/sgx_provision" + depends_on: + - aesmd + volumes: + - "aesmd:/var/run/aesmd/" + ports: + - "127.0.0.1:3443:3443" + +volumes: + aesmd: diff --git a/dstack-util/tests/test_remove_orphans.sh b/dstack-util/tests/test_remove_orphans.sh new file mode 100755 index 000000000..d73c04194 --- /dev/null +++ b/dstack-util/tests/test_remove_orphans.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# Test script for remove-orphans command (both online and offline modes) +# Uses real docker compose to create containers for accurate testing + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +DSTACK_UTIL="$PROJECT_ROOT/target/release/dstack-util" +TEST_DIR=$(mktemp -d) +DOCKER_ROOT="/var/lib/docker" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Project name for tests +PROJECT_NAME="test-orphan-$$" + +cleanup() { + echo -e "${YELLOW}Cleaning up...${NC}" + rm -rf "$TEST_DIR" + # Clean up test containers + docker compose -f "$TEST_DIR/docker-compose.yaml" down -v 2>/dev/null || true + docker rm -f "${PROJECT_NAME}-old" 2>/dev/null || true +} + +trap cleanup EXIT + +echo -e "${YELLOW}=== Test remove-orphans commands ===${NC}" +echo "Test directory: $TEST_DIR" +echo "Project root: $PROJECT_ROOT" +echo "Project name: $PROJECT_NAME" + +# Check if Docker is available +if ! docker info >/dev/null 2>&1; then + echo -e "${RED}ERROR: Docker daemon not available${NC}" + exit 1 +fi + +# Build dstack-util in release mode +echo -e "\n${YELLOW}Building dstack-util...${NC}" +cargo build --release --package dstack-util --manifest-path "$PROJECT_ROOT/Cargo.toml" + +if [ ! -f "$DSTACK_UTIL" ]; then + echo -e "${RED}ERROR: dstack-util binary not found at $DSTACK_UTIL${NC}" + exit 1 +fi + +# ============================================ +# Setup: Create containers using docker compose +# ============================================ +echo -e "\n${YELLOW}=== Setup: Creating test containers with docker compose ===${NC}" + +# Create compose file with web, db, and old-service +cat >"$TEST_DIR/docker-compose-full.yaml" <"$TEST_DIR/docker-compose.yaml" <&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "would remove orphaned container old-service"; then + echo -e "${GREEN}✓ Dry-run correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Dry-run failed to identify orphaned container${NC}" + sudo systemctl start docker + exit 1 +fi + +# Test actual removal +echo -e "\n${YELLOW}Testing offline actual removal...${NC}" +OUTPUT=$(sudo "$DSTACK_UTIL" remove-orphans --no-dockerd -f "$TEST_DIR/docker-compose.yaml" -d "$DOCKER_ROOT" 2>&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "removing orphaned container old-service"; then + echo -e "${GREEN}✓ Removal correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Removal failed to identify orphaned container${NC}" + sudo systemctl start docker + exit 1 +fi + +# ============================================ +# Restart Docker and verify +# ============================================ +echo -e "\n${YELLOW}Restarting Docker daemon...${NC}" +sudo systemctl start docker + +# Wait for docker to start +sleep 3 + +# Verify old-service container is gone +echo -e "\n${YELLOW}Verifying results after Docker restart...${NC}" +echo "Remaining containers:" +docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "table {{.Names}}\t{{.Status}}" + +if docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" | grep -q "old-service"; then + echo -e "${RED}✗ old-service container still exists${NC}" + exit 1 +else + echo -e "${GREEN}✓ old-service container was removed${NC}" +fi + +# Verify web and db containers still exist +if docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" | grep -q "web"; then + echo -e "${GREEN}✓ web container still exists${NC}" +else + echo -e "${RED}✗ web container was incorrectly removed${NC}" + exit 1 +fi + +if docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "{{.Names}}" | grep -q "db"; then + echo -e "${GREEN}✓ db container still exists${NC}" +else + echo -e "${RED}✗ db container was incorrectly removed${NC}" + exit 1 +fi + +# ============================================ +# Test 2: Online mode (with Docker daemon) +# ============================================ +echo -e "\n${YELLOW}=== Test 2: Online mode (remove-orphans) ===${NC}" + +# Create another orphan container using docker run +echo "Creating orphan container for online test..." +docker run -d --name "${PROJECT_NAME}-old" \ + --label "com.docker.compose.project=${PROJECT_NAME}" \ + --label "com.docker.compose.service=another-old-service" \ + alpine:latest sleep infinity + +echo "Containers before online removal:" +docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "table {{.Names}}\t{{.Status}}" + +# Test dry-run +echo -e "\n${YELLOW}Testing online dry-run mode...${NC}" +OUTPUT=$("$DSTACK_UTIL" remove-orphans -f "$TEST_DIR/docker-compose.yaml" -n 2>&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "would remove orphaned container another-old-service"; then + echo -e "${GREEN}✓ Online dry-run correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Online dry-run failed to identify orphaned container${NC}" + exit 1 +fi + +# Verify orphan still exists after dry-run +if docker ps -a --format "{{.Names}}" | grep -q "${PROJECT_NAME}-old"; then + echo -e "${GREEN}✓ Online dry-run did not remove container${NC}" +else + echo -e "${RED}✗ Online dry-run incorrectly removed container${NC}" + exit 1 +fi + +# Test actual removal +echo -e "\n${YELLOW}Testing online actual removal...${NC}" +OUTPUT=$("$DSTACK_UTIL" remove-orphans -f "$TEST_DIR/docker-compose.yaml" 2>&1) +echo "$OUTPUT" + +if echo "$OUTPUT" | grep -q "removing orphaned container another-old-service"; then + echo -e "${GREEN}✓ Online removal correctly identified orphaned container${NC}" +else + echo -e "${RED}✗ Online removal failed to identify orphaned container${NC}" + exit 1 +fi + +# Verify orphan was removed +if ! docker ps -a --format "{{.Names}}" | grep -q "${PROJECT_NAME}-old"; then + echo -e "${GREEN}✓ Orphaned container was removed${NC}" +else + echo -e "${RED}✗ Orphaned container was NOT removed${NC}" + exit 1 +fi + +# Verify other containers still exist +echo "Containers after online removal:" +docker ps -a --filter "label=com.docker.compose.project=${PROJECT_NAME}" --format "table {{.Names}}\t{{.Status}}" + +# Final cleanup +echo -e "\n${YELLOW}Final cleanup...${NC}" +docker compose -f "$TEST_DIR/docker-compose.yaml" down -v 2>/dev/null || true + +echo -e "\n${GREEN}=== All tests passed! ===${NC}" From e3fa46f1dcec6dc7a7b5618b233c7ae85fbc37f2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 04:18:52 +0000 Subject: [PATCH 004/264] cvm: dstack-prepare depends on network-online.target --- basefiles/dstack-prepare.service | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/basefiles/dstack-prepare.service b/basefiles/dstack-prepare.service index 12a47ea76..833ad4090 100644 --- a/basefiles/dstack-prepare.service +++ b/basefiles/dstack-prepare.service @@ -1,6 +1,7 @@ [Unit] Description=dstack Guest Preparation Service -After=network.target chronyd.service +After=network.target network-online.target chronyd.service +Wants=network-online.target Before=app-compose.service dstack-guest-agent.service docker.service OnFailure=reboot.target From a1ae983f28f08381883b40172da809c1415f8f10 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 04:28:17 +0000 Subject: [PATCH 005/264] cvm: re-structure the volatile dirs --- basefiles/dstack-prepare.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index a4704e344..fa00f38ba 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -19,11 +19,10 @@ mount_overlay() { mkdir -p $dst/upper $dst/work mount -t overlay overlay -o lowerdir=$src,upperdir=$dst/upper,workdir=$dst/work $src } -mount_overlay /etc/wireguard $OVERLAY_TMP -mount_overlay /etc/docker $OVERLAY_TMP -mount_overlay /usr/bin $OVERLAY_TMP -mount_overlay /home/root $OVERLAY_TMP - +mount_overlay /etc $OVERLAY_TMP +mount_overlay /usr $OVERLAY_TMP +mount_overlay /bin $OVERLAY_TMP +mount_overlay /home $OVERLAY_TMP # Make sure the system time is synchronized echo "Syncing system time..." @@ -41,7 +40,6 @@ echo "Mounting docker dirs to persistent storage" mkdir -p $DATA_MNT/var/lib/docker mount --rbind $DATA_MNT/var/lib/docker /var/lib/docker mount --rbind $WORK_DIR /dstack -mount_overlay /etc/users $OVERLAY_PERSIST cd /dstack From 63347264d69b063d704dc23d6211b0e55f7e9b09 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 13:44:51 +0800 Subject: [PATCH 006/264] dstack-mr: Support for vvfat/vhd shared volume --- dstack-mr/src/acpi.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/dstack-mr/src/acpi.rs b/dstack-mr/src/acpi.rs index 5976c10fc..a39759e83 100644 --- a/dstack-mr/src/acpi.rs +++ b/dstack-mr/src/acpi.rs @@ -63,12 +63,33 @@ impl Machine<'_> { "tdx-guest,id=tdx", "-device", "vhost-vsock-pci,guest-cid=3", - "-virtfs", - &format!( - "local,path={shared_dir},mount_tag=host-shared,readonly=on,security_model=none,id=virtfs0", - ), ]); + // Configure shared files delivery: either via disk or 9p + match self.host_share_mode.as_str() { + "" | "9p" => { + // Use 9p virtfs (default) + cmd.args([ + "-virtfs", + &format!( + "local,path={shared_dir},mount_tag=host-shared,readonly=on,security_model=none,id=virtfs0", + ), + ]); + } + "vvfat" | "vhd" => { + // Use a second virtual disk (hd2) to share files + cmd.args([ + "-drive", + &format!("file={dummy_disk},if=none,id=hd2,format=raw,readonly=on"), + "-device", + "virtio-blk-pci,drive=hd2", + ]); + } + _ => { + bail!("Invalid shared disk mode: {}", self.host_share_mode); + } + } + if self.root_verity { cmd.args([ "-drive", From 7fff0196c9aa4be7b5576dda7aee433bb4dbe1da Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 14:01:28 +0800 Subject: [PATCH 007/264] cvm: Support for alternative host share modes --- Cargo.toml | 2 + dstack-mr/src/machine.rs | 2 + dstack-types/src/lib.rs | 4 + dstack-types/src/shared_filenames.rs | 1 + dstack-util/src/system_setup.rs | 64 +++++++++++++- verifier/src/verification.rs | 1 + vmm/Cargo.toml | 2 + vmm/src/app.rs | 1 + vmm/src/app/qemu.rs | 127 ++++++++++++++++++++++++--- vmm/src/config.rs | 9 ++ vmm/vmm.toml | 8 ++ 11 files changed, 209 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 45652434a..d12caa3a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,8 @@ yasna = "0.5.2" bytes = "1.10.1" figment = "0.10.19" object = "0.36.4" +fatfs = "0.3.6" +fscommon = "0.1.1" # Networking/HTTP bollard = "0.18.1" diff --git a/dstack-mr/src/machine.rs b/dstack-mr/src/machine.rs index c08e6cdf1..b664d2c40 100644 --- a/dstack-mr/src/machine.rs +++ b/dstack-mr/src/machine.rs @@ -30,6 +30,8 @@ pub struct Machine<'a> { pub num_nvswitches: u32, pub hotplug_off: bool, pub root_verity: bool, + #[builder(default)] + pub host_share_mode: String, } fn parse_version_tuple(v: &str) -> Result<(u32, u32, u32)> { diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 1188a1f02..5c7a8dc7b 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -159,6 +159,10 @@ pub struct VmConfig { pub hotplug_off: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub image: Option, + /// If true, shared files are provided via a second virtual disk (hd2) + /// If false (default), shared files are provided via 9p virtfs + #[serde(default)] + pub host_share_mode: String, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/dstack-types/src/shared_filenames.rs b/dstack-types/src/shared_filenames.rs index 2588ae78f..5c3ef8282 100644 --- a/dstack-types/src/shared_filenames.rs +++ b/dstack-types/src/shared_filenames.rs @@ -12,6 +12,7 @@ pub const DECRYPTED_ENV_JSON: &str = ".decrypted-env.json"; pub const INSTANCE_INFO: &str = ".instance_info"; pub const HOST_SHARED_DIR: &str = "/dstack/.host-shared"; pub const HOST_SHARED_DIR_NAME: &str = ".host-shared"; +pub const HOST_SHARED_DISK_LABEL: &str = "DSTACKSHR"; pub mod compat_v3 { pub const SYS_CONFIG: &str = "config.json"; diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 6e5687027..933ea48e6 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -17,7 +17,7 @@ use dstack_kms_rpc as rpc; use dstack_types::{ shared_filenames::{ APP_COMPOSE, APP_KEYS, DECRYPTED_ENV, DECRYPTED_ENV_JSON, ENCRYPTED_ENV, - HOST_SHARED_DIR_NAME, INSTANCE_INFO, SYS_CONFIG, USER_CONFIG, + HOST_SHARED_DIR_NAME, HOST_SHARED_DISK_LABEL, INSTANCE_INFO, SYS_CONFIG, USER_CONFIG, }, KeyProvider, KeyProviderInfo, }; @@ -207,6 +207,42 @@ struct HostShared { } impl HostShared { + /// Find block device by volume label + fn find_disk_by_label(label: &str) -> Option { + let label_path = format!("/dev/disk/by-label/{}", label); + if Path::new(&label_path).exists() { + return Some(label_path); + } + + // Fallback: scan /sys/block for devices and check their labels with blkid + if let Ok(entries) = fs::read_dir("/sys/block") { + for entry in entries.flatten() { + let dev_name = entry.file_name(); + let dev_path = format!("/dev/{}", dev_name.to_string_lossy()); + + // Use blkid to check the label + if let Ok(output) = Command::new("blkid") + .arg("-s") + .arg("LABEL") + .arg("-o") + .arg("value") + .arg(&dev_path) + .output() + { + if output.status.success() { + let found_label = + String::from_utf8_lossy(&output.stdout).trim().to_string(); + if found_label == label { + return Some(dev_path); + } + } + } + } + } + + None + } + fn load(host_shared_dir: impl Into) -> Result { let host_shared_dir = host_shared_dir.into(); let sys_config = deserialize_json_file(host_shared_dir.sys_config_file())?; @@ -259,7 +295,31 @@ impl HostShared { cmd! { info "Mounting host-shared"; mkdir -p $host_shared_dir; - mount -t 9p -o trans=virtio,version=9p2000.L,ro host-shared $host_shared_dir; + }?; + + // Try to detect and mount shared disk by label first, fallback to 9p + let disk_device = Self::find_disk_by_label(HOST_SHARED_DISK_LABEL); + let mounted_via_disk = if let Some(dev) = disk_device { + info!("Found shared disk at {}", dev); + let mount_result = cmd! { + info "Attempting to mount shared disk"; + mount -o ro $dev $host_shared_dir; + }; + mount_result.is_ok() + } else { + false + }; + + if !mounted_via_disk { + info!("Shared disk not found, trying 9p virtfs"); + cmd! { + mount -t 9p -o trans=virtio,version=9p2000.L,ro host-shared $host_shared_dir; + }?; + } else { + info!("Successfully mounted shared disk"); + } + + cmd! { mkdir -p $host_shared_copy_dir; info "Copying host-shared files"; }?; diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index a53da571c..3230865be 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -286,6 +286,7 @@ impl CvmVerifier { .hugepages(vm_config.hugepages) .num_gpus(vm_config.num_gpus) .num_nvswitches(vm_config.num_nvswitches) + .host_share_mode(vm_config.host_share_mode.clone()) .build() .measure_with_logs() .context("Failed to compute expected MRs")?; diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index 84cb5ff67..c5da0b832 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -50,6 +50,8 @@ lspci.workspace = true base64.workspace = true serde-human-bytes.workspace = true size-parser = { workspace = true, features = ["serde"] } +fatfs.workspace = true +fscommon.workspace = true or-panic.workspace = true [dev-dependencies] diff --git a/vmm/src/app.rs b/vmm/src/app.rs index 289ddf7ab..a46376276 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -831,6 +831,7 @@ fn make_vm_config(cfg: &Config, manifest: &Manifest, image: &Image) -> dstack_ty hugepages: manifest.hugepages, num_gpus: gpus.gpus.len() as u32, num_nvswitches: gpus.bridges.len() as u32, + host_share_mode: cfg.cvm.host_share_mode.clone(), hotplug_off: cfg.cvm.qemu_hotplug_off, image: Some(manifest.image.clone()), } diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index f2ab2f5f0..167fb2b09 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -22,7 +22,9 @@ use base64::prelude::*; use bon::Builder; use dstack_types::{ mr_config::MrConfig, - shared_filenames::{APP_COMPOSE, ENCRYPTED_ENV, INSTANCE_INFO, USER_CONFIG}, + shared_filenames::{ + APP_COMPOSE, ENCRYPTED_ENV, HOST_SHARED_DISK_LABEL, INSTANCE_INFO, USER_CONFIG, + }, AppCompose, }; use dstack_vmm_rpc as pb; @@ -93,6 +95,70 @@ fn create_hd( Ok(()) } +/// Create a FAT32 disk image from a directory using pure Rust (no external commands) +fn create_shared_disk(disk_path: impl AsRef, shared_dir: impl AsRef) -> Result<()> { + use fatfs::{FileSystem, FormatVolumeOptions, FsOptions}; + use std::io::{Cursor, Seek, SeekFrom, Write}; + + let disk_path = disk_path.as_ref(); + let shared_dir = shared_dir.as_ref(); + + const DISK_SIZE: usize = 8 * 1024 * 1024; + let mut disk_data = vec![0u8; DISK_SIZE]; + + { + let cursor = Cursor::new(&mut disk_data); + let mut label_bytes = [b' '; 11]; + let label_str = HOST_SHARED_DISK_LABEL.as_bytes(); + let copy_len = label_str.len().min(11); + label_bytes[..copy_len].copy_from_slice(&label_str[..copy_len]); + let format_opts = FormatVolumeOptions::new() + .fat_type(fatfs::FatType::Fat32) + .volume_label(label_bytes); + fatfs::format_volume(cursor, format_opts).context("Failed to format disk as FAT32")?; + } + + // Open the formatted filesystem in memory and copy files + { + let mut cursor = Cursor::new(&mut disk_data); + cursor + .seek(SeekFrom::Start(0)) + .context("Failed to seek to start")?; + let fs = + FileSystem::new(cursor, FsOptions::new()).context("Failed to open FAT32 filesystem")?; + let root_dir = fs.root_dir(); + + // Copy all files from shared_dir to the FAT32 root + for entry in fs::read_dir(shared_dir).context("Failed to read shared directory")? { + let entry = entry.context("Failed to read directory entry")?; + let path = entry.path(); + + if path.is_file() { + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + // Read source file + let content = fs::read(&path) + .with_context(|| format!("Failed to read file {}", path.display()))?; + + // Write to FAT32 filesystem + let mut fat_file = root_dir + .create_file(&filename_str) + .with_context(|| format!("Failed to create file {filename_str} in FAT32"))?; + fat_file + .write_all(&content) + .with_context(|| format!("Failed to write file {filename_str} to FAT32"))?; + fat_file.flush().context("Failed to flush FAT32 file")?; + } + } + } + + fs::write(disk_path, &disk_data) + .with_context(|| format!("Failed to write disk image to {}", disk_path.display()))?; + + Ok(()) +} + impl VmInfo { pub fn to_pb(&self, gw: &GatewayConfig, brief: bool) -> pb::VmInfo { let workdir = VmWorkDir::new(&self.workdir); @@ -470,15 +536,52 @@ impl VmConfig { .arg("-device") .arg(format!("vhost-vsock-pci,guest-cid={}", self.cid)); - let ro = if self.image.info.shared_ro { - "on" - } else { - "off" - }; - command.arg("-virtfs").arg(format!( - "local,path={},mount_tag=host-shared,readonly={ro},security_model=mapped,id=virtfs0", - shared_dir.display(), - )); + // Configure shared files delivery: either via disk or 9p + match cfg.host_share_mode.as_str() { + "9p" => { + // Use 9p virtfs (default) + let ro = if self.image.info.shared_ro { + "on" + } else { + "off" + }; + command.arg("-virtfs").arg(format!( + "local,path={},mount_tag=host-shared,readonly={ro},security_model=mapped,id=virtfs0", + shared_dir.display(), + )); + } + "vvfat" => { + command + .arg("-blockdev") + .arg(format!( + "driver=vvfat,node-name=vvfat0,read-only=on,dir={},label={}", + shared_dir.display(), + HOST_SHARED_DISK_LABEL + )) + .arg("-device") + .arg("virtio-blk-pci,drive=vvfat0"); + } + "vhd" => { + // Use a second virtual disk (hd2) to share files + let shared_disk_path = workdir.shared_disk_path(); + if shared_disk_path.exists() { + fs::remove_file(&shared_disk_path).context("Failed to remove shared disk")?; + } + create_shared_disk(&shared_disk_path, &shared_dir) + .context("Failed to create shared disk")?; + command + .arg("-drive") + .arg(format!( + "file={},if=none,id=hd2,format=raw,readonly=on", + shared_disk_path.display() + )) + .arg("-device") + .arg("virtio-blk-pci,drive=hd2"); + } + _ => { + bail!("Invalid host sharing mode: {}", cfg.host_share_mode); + } + } let hugepages = self.manifest.hugepages; let pin_numa = self.manifest.pin_numa; @@ -878,6 +981,10 @@ impl VmWorkDir { self.workdir.join("hda.img") } + pub fn shared_disk_path(&self) -> PathBuf { + self.workdir.join("shared.img") + } + pub fn qmp_socket(&self) -> PathBuf { self.workdir.join("qmp.sock") } diff --git a/vmm/src/config.rs b/vmm/src/config.rs index c4d15395d..bbf6612d4 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -190,6 +190,15 @@ pub struct CvmConfig { /// Networking configuration pub networking: Networking, + + /// Host sharing mode. (9p, vhd, vvfat) + pub host_share_mode: String, + + /// QGS (Quote Generation Service) vsock port for kernel-level TSM support. + /// When set, QEMU will pass this port to tdx-guest for configfs-tsm quote generation. + /// The guest kernel will use this vsock port to communicate with the host QGS. + /// Default is None (disabled), common value is 4050. + pub qgs_port: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/vmm/vmm.toml b/vmm/vmm.toml index f4d504e00..a861a1909 100644 --- a/vmm/vmm.toml +++ b/vmm/vmm.toml @@ -38,6 +38,14 @@ use_mrconfigid = true qemu_pci_hole64_size = 0 qemu_hotplug_off = false +host_share_mode = "vvfat" + +# QGS (Quote Generation Service) vsock port for kernel-level TSM support. +# When set, QEMU will pass this port to tdx-guest for configfs-tsm quote generation. +# The guest kernel will use this vsock port to communicate with the host QGS. +# Default is None (disabled), common value is 4050. +qgs_port = 4050 + [cvm.networking] mode = "user" From 8aab28fed7f5f91d4c11c2620a4f351b49a5d513 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 17:06:01 +0800 Subject: [PATCH 008/264] Remove tdx-attest-sys --- Cargo.toml | 2 - dstack-util/src/host_api.rs | 3 +- .../src/system_setup/config_id_verifier.rs | 2 +- guest-agent/src/rpc_service.rs | 13 +- tdx-attest-sys/Cargo.lock | 286 ----- tdx-attest-sys/Cargo.toml | 16 - tdx-attest-sys/bindings.h | 7 - tdx-attest-sys/build.rs | 25 - tdx-attest-sys/csrc/README.txt | 2 - tdx-attest-sys/csrc/qgs_msg_lib.cpp | 1073 ----------------- tdx-attest-sys/csrc/qgs_msg_lib.h | 209 ---- tdx-attest-sys/csrc/tdx_attest.c | 1045 ---------------- tdx-attest-sys/csrc/tdx_attest.h | 268 ---- tdx-attest-sys/src/lib.rs | 10 - tdx-attest/Cargo.toml | 5 +- tdx-attest/src/dummy.rs | 45 +- tdx-attest/src/lib.rs | 17 +- tdx-attest/src/linux.rs | 194 +-- tdx-attest/src/linux/configfs.rs | 146 +++ 19 files changed, 233 insertions(+), 3135 deletions(-) delete mode 100644 tdx-attest-sys/Cargo.lock delete mode 100644 tdx-attest-sys/Cargo.toml delete mode 100644 tdx-attest-sys/bindings.h delete mode 100644 tdx-attest-sys/build.rs delete mode 100644 tdx-attest-sys/csrc/README.txt delete mode 100644 tdx-attest-sys/csrc/qgs_msg_lib.cpp delete mode 100644 tdx-attest-sys/csrc/qgs_msg_lib.h delete mode 100644 tdx-attest-sys/csrc/tdx_attest.c delete mode 100644 tdx-attest-sys/csrc/tdx_attest.h delete mode 100644 tdx-attest-sys/src/lib.rs create mode 100644 tdx-attest/src/linux/configfs.rs diff --git a/Cargo.toml b/Cargo.toml index d12caa3a9..652a7dfe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ members = [ "kms/rpc", "ra-rpc", "ra-tls", - "tdx-attest-sys", "tdx-attest", "dstack-util", "iohash", @@ -68,7 +67,6 @@ cc-eventlog = { path = "cc-eventlog" } supervisor = { path = "supervisor" } supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } -tdx-attest-sys = { path = "tdx-attest-sys" } certbot = { path = "certbot" } rocket-vsock-listener = { path = "rocket-vsock-listener" } host-api = { path = "host-api", default-features = false } diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index cc40581a8..0fdb264a0 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -72,8 +72,7 @@ impl HostApi { let (pk, sk) = generate_keypair(); let mut report_data = [0u8; 64]; report_data[..PUBLICKEYBYTES].copy_from_slice(pk.as_bytes()); - let (_, quote) = - tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?; + let quote = tdx_attest::get_quote(&report_data).context("Failed to get quote")?; let provision = self .client diff --git a/dstack-util/src/system_setup/config_id_verifier.rs b/dstack-util/src/system_setup/config_id_verifier.rs index 4e69a465e..ab8eb48dc 100644 --- a/dstack-util/src/system_setup/config_id_verifier.rs +++ b/dstack-util/src/system_setup/config_id_verifier.rs @@ -7,7 +7,7 @@ use dstack_types::{mr_config::MrConfig, KeyProviderKind}; use tracing::info; fn read_mr_config_id() -> Result<[u8; 48]> { - let (_, quote) = tdx_attest::get_quote(&[0u8; 64], None).context("Failed to get quote")?; + let quote = tdx_attest::get_quote(&[0u8; 64]).context("Failed to get quote")?; let quote = dcap_qvl::quote::Quote::parse("e).context("Failed to parse quote")?; let configid = quote .report diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 16fe61a4e..8be3f7569 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -510,8 +510,7 @@ impl TappdRpc for InternalRpcHandlerV0 { let event_log = read_event_logs().context("Failed to decode event log")?; let event_log = serde_json::to_string(&event_log).context("Failed to serialize event log")?; - let (_, quote) = - tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?; + let quote = tdx_attest::get_quote(&report_data).context("Failed to get quote")?; Ok(TdxQuoteResponse { quote, event_log, @@ -603,9 +602,8 @@ impl WorkerRpc for ExternalRpcHandler { &self.state.inner.vm_config, )?) } else { - let ed25519_quote = tdx_attest::get_quote(&ed25519_report_data, None) - .context("Failed to get ed25519 quote")? - .1; + let ed25519_quote = tdx_attest::get_quote(&ed25519_report_data) + .context("Failed to get ed25519 quote")?; let event_log = serde_json::to_string( &read_event_logs().context("Failed to read event log")?, )?; @@ -635,9 +633,8 @@ impl WorkerRpc for ExternalRpcHandler { &self.state.inner.vm_config, )?) } else { - let secp256k1_quote = tdx_attest::get_quote(&secp256k1_report_data, None) - .context("Failed to get secp256k1 quote")? - .1; + let secp256k1_quote = tdx_attest::get_quote(&secp256k1_report_data) + .context("Failed to get secp256k1 quote")?; let event_log = serde_json::to_string( &read_event_logs().context("Failed to read event log")?, )?; diff --git a/tdx-attest-sys/Cargo.lock b/tdx-attest-sys/Cargo.lock deleted file mode 100644 index 52d7f54c0..000000000 --- a/tdx-attest-sys/Cargo.lock +++ /dev/null @@ -1,286 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", -] - -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "either" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "libc" -version = "0.2.158" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" - -[[package]] -name = "libloading" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "log" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "prettyplease" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex" -version = "1.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "syn" -version = "2.0.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tdx-attest-sys" -version = "0.1.0" -dependencies = [ - "bindgen", -] - -[[package]] -name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/tdx-attest-sys/Cargo.toml b/tdx-attest-sys/Cargo.toml deleted file mode 100644 index 01cdd7a42..000000000 --- a/tdx-attest-sys/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: © 2024 Phala Network -# -# SPDX-License-Identifier: Apache-2.0 - -[package] -name = "tdx-attest-sys" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] - -[build-dependencies] -bindgen.workspace = true -cc.workspace = true diff --git a/tdx-attest-sys/bindings.h b/tdx-attest-sys/bindings.h deleted file mode 100644 index afd97d06d..000000000 --- a/tdx-attest-sys/bindings.h +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-FileCopyrightText: © 2024 Phala Network - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "csrc/tdx_attest.h" diff --git a/tdx-attest-sys/build.rs b/tdx-attest-sys/build.rs deleted file mode 100644 index 9e30e05ff..000000000 --- a/tdx-attest-sys/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: © 2024 Phala Network -// -// SPDX-License-Identifier: Apache-2.0 - -#![allow(clippy::expect_used)] - -use std::env; -use std::path::PathBuf; - -fn main() { - println!("cargo:rerun-if-changed=csrc/tdx_attest.c"); - println!("cargo:rerun-if-changed=csrc/qgs_msg_lib.cpp"); - let output_path = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set")); - bindgen::Builder::default() - .header("bindings.h") - .default_enum_style(bindgen::EnumVariation::ModuleConsts) - .generate() - .expect("Unable to generate bindings") - .write_to_file(output_path.join("bindings.rs")) - .expect("Couldn't write bindings!"); - cc::Build::new() - .file("csrc/tdx_attest.c") - .file("csrc/qgs_msg_lib.cpp") - .compile("tdx_attest"); -} diff --git a/tdx-attest-sys/csrc/README.txt b/tdx-attest-sys/csrc/README.txt deleted file mode 100644 index 16af54a4f..000000000 --- a/tdx-attest-sys/csrc/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -The C source code is forked from: https://github.com/intel/SGXDataCenterAttestationPrimitives/tree/DCAP_1.21/QuoteGeneration/quote_wrapper/tdx_attest -without functionality modification. diff --git a/tdx-attest-sys/csrc/qgs_msg_lib.cpp b/tdx-attest-sys/csrc/qgs_msg_lib.cpp deleted file mode 100644 index 254f673f3..000000000 --- a/tdx-attest-sys/csrc/qgs_msg_lib.cpp +++ /dev/null @@ -1,1073 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#include "qgs_msg_lib.h" - -#include -#include - -const uint32_t QGS_MSG_LIB_MAJOR_VER = 1; -const uint32_t QGS_MSG_LIB_MINOR_VER = 1; - -void qgs_msg_free(void *p_buf) { - free(p_buf); -} - -/** - * @brief Generate serialized get_quote request - * - * @param p_report Cannot be NULL - * @param report_size Cannot be 0 - * @param p_id_list Can be NULL - * @param id_list_size Can be 0 - * @param pp_req returned serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @param p_req_size return size of the serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @return qgs_msg_error_t - */ -qgs_msg_error_t qgs_msg_gen_get_quote_req( - const uint8_t *p_report, uint32_t report_size, - const uint8_t *p_id_list, uint32_t id_list_size, - uint8_t **pp_req, uint32_t *p_req_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_req_t *p_req = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!p_report || !report_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if ((!p_id_list && id_list_size) || (p_id_list && !id_list_size)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_req || !p_req_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(*p_req); - temp += report_size; - temp += id_list_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_req = (qgs_msg_get_quote_req_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_req) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_req->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_req->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_req->header.type = GET_QUOTE_REQ; - p_req->header.size = buf_size; - p_req->header.error_code = 0; - - p_req->report_size = report_size; - p_req->id_list_size = id_list_size; - memcpy(p_req->report_id_list, p_report, report_size); - if (id_list_size) { - memcpy(p_req->report_id_list + report_size, p_id_list, id_list_size); - } - *pp_req = (uint8_t *)p_req; - *p_req_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point : - return ret; -} - -/** - * @brief Generate serialized get_collateral request - * - * @param p_fsmpc Cannot be NULL - * @param fsmpc_size Cannot be 0 - * @param p_pckca Cannot be NULL - * @param pckca_size Cannot be 0 - * @param pp_req returned serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @param p_req_size return size of the serialized buffer, valid only if the return code is QGS_MSG_SUCCESS - * @return qgs_msg_error_t - */ -qgs_msg_error_t qgs_msg_gen_get_collateral_req( - const uint8_t *p_fsmpc, uint32_t fsmpc_size, - const uint8_t *p_pckca, uint32_t pckca_size, - uint8_t **pp_req, uint32_t *p_req_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_req_t *p_req = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!p_fsmpc || !fsmpc_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_pckca || !pckca_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_req || !p_req_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(*p_req); - temp += fsmpc_size; - temp += pckca_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_req = (qgs_msg_get_collateral_req_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_req) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_req->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_req->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_req->header.type = GET_COLLATERAL_REQ; - p_req->header.size = buf_size; - p_req->header.error_code = 0; - - p_req->fsmpc_size = fsmpc_size; - p_req->pckca_size = pckca_size; - memcpy(p_req->fsmpc_pckca, p_fsmpc, fsmpc_size); - memcpy(p_req->fsmpc_pckca + fsmpc_size, p_pckca, pckca_size); - - *pp_req = (uint8_t *)p_req; - *p_req_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_quote_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_report, uint32_t *p_report_size, - const uint8_t **pp_id_list, uint32_t *p_id_list_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_req_t *p_req = NULL; - uint64_t temp = 0; - - if (!p_serialized_req || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_report || !p_report_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_id_list || !p_id_list_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_quote_req_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_quote_req_t *)p_serialized_req; - // Only major version is checked, minor change is deemed as compatible. - if (p_req->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_req->header.type != GET_QUOTE_REQ) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_req->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_req->header.error_code != 0) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - if (!p_req->report_size) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - temp = sizeof(qgs_msg_get_quote_req_t); - temp += p_req->report_size; - temp += p_req->id_list_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_req->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - *pp_report = p_req->report_id_list; - if (p_req->id_list_size) { - *pp_id_list = p_req->report_id_list + p_req->report_size; - } else { - *pp_id_list = NULL; - } - - *p_report_size = p_req->report_size; - *p_id_list_size = p_req->id_list_size; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_collateral_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_fsmpc, uint32_t *p_fsmpc_size, - const uint8_t **pp_pckca, uint32_t *p_pckca_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_req_t *p_req = NULL; - uint64_t temp = 0; - - if (!p_serialized_req || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_fsmpc || !p_fsmpc_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_pckca || !p_pckca_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_collateral_req_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_collateral_req_t *)p_serialized_req; - // Only major version is checked, minor change is deemed as compatible. - if (p_req->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_req->header.type != GET_COLLATERAL_REQ) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_req->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_req->header.error_code != 0) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - if (!p_req->fsmpc_size || !p_req->pckca_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - temp = sizeof(qgs_msg_get_collateral_req_t); - temp += p_req->fsmpc_size; - temp += p_req->pckca_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_req->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - *pp_fsmpc = p_req->fsmpc_pckca; - *pp_pckca = p_req->fsmpc_pckca + p_req->fsmpc_size; - - *p_fsmpc_size = p_req->fsmpc_size; - *p_pckca_size = p_req->pckca_size; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_error_resp( - uint32_t error_code, uint32_t type, - uint8_t **pp_resp, uint32_t *p_resp_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - uint32_t buf_size = 0; - qgs_msg_header_t *p_resp = NULL; - if (error_code == QGS_MSG_SUCCESS) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - switch (type) { - case GET_QUOTE_RESP: - buf_size = sizeof(qgs_msg_get_quote_resp_t); - break; - case GET_COLLATERAL_RESP: - buf_size = sizeof(qgs_msg_get_collateral_resp_t); - break; - case GET_PLATFORM_INFO_RESP: - buf_size = sizeof(qgs_msg_get_platform_info_resp_t); - break; - default: - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - p_resp = (qgs_msg_header_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->type = type; - p_resp->size = buf_size; - p_resp->error_code = error_code; - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_quote_resp( - const uint8_t *p_selected_id, uint32_t id_size, - const uint8_t *p_quote, uint32_t quote_size, - uint8_t **pp_resp, uint32_t *p_resp_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_resp_t *p_resp = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if ((!p_selected_id && id_size) || (p_selected_id && !id_size)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_quote || !quote_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(*p_resp); - temp += id_size; - temp += quote_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_resp = (qgs_msg_get_quote_resp_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->header.type = GET_QUOTE_RESP; - p_resp->header.size = buf_size; - p_resp->header.error_code = QGS_MSG_SUCCESS; - - p_resp->selected_id_size = id_size; - p_resp->quote_size = quote_size; - if (id_size) { - memcpy(p_resp->id_quote, p_selected_id, id_size); - } - memcpy(p_resp->id_quote + id_size, p_quote, quote_size); - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point : - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_collateral_resp( - uint16_t major_version, uint16_t minor_version, - const uint8_t *p_pck_crl_issuer_chain, uint32_t pck_crl_issuer_chain_size, - const uint8_t *p_root_ca_crl, uint32_t root_ca_crl_size, - const uint8_t *p_pck_crl, uint32_t pck_crl_size, - const uint8_t *p_tcb_info_issuer_chain, uint32_t tcb_info_issuer_chain_size, - const uint8_t *p_tcb_info, uint32_t tcb_info_size, - const uint8_t *p_qe_identity_issuer_chain, uint32_t qe_identity_issuer_chain_size, - const uint8_t *p_qe_identity, uint32_t qe_identity_size, - uint8_t **pp_resp, uint32_t *p_resp_size, - const qgs_msg_header_t *p_req_header) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_resp_t *p_resp = NULL; - uint8_t *p_ptr = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - //TODO major_version and minor_version are ignored here, is 0.0 a valid version? - if (!p_pck_crl_issuer_chain || !pck_crl_issuer_chain_size - || !p_root_ca_crl || !root_ca_crl_size - || !p_pck_crl || !pck_crl_size - || !p_tcb_info_issuer_chain || !tcb_info_issuer_chain_size - || !p_tcb_info || !tcb_info_size - || !p_qe_identity_issuer_chain || !qe_identity_issuer_chain_size - || !p_qe_identity || !qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (p_req_header->major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - if (p_req_header->minor_version == 0) { - temp = sizeof(major_version) + sizeof(minor_version) + sizeof(*p_resp); - } else { - temp = sizeof(*p_resp); - } - temp += pck_crl_issuer_chain_size; - temp += root_ca_crl_size; - temp += pck_crl_size; - temp += tcb_info_issuer_chain_size; - temp += tcb_info_size; - temp += qe_identity_issuer_chain_size; - temp += qe_identity_size; - - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_resp = (qgs_msg_get_collateral_resp_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->header.type = GET_COLLATERAL_RESP; - p_resp->header.size = buf_size; - p_resp->header.error_code = QGS_MSG_SUCCESS; - - p_resp->major_version = major_version; - p_resp->minor_version = minor_version; - p_ptr = p_resp->collaterals; - if (pck_crl_issuer_chain_size) { - p_resp->pck_crl_issuer_chain_size = pck_crl_issuer_chain_size; - memcpy(p_ptr, p_pck_crl_issuer_chain, pck_crl_issuer_chain_size); - p_ptr += pck_crl_issuer_chain_size; - } - - if (root_ca_crl_size) { - p_resp->root_ca_crl_size = root_ca_crl_size; - memcpy(p_ptr, p_root_ca_crl, root_ca_crl_size); - p_ptr += root_ca_crl_size; - } - - if (pck_crl_size) { - p_resp->pck_crl_size = pck_crl_size; - memcpy(p_ptr, p_pck_crl, pck_crl_size); - p_ptr += pck_crl_size; - } - - if (tcb_info_issuer_chain_size) { - p_resp->tcb_info_issuer_chain_size = tcb_info_issuer_chain_size; - memcpy(p_ptr, p_tcb_info_issuer_chain, tcb_info_issuer_chain_size); - p_ptr += tcb_info_issuer_chain_size; - } - - if (tcb_info_size) { - p_resp->tcb_info_size = tcb_info_size; - memcpy(p_ptr, p_tcb_info, tcb_info_size); - p_ptr += tcb_info_size; - } - - if (qe_identity_issuer_chain_size) { - p_resp->qe_identity_issuer_chain_size = qe_identity_issuer_chain_size; - memcpy(p_ptr, p_qe_identity_issuer_chain, qe_identity_issuer_chain_size); - p_ptr += qe_identity_issuer_chain_size; - } - - if (root_ca_crl_size) { - p_resp->qe_identity_size = qe_identity_size; - memcpy(p_ptr, p_qe_identity, qe_identity_size); - } - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_quote_resp( - const uint8_t *p_serialized_resp, uint32_t size, - const uint8_t **pp_selected_id, uint32_t *p_id_size, - const uint8_t **pp_quote, uint32_t *p_quote_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_quote_resp_t *p_resp = NULL; - uint64_t temp = 0; - - if (!p_serialized_resp || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_selected_id || !p_id_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_quote || !p_quote_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_quote_resp_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_resp = (qgs_msg_get_quote_resp_t *)p_serialized_resp; - // Only major version is checked, minor change is deemed as compatible. - if (p_resp->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_resp->header.type != GET_QUOTE_RESP) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_resp->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - temp = sizeof(qgs_msg_get_quote_resp_t); - temp += p_resp->selected_id_size; - temp += p_resp->quote_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_resp->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.error_code == QGS_MSG_SUCCESS) { - if (!p_resp->quote_size) { - // It makes no sense to return success and an empty quote - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - if (p_resp->selected_id_size) { - *pp_selected_id = p_resp->id_quote; - *p_id_size = p_resp->selected_id_size; - } else { - *pp_selected_id = NULL; - *p_id_size = 0; - } - *pp_quote = p_resp->id_quote + p_resp->selected_id_size; - *p_quote_size = p_resp->quote_size; - } else if (p_resp->header.error_code < QGS_MSG_ERROR_MAX) { - if (p_resp->selected_id_size || p_resp->quote_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *pp_selected_id = NULL; - *p_id_size = 0; - *pp_quote = NULL; - *p_quote_size = 0; - } else { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_collateral_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_major_version, uint16_t *p_minor_version, - const uint8_t **pp_pck_crl_issuer_chain, uint32_t *p_pck_crl_issuer_chain_size, - const uint8_t **pp_root_ca_crl, uint32_t *p_root_ca_crl_size, - const uint8_t **pp_pck_crl, uint32_t *p_pck_crl_size, - const uint8_t **pp_tcb_info_issuer_chain, uint32_t *p_tcb_info_issuer_chain_size, - const uint8_t **pp_tcb_info, uint32_t *p_tcb_info_size, - const uint8_t **pp_qe_identity_issuer_chain, uint32_t *p_qe_identity_issuer_chain_size, - const uint8_t **pp_qe_identity, uint32_t *p_qe_identity_size) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_collateral_resp_t *p_resp = NULL; - uint64_t temp = 0; - - if (!p_serialized_resp || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_major_version || !p_minor_version - || !pp_pck_crl_issuer_chain || !p_pck_crl_issuer_chain_size - || !pp_root_ca_crl || !p_root_ca_crl_size - || !pp_pck_crl || !p_pck_crl_size - || !pp_tcb_info_issuer_chain || !p_tcb_info_issuer_chain_size - || !pp_tcb_info || !p_tcb_info_size - || !pp_qe_identity_issuer_chain || !p_qe_identity_issuer_chain_size - || !pp_qe_identity || !p_qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_collateral_resp_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_resp = (qgs_msg_get_collateral_resp_t *)p_serialized_resp; - // Only major version is checked, minor change is deemed as compatible. - if (p_resp->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_resp->header.type != GET_COLLATERAL_RESP) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_resp->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.minor_version == 0) { - temp = sizeof(*p_resp) + sizeof(p_resp->major_version) + sizeof(p_resp->minor_version); - } else { - temp = sizeof(*p_resp); - } - temp += p_resp->pck_crl_issuer_chain_size; - temp += p_resp->root_ca_crl_size; - temp += p_resp->pck_crl_size; - temp += p_resp->tcb_info_issuer_chain_size; - temp += p_resp->tcb_info_size; - temp += p_resp->qe_identity_issuer_chain_size; - temp += p_resp->qe_identity_size; - - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_resp->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.error_code == QGS_MSG_SUCCESS) { - // It makes no sense to return success and empty collaterals - if (!p_resp->pck_crl_issuer_chain_size - || !p_resp->root_ca_crl_size - || !p_resp->pck_crl_size - || !p_resp->tcb_info_issuer_chain_size - || !p_resp->tcb_info_size - || !p_resp->qe_identity_issuer_chain_size - || !p_resp->qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_major_version = p_resp->major_version; - *p_minor_version = p_resp->minor_version; - - *pp_pck_crl_issuer_chain = p_resp->collaterals; - *p_pck_crl_issuer_chain_size = p_resp->pck_crl_issuer_chain_size; - - *pp_root_ca_crl = *pp_pck_crl_issuer_chain + p_resp->pck_crl_issuer_chain_size; - *p_root_ca_crl_size = p_resp->root_ca_crl_size; - - *pp_pck_crl = *pp_root_ca_crl + p_resp->root_ca_crl_size; - *p_pck_crl_size = p_resp->pck_crl_size; - - *pp_tcb_info_issuer_chain = *pp_pck_crl + p_resp->pck_crl_size; - *p_tcb_info_issuer_chain_size = p_resp->tcb_info_issuer_chain_size; - - *pp_tcb_info = *pp_tcb_info_issuer_chain + p_resp->tcb_info_issuer_chain_size; - *p_tcb_info_size = p_resp->tcb_info_size; - - *pp_qe_identity_issuer_chain = *pp_tcb_info + p_resp->tcb_info_size; - *p_qe_identity_issuer_chain_size = p_resp->qe_identity_issuer_chain_size; - - *pp_qe_identity = *pp_qe_identity_issuer_chain + p_resp->qe_identity_issuer_chain_size; - *p_qe_identity_size = p_resp->qe_identity_size; - - } else if (p_resp->header.error_code < QGS_MSG_ERROR_MAX) { - if (p_resp->pck_crl_issuer_chain_size - || p_resp->root_ca_crl_size - || p_resp->pck_crl_size - || p_resp->tcb_info_issuer_chain_size - || p_resp->tcb_info_size - || p_resp->qe_identity_issuer_chain_size - || p_resp->qe_identity_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_major_version = 0; - *p_minor_version = 0; - - *pp_pck_crl_issuer_chain = NULL; - *p_pck_crl_issuer_chain_size = 0; - *pp_root_ca_crl = NULL; - *p_root_ca_crl_size = 0; - *pp_pck_crl = NULL; - *p_pck_crl_size = 0; - *pp_tcb_info_issuer_chain = NULL; - *p_tcb_info_issuer_chain_size = 0; - *pp_tcb_info = NULL; - *p_tcb_info_size = 0; - *pp_qe_identity_issuer_chain = NULL; - *p_qe_identity_issuer_chain_size = 0; - *pp_qe_identity = NULL; - *p_qe_identity_size = 0; - } else { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} - -uint32_t qgs_msg_get_type(const uint8_t *p_serialized_msg, uint32_t size, uint32_t *p_type) { - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - const qgs_msg_header_t *p_header = (const qgs_msg_header_t *)p_serialized_msg; - - if (size < sizeof(qgs_msg_header_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (p_header->major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - if (p_header->type >= QGS_MSG_TYPE_MAX) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - *p_type = p_header->type; - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_platform_info_req( - uint8_t **pp_req, uint32_t *p_req_size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_req_t *p_req = NULL; - - if (!pp_req || !p_req_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_platform_info_req_t *)calloc(sizeof(*p_req), sizeof(uint8_t)); - if (!p_req) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_req->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_req->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_req->header.type = GET_PLATFORM_INFO_REQ; - p_req->header.size = sizeof(*p_req); - p_req->header.error_code = 0; - - *pp_req = (uint8_t *)p_req; - *p_req_size = sizeof(*p_req); - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_platform_info_req( - const uint8_t *p_serialized_req, uint32_t size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_req_t *p_req = NULL; - - if (!p_serialized_req || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_platform_info_req_t - if (size < sizeof(qgs_msg_get_platform_info_req_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_req = (qgs_msg_get_platform_info_req_t *)p_serialized_req; - // Only major version is checked, minor change is deemed as compatible. - if (p_req->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_req->header.type != GET_PLATFORM_INFO_REQ) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_req->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_req->header.error_code != 0) { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_gen_get_platform_info_resp( - uint16_t tdqe_isvsvn, uint16_t pce_isvsvn, - const uint8_t *p_platform_id, uint32_t platform_id_size, - const uint8_t *p_cpusvn, uint32_t cpusvn_size, - uint8_t **pp_resp, uint32_t *p_resp_size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_resp_t *p_resp = NULL; - uint32_t buf_size = 0; - uint64_t temp = 0; - uint8_t *p_ptr = NULL; - - if (!pp_resp || !p_resp_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if ((!p_platform_id || !platform_id_size)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!p_cpusvn || !cpusvn_size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - temp = sizeof(tdqe_isvsvn) + sizeof(pce_isvsvn) + sizeof(*p_resp); - temp += platform_id_size + cpusvn_size; - if (temp < UINT32_MAX) { - buf_size = temp & UINT32_MAX; - } else { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - p_resp = (qgs_msg_get_platform_info_resp_t *)calloc(buf_size, sizeof(uint8_t)); - if (!p_resp) { - ret = QGS_MSG_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - p_resp->header.major_version = QGS_MSG_LIB_MAJOR_VER; - p_resp->header.minor_version = QGS_MSG_LIB_MINOR_VER; - p_resp->header.type = GET_PLATFORM_INFO_RESP; - p_resp->header.size = buf_size; - p_resp->header.error_code = QGS_MSG_SUCCESS; - - p_resp->platform_id_size = platform_id_size; - p_resp->cpusvn_size = cpusvn_size; - - p_resp->tdqe_isvsvn = tdqe_isvsvn; - p_resp->pce_isvsvn = pce_isvsvn; - - p_ptr = p_resp->platform_id_cpusvn; - memcpy(p_ptr, p_platform_id, platform_id_size); - p_ptr += platform_id_size; - - memcpy(p_ptr, p_cpusvn, cpusvn_size); - - *pp_resp = (uint8_t *)p_resp; - *p_resp_size = buf_size; - ret = QGS_MSG_SUCCESS; - -ret_point: - return ret; -} - -qgs_msg_error_t qgs_msg_inflate_get_platform_info_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_tdqe_isvsvn, uint16_t *p_pce_isvsvn, - const uint8_t **pp_platform_id, uint32_t *p_platform_id_size, - const uint8_t **pp_cpusvn, uint32_t *p_cpusvn_size) -{ - qgs_msg_error_t ret = QGS_MSG_SUCCESS; - qgs_msg_get_platform_info_resp_t *p_resp = NULL; - uint64_t temp = 0; - - if (!p_serialized_resp || !size) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (!pp_platform_id || !p_platform_id_size - || !pp_cpusvn || !p_cpusvn_size - || !p_tdqe_isvsvn || !p_pce_isvsvn) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - // sanity check, the size shouldn't smaller than qgs_msg_get_quote_req_t - if (size < sizeof(qgs_msg_get_platform_info_resp_t)) { - ret = QGS_MSG_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_resp = (qgs_msg_get_platform_info_resp_t *)p_serialized_resp; - // Only major version is checked, minor change is deemed as compatible. - if (p_resp->header.major_version != QGS_MSG_LIB_MAJOR_VER) { - ret = QGS_MSG_ERROR_INVALID_VERSION; - goto ret_point; - } - - if (p_resp->header.type != GET_PLATFORM_INFO_RESP) { - ret = QGS_MSG_ERROR_INVALID_TYPE; - goto ret_point; - } - - if (p_resp->header.size != size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - temp = sizeof(p_resp->tdqe_isvsvn) + sizeof(p_resp->pce_isvsvn) + sizeof(*p_resp); - temp += p_resp->platform_id_size; - temp += p_resp->cpusvn_size; - if (temp >= UINT32_MAX) { - ret = QGS_MSG_ERROR_UNEXPECTED; - goto ret_point; - } - if (p_resp->header.size != temp) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - - if (p_resp->header.error_code == QGS_MSG_SUCCESS) { - // It makes no sense to return success and empty platform info - if (!p_resp->platform_id_size || !p_resp->cpusvn_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_tdqe_isvsvn = p_resp->tdqe_isvsvn; - *p_pce_isvsvn = p_resp->pce_isvsvn; - - *pp_platform_id = p_resp->platform_id_cpusvn; - *p_platform_id_size = p_resp->platform_id_size; - - *pp_cpusvn = *pp_platform_id + p_resp->platform_id_size; - *p_cpusvn_size = p_resp->cpusvn_size; - - } else if (p_resp->header.error_code < QGS_MSG_ERROR_MAX) { - if (p_resp->platform_id_size || p_resp->cpusvn_size) { - ret = QGS_MSG_ERROR_INVALID_SIZE; - goto ret_point; - } - *p_tdqe_isvsvn = 0; - *p_pce_isvsvn = 0; - - *pp_platform_id = NULL; - *p_platform_id_size = 0; - - *pp_cpusvn = NULL; - *p_cpusvn_size = 0; - } else { - ret = QGS_MSG_ERROR_INVALID_CODE; - goto ret_point; - } - - ret = QGS_MSG_SUCCESS; -ret_point: - return ret; -} \ No newline at end of file diff --git a/tdx-attest-sys/csrc/qgs_msg_lib.h b/tdx-attest-sys/csrc/qgs_msg_lib.h deleted file mode 100644 index c235806c9..000000000 --- a/tdx-attest-sys/csrc/qgs_msg_lib.h +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - - -/** - * File: qgs_msg_lib.h - * - * Description: Message and API definitions for TDX QGS messages - * - */ -#ifndef _QGS_MSG_DEF_H_ -#define _QGS_MSG_DEF_H_ -#include - -#ifndef QGS_MSG_MK_ERROR -#define QGS_MSG_MK_ERROR(x) (0x00012000 | (x)) -#endif - -#pragma pack(push, 1) - -/** Possible errors generated by the qgs message library. */ -typedef enum _qgs_msg_error_t { - QGS_MSG_SUCCESS = 0x0000, ///< Success - QGS_MSG_ERROR_UNEXPECTED = QGS_MSG_MK_ERROR(0x0001), ///< Unexpected error - QGS_MSG_ERROR_OUT_OF_MEMORY = QGS_MSG_MK_ERROR(0x0002), ///< Not enough memory is available to complete this operation - QGS_MSG_ERROR_INVALID_PARAMETER = QGS_MSG_MK_ERROR(0x0003), ///< The parameter is incorrect - QGS_MSG_ERROR_INVALID_VERSION = QGS_MSG_MK_ERROR(0x0004), ///< Unrecognized version of serialized data - QGS_MSG_ERROR_INVALID_TYPE = QGS_MSG_MK_ERROR(0x0005), ///< Invalid message type found - QGS_MSG_ERROR_INVALID_SIZE = QGS_MSG_MK_ERROR(0x0006), ///< Invalid message size found - QGS_MSG_ERROR_INVALID_CODE = QGS_MSG_MK_ERROR(0x0007), ///< Invalid error code - - QGS_MSG_ERROR_MAX, ///< Indicate max error to allow better translation. -} qgs_msg_error_t; - -typedef enum _qgs_msg_type_t { - GET_QUOTE_REQ = 0, - GET_QUOTE_RESP = 1, - GET_COLLATERAL_REQ = 2, - GET_COLLATERAL_RESP = 3, - GET_PLATFORM_INFO_REQ = 4, - GET_PLATFORM_INFO_RESP = 5, - QGS_MSG_TYPE_MAX -} qgs_msg_type_t; - -typedef struct _qgs_msg_header_t { - uint16_t major_version; - uint16_t minor_version; - uint32_t type; - uint32_t size; // size of the whole message, include this header, in byte - uint32_t error_code; // used in response only -} qgs_msg_header_t; - -typedef struct _qgs_msg_get_quote_req_t { - qgs_msg_header_t header; // header.type = GET_QUOTE_REQ - uint32_t report_size; // cannot be 0 - uint32_t id_list_size; // length of id_list, in byte, can be 0 - uint8_t report_id_list[]; // report followed by id list -} qgs_msg_get_quote_req_t; - -typedef struct _qgs_msg_get_quote_resp_s { - qgs_msg_header_t header; // header.type = GET_QUOTE_RESP - uint32_t selected_id_size; // can be 0 in case only one id is sent in request - uint32_t quote_size; // length of quote_data, in byte - uint8_t id_quote[]; // selected id followed by quote -} qgs_msg_get_quote_resp_t; - -typedef struct _qgs_msg_get_collateral_req_t { - qgs_msg_header_t header; // header.type = GET_COLLATERAL_REQ - uint32_t fsmpc_size; // length of fsmpc, in byte - uint32_t pckca_size; // length of pckca, in byte - uint8_t fsmpc_pckca[]; // fsmpc followed by pckca -} qgs_msg_get_collateral_req_t; - -typedef struct _qgs_msg_get_collateral_resp_s { - qgs_msg_header_t header; // header.type = GET_COLLATERAL_RESP - uint16_t major_version; - uint16_t minor_version; - uint32_t pck_crl_issuer_chain_size; - uint32_t root_ca_crl_size; - uint32_t pck_crl_size; - uint32_t tcb_info_issuer_chain_size; - uint32_t tcb_info_size; - uint32_t qe_identity_issuer_chain_size; - uint32_t qe_identity_size; - uint8_t collaterals[]; // payload filled in same order as upper sizes parameters -} qgs_msg_get_collateral_resp_t; - -typedef struct _qgs_msg_get_platform_info_req_t { - qgs_msg_header_t header; // header.type = GET_PLATFORM_INFO_REQ -} qgs_msg_get_platform_info_req_t; - -typedef struct _qgs_msg_get_platform_info_resp_s { - qgs_msg_header_t header; // header.type = GET_PLATFORM_INFO_RESP - uint16_t tdqe_isvsvn; - uint16_t pce_isvsvn; - uint32_t platform_id_size; - uint32_t cpusvn_size; - uint8_t platform_id_cpusvn[]; -} qgs_msg_get_platform_info_resp_t; - -#pragma pack(pop) - -#if defined(__cplusplus) -extern "C" { -#endif -void qgs_msg_free(void *buf); - -qgs_msg_error_t qgs_msg_gen_get_quote_req( - const uint8_t *p_report, uint32_t report_size, - const uint8_t *p_id_list, uint32_t id_list_size, - uint8_t **pp_req, uint32_t *p_req_size); -qgs_msg_error_t qgs_msg_gen_get_collateral_req( - const uint8_t *p_fsmpc, uint32_t fsmpc_size, - const uint8_t *p_pckca, uint32_t pckca_size, - uint8_t **pp_req, uint32_t *p_req_size); - -qgs_msg_error_t qgs_msg_inflate_get_quote_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_report, uint32_t *p_report_size, - const uint8_t **pp_id_list, uint32_t *p_id_list_size); -qgs_msg_error_t qgs_msg_inflate_get_collateral_req( - const uint8_t *p_serialized_req, uint32_t size, - const uint8_t **pp_fsmpc, uint32_t *p_fsmpc_size, - const uint8_t **pp_pckca, uint32_t *p_pckca_size); - -qgs_msg_error_t qgs_msg_gen_error_resp( - uint32_t error_code, uint32_t type, - uint8_t **pp_resp, uint32_t *p_resp_size); - -qgs_msg_error_t qgs_msg_gen_get_quote_resp( - const uint8_t *p_selected_id, uint32_t id_size, - const uint8_t *p_quote, uint32_t quote_size, - uint8_t **pp_resp, uint32_t *p_resp_size); -qgs_msg_error_t qgs_msg_gen_get_collateral_resp( - uint16_t major_version, uint16_t minor_version, - const uint8_t *p_pck_crl_issuer_chain, uint32_t pck_crl_issuer_chain_size, - const uint8_t *p_root_ca_crl, uint32_t root_ca_crl_size, - const uint8_t *p_pck_crl, uint32_t pck_crl_size, - const uint8_t *p_tcb_info_issuer_chain, uint32_t tcb_info_issuer_chain_size, - const uint8_t *p_tcb_info, uint32_t tcb_info_size, - const uint8_t *p_qe_identity_issuer_chain, uint32_t qe_identity_issuer_chain_size, - const uint8_t *p_qe_identity, uint32_t qe_identity_size, - uint8_t **pp_resp, uint32_t *p_resp_size, - const qgs_msg_header_t *p_req_header); - -qgs_msg_error_t qgs_msg_inflate_get_quote_resp( - const uint8_t *p_serialized_resp, uint32_t size, - const uint8_t **pp_selected_id, uint32_t *p_id_size, - const uint8_t **pp_quote, uint32_t *p_quote_size); -qgs_msg_error_t qgs_msg_inflate_get_collateral_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_major_version, uint16_t *p_minor_version, - const uint8_t **pp_pck_crl_issuer_chain, uint32_t *p_pck_crl_issuer_chain_size, - const uint8_t **pp_root_ca_crl, uint32_t *p_root_ca_crl_size, - const uint8_t **pp_pck_crl, uint32_t *p_pck_crl_size, - const uint8_t **pp_tcb_info_issuer_chain, uint32_t *p_tcb_info_issuer_chain_size, - const uint8_t **pp_tcb_info, uint32_t *p_tcb_info_size, - const uint8_t **pp_qe_identity_issuer_chain, uint32_t *p_qe_identity_issuer_chain_size, - const uint8_t **pp_qe_identity, uint32_t *p_qe_identity_size); -uint32_t qgs_msg_get_type(const uint8_t *p_serialized_msg, uint32_t size, uint32_t *p_type); - -qgs_msg_error_t qgs_msg_gen_get_platform_info_req( - uint8_t **pp_req, uint32_t *p_req_size); -qgs_msg_error_t qgs_msg_inflate_get_platform_info_req( - const uint8_t *p_serialized_req, uint32_t size); -qgs_msg_error_t qgs_msg_gen_get_platform_info_resp( - uint16_t tdqe_isvsvn, uint16_t pce_isvsvn, - const uint8_t *p_platform_id, uint32_t platform_id_size, - const uint8_t *p_cpusvn, uint32_t cpusvn_size, - uint8_t **pp_resp, uint32_t *p_resp_size); -qgs_msg_error_t qgs_msg_inflate_get_platform_info_resp( - const uint8_t *p_serialized_resp, uint32_t size, - uint16_t *p_tdqe_isvsvn, uint16_t *p_pce_isvsvn, - const uint8_t **pp_platform_id, uint32_t *p_platform_id_size, - const uint8_t **pp_cpusvn, uint32_t *p_cpusvn_size); - -#if defined(__cplusplus) -} -#endif - -#endif \ No newline at end of file diff --git a/tdx-attest-sys/csrc/tdx_attest.c b/tdx-attest-sys/csrc/tdx_attest.c deleted file mode 100644 index 5b6a9399f..000000000 --- a/tdx-attest-sys/csrc/tdx_attest.c +++ /dev/null @@ -1,1045 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - -#ifndef SERVTD_ATTEST - -#define _GNU_SOURCE -#include -#include -#include "qgs_msg_lib.h" -#include "tdx_attest.h" - -#include -#include -#include -#include -#include // For strtoul -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define TDX_ATTEST_DEV_PATH "/dev/tdx_guest" -#define CFG_FILE_PATH "/etc/tdx-attest.conf" -#define DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV "DCAP_TDX_QUOTE_CONFIGFS_PATH" -#define QUOTE_CONFIGFS_PATH "/sys/kernel/config/tsm/report" -#define DEFAULT_DCAP_TDX_QUOTE_CONFIGFS_PATH QUOTE_CONFIGFS_PATH"/com.intel.dcap" - -// TODO: Should include kernel header, but the header file are included by -// different package in differnt distro, and installed in different locations. -// So add these defines here. Need to remove them later when kernel header -// became stable. - -#define TDX_CMD_GET_REPORT0 _IOWR('T', 1, struct tdx_report_req) -#ifdef V3_DRIVER -#define TDX_CMD_VERIFY_REPORT _IOWR('T', 2, struct tdx_verify_report_req) -#define TDX_CMD_EXTEND_RTMR _IOW('T', 3, struct tdx_extend_rtmr_req) -#define TDX_CMD_GET_QUOTE _IOWR('T', 4, struct tdx_quote_req) -#else -#define TDX_CMD_VERIFY_REPORT _IOR('T', 2, struct tdx_verify_report_req) -#define TDX_CMD_EXTEND_RTMR _IOR('T', 3, struct tdx_extend_rtmr_req) -#define TDX_CMD_GET_QUOTE _IOR('T', 4, struct tdx_quote_req) -#endif - - -/* TD Quote status codes */ -#define GET_QUOTE_SUCCESS 0 -#define GET_QUOTE_IN_FLIGHT 0xffffffffffffffff -#define GET_QUOTE_ERROR 0x8000000000000000 -#define GET_QUOTE_SERVICE_UNAVAILABLE 0x8000000000000001 - -#define TDX_EXTEND_RTMR_DATA_LEN 48 - -#ifdef DEBUG -#define TDX_TRACE \ - do { \ - fprintf(stderr, "\n[%s:%d] ", __FILE__, __LINE__); \ - perror(NULL); \ - }while(0) -#else -#define TDX_TRACE -#endif - -struct tdx_report_req { - __u8 reportdata[TDX_REPORT_DATA_SIZE]; - __u8 tdreport[TDX_REPORT_SIZE]; -}; - -struct tdx_extend_rtmr_req { - __u8 data[TDX_EXTEND_RTMR_DATA_LEN]; - __u8 index; -}; - -struct tdx_quote_hdr { - /* Quote version, filled by TD */ - __u64 version; - /* Status code of Quote request, filled by VMM */ - __u64 status; - /* Length of TDREPORT, filled by TD */ - __u32 in_len; - /* Length of Quote, filled by VMM */ - __u32 out_len; - /* Actual Quote data or TDREPORT on input */ - __u64 data[0]; -}; - -struct tdx_quote_req { - __u64 buf; - __u64 len; -}; - -static const unsigned HEADER_SIZE = 4; -static const size_t REQ_BUF_SIZE = 4 * 4 * 1024; // 4 pages -static const size_t QUOTE_BUF_SIZE = 8 * 1024; //8K -static const size_t QUOTE_MIN_SIZE = 1020; - -static const tdx_uuid_t g_intel_tdqe_uuid = {TDX_SGX_ECDSA_ATTESTATION_ID}; - -static unsigned int get_vsock_port(void) -{ - FILE *p_config_fd = NULL; - char *p_line = NULL; - char *p = NULL; - size_t line_len = 0; - long long_num = 0; - unsigned int port = 0; - - p_config_fd = fopen(CFG_FILE_PATH, "r"); - if (NULL == p_config_fd) { - TDX_TRACE; - return 0; - } - while(-1 != getline(&p_line, &line_len, p_config_fd)) { - char temp[11] = {0}; - int number = 0; - int ret = sscanf(p_line, " %10[#]", temp); - if (ret == 1) { - continue; - } - /* leading or trailing white space are ignored, white space around '=' - are also ignored. The number should no longer than 10 characters. - Trailing non-whitespace are not allowed. */ - ret = sscanf(p_line, " port = %10[0-9] %n", temp, &number); - /* Make sure number is positive then make the cast. It's not likely to - have a negtive value, just a defense-in-depth. The cast is used to - suppress the -Wsign-compare warning. */ - if (ret == 1 && number > 0 && ((size_t)number < line_len) - && !p_line[number]) { - errno = 0; - long_num = strtol(temp, &p, 10); - if (p == temp) { - TDX_TRACE; - port = 0; - break; - } - - // make sure that no range error occurred - if (errno == ERANGE || long_num > UINT_MAX) { - TDX_TRACE; - port = 0; - break; - } - - // range is ok, so we can convert to int - port = (unsigned int)long_num & 0xFFFFFFFF; -#ifdef DEBUG - fprintf(stdout, "\nGet the vsock port number [%u]\n", port); -#endif - break; - } - } - - /* p_line is allocated by sscanf */ - free(p_line); - fclose(p_config_fd); - - return port; -} - -static tdx_attest_error_t get_tdx_report( - int devfd, - const tdx_report_data_t *p_tdx_report_data, - tdx_report_t *p_tdx_report) -{ - if (-1 == devfd) { - return TDX_ATTEST_ERROR_UNEXPECTED; - } - if (!p_tdx_report) { - fprintf(stderr, "\nNeed to input TDX report."); - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (!p_tdx_report_data) { - fprintf(stderr, "\nNeed to input TDX report data."); - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - struct tdx_report_req req = {0}; - memcpy(req.reportdata, p_tdx_report_data->d, sizeof(req.reportdata)); - - if (-1 == ioctl(devfd, TDX_CMD_GET_REPORT0, &req)) { - TDX_TRACE; - return TDX_ATTEST_ERROR_REPORT_FAILURE; - } - memcpy(p_tdx_report->d, req.tdreport, sizeof(p_tdx_report->d)); - return TDX_ATTEST_SUCCESS; -} - -#define MAX_PATH 260 - -static int b_mkdir = 1; -pthread_mutex_t mkdir_mutex; - -void __attribute__((constructor)) init_mutex(void) { pthread_mutex_init(&mkdir_mutex, NULL); } -void __attribute__((destructor)) destroy_mutex(void) { pthread_mutex_destroy(&mkdir_mutex); } - -static tdx_attest_error_t prepare_configfs(char **p_configfs_path) { - int ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - char *configfs_path = NULL; - do { - // Retrive DCAP TDX quote configFS path from environment - configfs_path = secure_getenv(DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - if (configfs_path == NULL) { - syslog(LOG_INFO, "libtdx_attest: env '%s' is not provided - try default path.", - DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - break; - } - if (strnlen(configfs_path, MAX_PATH) >= MAX_PATH - 20) { - syslog(LOG_ERR, "libtdx_attest: env '%s' is too long.", DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - return ret; - } - - // Check whether the configFS directory exists - DIR *dir = opendir(configfs_path); - if (dir == NULL) { - syslog(LOG_ERR, "libtdx_attest: env '%s' is not valid directory.", - DCAP_TDX_QUOTE_CONFIGFS_PATH_ENV); - return ret; - } - closedir(dir); - ret = TDX_ATTEST_SUCCESS; - } while (0); - - while (ret != TDX_ATTEST_SUCCESS) { - // Default DCAP TDX quote configFS path - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - configfs_path = DEFAULT_DCAP_TDX_QUOTE_CONFIGFS_PATH; - pthread_mutex_lock(&mkdir_mutex); - DIR *dir = opendir(configfs_path); - if (dir != NULL) { - pthread_mutex_unlock(&mkdir_mutex); - ret = TDX_ATTEST_SUCCESS; - closedir(dir); - break; - } - if (errno != ENOENT) { - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: default DCAP configFS not supported - fallback to vsock mode."); - break; - } - - // Create default DCAP TDX quote configFS path only once - if (!b_mkdir) { - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: default DCAP configFS not supported - fallback to vsock mode."); - break; - } - b_mkdir = 0; - - dir = opendir(QUOTE_CONFIGFS_PATH); - if (dir == NULL) { - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: configFS not supported - fallback to vsock mode."); - break; - } - closedir(dir); - - if (mkdir(configfs_path, S_IRWXU | S_IRWXG)) { - pthread_mutex_unlock(&mkdir_mutex); - if (errno == EEXIST && (dir = opendir(configfs_path)) != NULL) { - // Another process has just created configfs_path - ret = TDX_ATTEST_SUCCESS; - closedir(dir); - break; - } - syslog(LOG_INFO, "libtdx_attest: cannot create default configFS - fallback to vsock mode."); - break; - } - char provider_path[MAX_PATH]; - snprintf(provider_path, sizeof(provider_path), "%s/provider", configfs_path); - for (size_t retry = 0; retry < 5; retry++) { - // Linux kernel will create provider, generation, inblob, outblob in configfs_path - // after configfs_path direcotry created. - if (access(provider_path, F_OK) == 0) { - pthread_mutex_unlock(&mkdir_mutex); - ret = TDX_ATTEST_SUCCESS; - break; - } - usleep((useconds_t)retry); - } - pthread_mutex_unlock(&mkdir_mutex); - syslog(LOG_INFO, "libtdx_attest: unavailable default configFS - fallback to vsock mode."); - break; - } - - if (ret != TDX_ATTEST_SUCCESS) { - //Both configfs path are unavailable - return ret; - } - - // For Intel TDX, provider is "tdx_guest" - char provider_path[MAX_PATH]; - snprintf(provider_path, sizeof(provider_path), "%s/provider", configfs_path); - int fd = open(provider_path, O_RDONLY); - if (-1 == fd) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: cannot open configFS `%s`.", provider_path); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - // Read the entire file in one shot - char provider[16] = {0}; - ssize_t byte_size = read(fd, provider, 15); - close(fd); - - if (byte_size == -1 || byte_size == 0 || - strncmp(provider, "tdx_guest", sizeof("tdx_guest") - 1)) { - syslog(LOG_ERR, "libtdx_attest: configFS unsupported provider."); - return TDX_ATTEST_ERROR_NOT_SUPPORTED; - } - *p_configfs_path = configfs_path; - return TDX_ATTEST_SUCCESS; -} - -static tdx_attest_error_t read_configfs_generation(char *generation_path, long* p_generation) -{ - int fd = open(generation_path, O_RDONLY); - if (-1 == fd) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: failed to open configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } -#ifdef DEBUG - fprintf(stdout, "\nstart to read generation\n"); -#endif - #define GENERATION_MAX_LENGTH 20 - char str_generation[GENERATION_MAX_LENGTH] = {0}; - ssize_t byte_size = read(fd, str_generation, GENERATION_MAX_LENGTH); - if (byte_size == -1) { - TDX_TRACE; - close(fd); - syslog(LOG_ERR, "libtdx_attest: failed to read configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - close(fd); - if (byte_size == 0) { - syslog(LOG_ERR, "libtdx_attest: no content of configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - if (byte_size >= GENERATION_MAX_LENGTH) { - syslog(LOG_ERR, "libtdx_attest: too large configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - errno = 0; - long generation = strtol(str_generation, NULL, 10); - if (errno != 0) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: cannot parse configFS generation."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - *p_generation = generation; - -#ifdef DEBUG - fprintf(stdout, "\ngeneration: %ld\n", generation); -#endif - return TDX_ATTEST_SUCCESS; -} - -#define RETRY_WAIT_TIME_USEC 10000000 - -tdx_attest_error_t tdx_att_get_quote( - const tdx_report_data_t *p_tdx_report_data, - const tdx_uuid_t *p_att_key_id_list, - uint32_t list_size, - tdx_uuid_t *p_att_key_id, - uint8_t **pp_quote, - uint32_t *p_quote_size, - uint32_t flags) -{ - int s = -1; - int devfd = -1; - - const uint8_t *p_quote = NULL; - uint32_t quote_size = 0; - tdx_attest_error_t ret = TDX_ATTEST_ERROR_UNEXPECTED; - uint8_t *p_blob_payload = NULL; - - if ((!p_att_key_id_list && list_size) || - (p_att_key_id_list && !list_size)) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (!pp_quote) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (flags) { - //TODO: I think we need to have a runtime version to make this flag usable. - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - - // Currently only intel TDQE are supported - if (1 < list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_att_key_id_list && memcmp(p_att_key_id_list, &g_intel_tdqe_uuid, - sizeof(g_intel_tdqe_uuid))) { - return TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID; - } - - *pp_quote = NULL; - - do { - char *configfs_path = NULL; - if (prepare_configfs(&configfs_path) != TDX_ATTEST_SUCCESS) - break; - - char inblob_path[MAX_PATH]; - snprintf(inblob_path, sizeof(inblob_path), "%s/inblob", configfs_path); - - // Lock `inblob` to avoid other processes accessing it using libtdx_attest - // Will unlock it via close() - int fd_lock = open(inblob_path, O_WRONLY | O_CLOEXEC); - if (-1 == fd_lock) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: failed to open configFS inblob."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - if (flock(fd_lock, LOCK_EX)) { - TDX_TRACE; - close(fd_lock); - syslog(LOG_ERR, "libtdx_attest: failed to lock configFS inblob."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - /* Read and check generation value before writing inblob, after writing inblob and after - reading outblob to make sure that outblob matches inblob */ - char generation_path[MAX_PATH]; - snprintf(generation_path, sizeof(generation_path), "%s/generation", configfs_path); - long generation1; - ret = read_configfs_generation(generation_path, &generation1); - if (ret) { - close(fd_lock); - return ret; - } - - // Write TDX report data to inblob - int fd_inblob = open(inblob_path, O_WRONLY); - if (-1 == fd_inblob) { - TDX_TRACE; - close(fd_lock); - syslog(LOG_ERR, "libtdx_attest: failed to open configFS inblob."); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - ssize_t byte_size = 0; - // Wait and retry when EBUSY; other TDX Quotes are being generating - for (int retry = 0; retry < 3; retry++) { - errno = 0; - byte_size = write(fd_inblob, p_tdx_report_data, sizeof(*p_tdx_report_data)); - if (errno != EBUSY) - break; - usleep(RETRY_WAIT_TIME_USEC); - } - if (byte_size != sizeof(*p_tdx_report_data)) { - if (errno == EBUSY) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_BUSY; - } else { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_UNEXPECTED; - } - close(fd_lock); - close(fd_inblob); - syslog(LOG_ERR, "libtdx_attest: failed to write configFS inblob."); - return ret; - } - close(fd_inblob); - - long generation2; - do { - ret = read_configfs_generation(generation_path, &generation2); - if (ret) { - close(fd_lock); - return ret; - } - // In rare cases, generation is not updated - } while (generation2 == generation1 && !usleep(0)); - if (generation2 != generation1 + 1) { - // Another TDX quote generation has been triggered - close(fd_lock); - return TDX_ATTEST_ERROR_BUSY; - } - - // Read TDX quote from outblob - char outblob_path[MAX_PATH]; - snprintf(outblob_path, sizeof(outblob_path), "%s/outblob", configfs_path); - int fd = open(outblob_path, O_RDONLY); - if (-1 == fd) { - TDX_TRACE; - syslog(LOG_ERR, "libtdx_attest: failed to open configFS outblob."); - close(fd_lock); - return TDX_ATTEST_ERROR_UNEXPECTED; - } - - // Allocate memory for the entire file content - p_blob_payload = malloc(QUOTE_BUF_SIZE); - if (p_blob_payload == NULL) { - close(fd_lock); - close(fd); - return TDX_ATTEST_ERROR_OUT_OF_MEMORY; - } -#ifdef DEBUG - fprintf(stdout, "\nstart to read outblob\n"); -#endif - // Read the entire file in one shot - for (int retry = 0; retry < 3; retry++) { - errno = 0; - byte_size = read(fd, p_blob_payload, QUOTE_BUF_SIZE); - if (errno == EBUSY) { - usleep(RETRY_WAIT_TIME_USEC); - } else if (errno != EINTR && errno != ETIMEDOUT) - break; - } - if (byte_size == -1 || byte_size == 0) { - if (errno == EBUSY || errno == EINTR || errno == ETIMEDOUT) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_BUSY; - } else - ret = TDX_ATTEST_ERROR_QUOTE_FAILURE; - close(fd_lock); - close(fd); - free(p_blob_payload); - syslog(LOG_ERR, "libtdx_attest: failed to read outblob."); - return ret; - } - close(fd); - - quote_size = (uint32_t)byte_size; -#ifdef DEBUG - fprintf(stdout, "\nquote size: %d\n", quote_size); -#endif - if (quote_size <= QUOTE_MIN_SIZE || quote_size == QUOTE_BUF_SIZE) { - close(fd_lock); - free(p_blob_payload); - return TDX_ATTEST_ERROR_QUOTE_FAILURE; - } - - long generation3; - ret = read_configfs_generation(generation_path, &generation3); - close(fd_lock); - if (ret) { - free(p_blob_payload); - return ret; - } - // Another TDX quote generation is triggered - if (generation3 != generation2) { - free(p_blob_payload); - return TDX_ATTEST_ERROR_BUSY; - } - - void* tmp_p = realloc(p_blob_payload, quote_size); - if (tmp_p == NULL) { - free(p_blob_payload); - return TDX_ATTEST_ERROR_OUT_OF_MEMORY; - } - *pp_quote = tmp_p; - - if (p_quote_size) { - *p_quote_size = quote_size; - } - if (p_att_key_id) { - *p_att_key_id = g_intel_tdqe_uuid; - } - return TDX_ATTEST_SUCCESS; - } while (0); - -#ifdef DEBUG - fprintf(stdout, "\ngoto legacy logic\n"); -#endif - - uint32_t recieved_bytes = 0; - uint32_t in_msg_size = 0; - unsigned int vsock_port = 0; - uint32_t msg_size = 0; - qgs_msg_error_t qgs_msg_ret = QGS_MSG_SUCCESS; - qgs_msg_header_t *p_header = NULL; - uint8_t *p_req = NULL; - const uint8_t *p_selected_id = NULL; - uint32_t id_size = 0; - - tdx_report_t tdx_report; - memset(&tdx_report, 0, sizeof(tdx_report)); - - struct tdx_quote_hdr *p_get_quote_blob = malloc(REQ_BUF_SIZE); - if (!p_get_quote_blob) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - devfd = open(TDX_ATTEST_DEV_PATH, O_RDWR | O_SYNC); - if (-1 == devfd) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_DEVICE_FAILURE; - goto ret_point; - } - - ret = get_tdx_report(devfd, p_tdx_report_data, &tdx_report); - if (TDX_ATTEST_SUCCESS != ret) { - goto ret_point; - } - - qgs_msg_ret = qgs_msg_gen_get_quote_req(tdx_report.d, sizeof(tdx_report.d), - NULL, 0, &p_req, &msg_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { -#ifdef DEBUG - fprintf(stdout, "\nqgs_msg_gen_get_quote_req return 0x%x\n", qgs_msg_ret); -#endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - if (msg_size > REQ_BUF_SIZE - sizeof(struct tdx_quote_hdr) - HEADER_SIZE) { -#ifdef DEBUG - fprintf(stdout, "\nqmsg_size[%d] is too big\n", msg_size); - #endif - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - goto ret_point; - } - - p_blob_payload = (uint8_t *)&p_get_quote_blob->data; - p_blob_payload[0] = (uint8_t)((msg_size >> 24) & 0xFF); - p_blob_payload[1] = (uint8_t)((msg_size >> 16) & 0xFF); - p_blob_payload[2] = (uint8_t)((msg_size >> 8) & 0xFF); - p_blob_payload[3] = (uint8_t)(msg_size & 0xFF); - - memcpy(p_blob_payload + HEADER_SIZE, p_req, msg_size); - - do { - vsock_port = get_vsock_port(); - if (!vsock_port) { - syslog(LOG_INFO, "libtdx_attest: cannot parse sock port - fallback to tdvmcall mode."); - break; - } - s = socket(AF_VSOCK, SOCK_STREAM, 0); - if (-1 == s) { - syslog(LOG_INFO, "libtdx_attest: cannot create socket - fallback to tdvmcall mode."); - break; - } - struct sockaddr_vm vm_addr; - memset(&vm_addr, 0, sizeof(vm_addr)); - vm_addr.svm_family = AF_VSOCK; - vm_addr.svm_reserved1 = 0; - vm_addr.svm_port = vsock_port; - vm_addr.svm_cid = VMADDR_CID_HOST; - if (connect(s, (struct sockaddr *)&vm_addr, sizeof(vm_addr))) { - syslog(LOG_INFO, "libtdx_attest: cannot connect - fallback to tdvmcall mode."); - break; - } - - // Write to socket - if (HEADER_SIZE + msg_size != send(s, p_blob_payload, - HEADER_SIZE + msg_size, 0)) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_VSOCK_FAILURE; - goto ret_point; - } - - // Read the response size header - if (HEADER_SIZE != recv(s, p_blob_payload, - HEADER_SIZE, 0)) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_VSOCK_FAILURE; - goto ret_point; - } - - // decode the size - for (unsigned i = 0; i < HEADER_SIZE; ++i) { - in_msg_size = in_msg_size * 256 + ((p_blob_payload[i]) & 0xFF); - } - - // prepare the buffer and read the reply body - #ifdef DEBUG - fprintf(stdout, "\nReply message body is %u bytes", in_msg_size); - #endif - - if (REQ_BUF_SIZE - sizeof(struct tdx_quote_hdr) - HEADER_SIZE < in_msg_size) { - #ifdef DEBUG - fprintf(stdout, "\nReply message body is too big"); - #endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - while( recieved_bytes < in_msg_size) { - int recv_ret = (int)recv(s, p_blob_payload + HEADER_SIZE + recieved_bytes, - in_msg_size - recieved_bytes, 0); - if (recv_ret < 0) { - ret = TDX_ATTEST_ERROR_VSOCK_FAILURE; - goto ret_point; - } - recieved_bytes += (uint32_t)recv_ret; - } - #ifdef DEBUG - fprintf(stdout, "\nGet %u bytes response from vsock", recieved_bytes); - #endif - - goto done; - } while (0); - - int ioctl_ret; - struct tdx_quote_req arg; - p_get_quote_blob->version = 1; - p_get_quote_blob->status = 0; - p_get_quote_blob->in_len = HEADER_SIZE + msg_size; - p_get_quote_blob->out_len = 0; - arg.buf = (__u64)p_get_quote_blob; - arg.len = REQ_BUF_SIZE; - - ioctl_ret = ioctl(devfd, TDX_CMD_GET_QUOTE, &arg); - if (EBUSY == ioctl_ret) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_BUSY; - goto ret_point; - } else if (ioctl_ret) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_QUOTE_FAILURE; - goto ret_point; - } - if (p_get_quote_blob->status - || p_get_quote_blob->out_len <= HEADER_SIZE) { - TDX_TRACE; - if (GET_QUOTE_IN_FLIGHT == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_BUSY; - } else if (GET_QUOTE_SERVICE_UNAVAILABLE == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - } else { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - } - goto ret_point; - } - - //in_msg_size is the size of serialized response - for (unsigned i = 0; i < HEADER_SIZE; ++i) { - in_msg_size = in_msg_size * 256 + ((p_blob_payload[i]) & 0xFF); - } - if (in_msg_size != p_get_quote_blob->out_len - HEADER_SIZE) { - TDX_TRACE; - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - #ifdef DEBUG - fprintf(stdout, "\nGet %u bytes response from tdvmcall", in_msg_size); - #endif - -done: - qgs_msg_ret = qgs_msg_inflate_get_quote_resp( - p_blob_payload + HEADER_SIZE, in_msg_size, - &p_selected_id, &id_size, - &p_quote, "e_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { - #ifdef DEBUG - fprintf(stdout, "\nqgs_msg_inflate_get_quote_resp return 0x%x", qgs_msg_ret); - #endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - // We've called qgs_msg_inflate_get_quote_resp, the message type should be GET_QUOTE_RESP - p_header = (qgs_msg_header_t *)(p_blob_payload + HEADER_SIZE); - if (p_header->error_code != 0) { - #ifdef DEBUG - fprintf(stdout, "\nerror code in resp msg is 0x%x", p_header->error_code); - #endif - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - *pp_quote = malloc(quote_size); - if (!*pp_quote) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - memcpy(*pp_quote, p_quote, quote_size); - if (p_quote_size) { - *p_quote_size = quote_size; - } - if (p_att_key_id) { - *p_att_key_id = g_intel_tdqe_uuid; - } - ret = TDX_ATTEST_SUCCESS; - -ret_point: - if (s >= 0) { - close(s); - } - if (-1 != devfd) { - close(devfd); - } - qgs_msg_free(p_req); - free(p_get_quote_blob); - - return ret; -} - -tdx_attest_error_t tdx_att_free_quote( - uint8_t *p_quote) -{ - free(p_quote); - return TDX_ATTEST_SUCCESS; -} - -tdx_attest_error_t tdx_att_get_report( - const tdx_report_data_t *p_tdx_report_data, - tdx_report_t *p_tdx_report) -{ - int devfd; - tdx_attest_error_t ret = TDX_ATTEST_SUCCESS; - - devfd = open(TDX_ATTEST_DEV_PATH, O_RDWR | O_SYNC); - if (-1 == devfd) { - TDX_TRACE; - return TDX_ATTEST_ERROR_DEVICE_FAILURE; - } - - ret = get_tdx_report(devfd, p_tdx_report_data, p_tdx_report); - - close(devfd); - return ret; -} - -tdx_attest_error_t tdx_att_get_supported_att_key_ids( - tdx_uuid_t *p_att_key_id_list, - uint32_t *p_list_size) -{ - if (!p_list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_att_key_id_list && !*p_list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (!p_att_key_id_list && *p_list_size) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_att_key_id_list) { - p_att_key_id_list[0] = g_intel_tdqe_uuid; - } - *p_list_size = 1; - return TDX_ATTEST_SUCCESS; -} - -tdx_attest_error_t tdx_att_extend( - const tdx_rtmr_event_t *p_rtmr_event) -{ -#ifdef TDX_CMD_EXTEND_RTMR - int devfd = -1; - struct tdx_extend_rtmr_req req; - if (!p_rtmr_event || p_rtmr_event->version != 1) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - if (p_rtmr_event->event_data_size) { - return TDX_ATTEST_ERROR_NOT_SUPPORTED; - } - if (p_rtmr_event->rtmr_index > 3) { - return TDX_ATTEST_ERROR_INVALID_PARAMETER; - } - - devfd = open(TDX_ATTEST_DEV_PATH, O_RDWR | O_SYNC); - if (-1 == devfd) { - TDX_TRACE; - return TDX_ATTEST_ERROR_DEVICE_FAILURE; - } - - static_assert(TDX_EXTEND_RTMR_DATA_LEN == sizeof(p_rtmr_event->extend_data), - "rtmr extend size mismatch!"); - req.index = (uint8_t)p_rtmr_event->rtmr_index; - memcpy(req.data, p_rtmr_event->extend_data, TDX_EXTEND_RTMR_DATA_LEN); - if (-1 == ioctl(devfd, TDX_CMD_EXTEND_RTMR, &req)) { - TDX_TRACE; - close(devfd); - if (EINVAL == errno) { - return TDX_ATTEST_ERROR_INVALID_RTMR_INDEX; - } - return TDX_ATTEST_ERROR_EXTEND_FAILURE; - } - close(devfd); - return TDX_ATTEST_SUCCESS; -#else - (void)p_rtmr_event; - return TDX_ATTEST_ERROR_NOT_SUPPORTED; -#endif -} - -#else - -#include "tdx_attest.h" -#include "servtd_com.h" -#include "servtd_external.h" -#include "qgs_msg_lib.h" - -#include -#include -#include -#include - -__attribute__ ((visibility("default"))) tdx_attest_error_t tdx_att_get_quote_by_report ( - const void *p_tdx_report, - uint32_t tdx_report_size, - void *p_quote, - uint32_t *p_quote_size) -{ - uint32_t quote_size = 0; - uint32_t in_msg_size = 0; - tdx_attest_error_t ret = TDX_ATTEST_ERROR_UNEXPECTED; - struct servtd_tdx_quote_hdr *p_get_quote_blob = NULL; - uint8_t *p_blob_payload = NULL; - uint32_t msg_size = 0; - int servtd_get_quote_ret = 0; - const uint8_t *tmp_p_quote = NULL; - const uint8_t *p_selected_id = NULL; - uint32_t id_size = 0; - qgs_msg_error_t qgs_msg_ret = QGS_MSG_SUCCESS; - qgs_msg_header_t *p_header = NULL; - uint8_t *p_req = NULL; - - if (NULL == p_tdx_report || TDX_REPORT_SIZE != tdx_report_size) { - ret = TDX_ATTEST_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - if (NULL == p_quote || NULL == p_quote_size || 0 == *p_quote_size) { - ret = TDX_ATTEST_ERROR_INVALID_PARAMETER; - goto ret_point; - } - - p_get_quote_blob = (struct servtd_tdx_quote_hdr *)malloc(SERVTD_REQ_BUF_SIZE); - if (!p_get_quote_blob) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - - qgs_msg_ret = qgs_msg_gen_get_quote_req(p_tdx_report, tdx_report_size, - NULL, 0, &p_req, &msg_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - if (msg_size > SERVTD_REQ_BUF_SIZE - sizeof(struct servtd_tdx_quote_hdr) - SERVTD_HEADER_SIZE) { - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - goto ret_point; - } - - p_blob_payload = (uint8_t *)&p_get_quote_blob->data; - p_blob_payload[0] = (uint8_t)((msg_size >> 24) & 0xFF); - p_blob_payload[1] = (uint8_t)((msg_size >> 16) & 0xFF); - p_blob_payload[2] = (uint8_t)((msg_size >> 8) & 0xFF); - p_blob_payload[3] = (uint8_t)(msg_size & 0xFF); - - // Serialization - memcpy(p_blob_payload + SERVTD_HEADER_SIZE, p_req, msg_size); - - p_get_quote_blob->version = 1; - p_get_quote_blob->status = 0; - p_get_quote_blob->in_len = SERVTD_HEADER_SIZE + msg_size; - p_get_quote_blob->out_len = 0; - - servtd_get_quote_ret = servtd_get_quote(p_get_quote_blob, SERVTD_REQ_BUF_SIZE); - if (servtd_get_quote_ret) { - ret = TDX_ATTEST_ERROR_QUOTE_FAILURE; - goto ret_point; - } - - if (p_get_quote_blob->status - || p_get_quote_blob->out_len <= SERVTD_HEADER_SIZE) { - if (GET_QUOTE_IN_FLIGHT == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_BUSY; - } else if (GET_QUOTE_SERVICE_UNAVAILABLE == p_get_quote_blob->status) { - ret = TDX_ATTEST_ERROR_NOT_SUPPORTED; - } else { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - } - goto ret_point; - } - - //in_msg_size is the size of serialized response, remove 4bytes header - for (unsigned i = 0; i < SERVTD_HEADER_SIZE; ++i) { - in_msg_size = in_msg_size * 256 + ((p_blob_payload[i]) & 0xFF); - } - if (in_msg_size != p_get_quote_blob->out_len - SERVTD_HEADER_SIZE) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - qgs_msg_ret = qgs_msg_inflate_get_quote_resp( - p_blob_payload + SERVTD_HEADER_SIZE, in_msg_size, - &p_selected_id, &id_size, - (const uint8_t **)&tmp_p_quote, "e_size); - if (QGS_MSG_SUCCESS != qgs_msg_ret) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - // We've called qgs_msg_inflate_get_quote_resp, the message type should be GET_QUOTE_RESP - p_header = (qgs_msg_header_t *)(p_blob_payload + SERVTD_HEADER_SIZE); - if (p_header->error_code != 0) { - ret = TDX_ATTEST_ERROR_UNEXPECTED; - goto ret_point; - } - - if (quote_size > *p_quote_size) { - ret = TDX_ATTEST_ERROR_OUT_OF_MEMORY; - goto ret_point; - } - memcpy(p_quote, tmp_p_quote, quote_size); - - *p_quote_size = quote_size; - ret = TDX_ATTEST_SUCCESS; - -ret_point: - qgs_msg_free(p_req); - SAFE_FREE(p_get_quote_blob); - return ret; -} - -#endif diff --git a/tdx-attest-sys/csrc/tdx_attest.h b/tdx-attest-sys/csrc/tdx_attest.h deleted file mode 100644 index 258617bf9..000000000 --- a/tdx-attest-sys/csrc/tdx_attest.h +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2011-2021 Intel Corporation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Intel Corporation nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ - - -/** - * File: tdx_attest.h - * - * Description: API definitions for TDX Attestation library - * - */ -#ifndef _TDX_ATTEST_H_ -#define _TDX_ATTEST_H_ -#include - -typedef enum _tdx_attest_error_t { - TDX_ATTEST_SUCCESS = 0x0000, ///< Success - TDX_ATTEST_ERROR_MIN = 0x0001, ///< Indicate min error to allow better translation. - TDX_ATTEST_ERROR_UNEXPECTED = 0x0001, ///< Unexpected error - TDX_ATTEST_ERROR_INVALID_PARAMETER = 0x0002, ///< The parameter is incorrect - TDX_ATTEST_ERROR_OUT_OF_MEMORY = 0x0003, ///< Not enough memory is available to complete this operation - TDX_ATTEST_ERROR_VSOCK_FAILURE = 0x0004, ///< vsock related failure - TDX_ATTEST_ERROR_REPORT_FAILURE = 0x0005, ///< Failed to get the TD Report - TDX_ATTEST_ERROR_EXTEND_FAILURE = 0x0006, ///< Failed to extend rtmr - TDX_ATTEST_ERROR_NOT_SUPPORTED = 0x0007, ///< Request feature is not supported - TDX_ATTEST_ERROR_QUOTE_FAILURE = 0x0008, ///< Failed to get the TD Quote - TDX_ATTEST_ERROR_BUSY = 0x0009, ///< The device driver return busy - TDX_ATTEST_ERROR_DEVICE_FAILURE = 0x000a, ///< Failed to acess tdx attest device - TDX_ATTEST_ERROR_INVALID_RTMR_INDEX = 0x000b, ///< Only supported RTMR index is 2 and 3 - TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID = 0x000c, ///< The platform Quoting infrastructure does not support any of the keys described in att_key_id_list - TDX_ATTEST_ERROR_MAX -} tdx_attest_error_t; - -#define TDX_UUID_SIZE 16 - -#pragma pack(push, 1) - -#define TDX_UUID_SIZE 16 -typedef struct _tdx_uuid_t -{ - uint8_t d[TDX_UUID_SIZE]; -} tdx_uuid_t; - -#define TDX_SGX_ECDSA_ATTESTATION_ID \ -{ \ - 0xe8, 0x6c, 0x04, 0x6e, 0x8c, 0xc4, 0x4d, 0x95, \ - 0x81, 0x73, 0xfc, 0x43, 0xc1, 0xfa, 0x4f, 0x3f \ -} - -#define TDX_REPORT_DATA_SIZE 64 -typedef struct _tdx_report_data_t -{ - uint8_t d[TDX_REPORT_DATA_SIZE]; -} tdx_report_data_t; - -#define TDX_REPORT_SIZE 1024 -typedef struct _tdx_report_t -{ - uint8_t d[TDX_REPORT_SIZE]; -} tdx_report_t; - -typedef struct _tdx_rtmr_event_t { - uint32_t version; - uint64_t rtmr_index; - uint8_t extend_data[48]; - uint32_t event_type; - uint32_t event_data_size; - uint8_t event_data[]; -} tdx_rtmr_event_t; - -#pragma pack(pop) - -#if defined(__cplusplus) -extern "C" { -#endif - -#ifndef SERVTD_ATTEST -/** - * @brief Request a Quote of the calling TD. - * - * The caller provides data intended to be cryptographically bound to the - * resulting Quote. (This data should not require confidentiality protection.) - * The caller also provides information about the type of Quote signing that - * should be used. - * - * In general, a given platform can create Quotes using - * different cryptographic algorithms or using different vendors’ code/enclaves. - * The att_key_id_list parameter is related to this. It is a list of key IDs - * supported by the eventual verifier of the Quote. How the caller of this - * function obtains this list is outside the scope of the R3AAL. - * - * A default key ID is supported and will be used when att_key_id_list == NULL. - * In this case, the default key ID is returned via the p_att_key_id parameter. - * - * When the function returns successfully, p_quote will point to a buffer - * containing the Quote. This buffer is allocated by the function. Use - * tdx_att_free_quote to free this buffer. - * - * @param p_tdx_report_data [in] Pointer to data that the caller/TD wants to - * cryptographically bind to the Quote, - * typically a hash. Cannot be NULL. - * @param att_key_id_list [in] List (array) of the attestation key IDs supported - * by the Quote verifier. The function compares the - * key IDs in att_key_id_list to the key IDs that - * the platform supports and uses the first match. - * May be NULL. If NULL, the API will use the - * platform’s default key ID. The uuid_t - * corresponding to the key ID that’s used is - * pointed to by p_att_key_id when the function - * returns unless p_att_key_id == NULL. - * @param list_size [in] Size of att_key_id_list in entries. - * @param p_att_key_id [out] The selected attestation key ID when the function - * returns. May be NULL indicating the platform’s - * default key ID - * @param pp_quote [out] Pointer to a pointer that the function will set equal - * to the address of the buffer containing the Quote. The - * function also allocates this buffer. Use - * tdx_att_free_quote to free this buffe - * @param p_quote_size [out] This function will place the size of the Quote, in - * bytes, in the uint32_t pointed to by the - * p_quote_size parameter. May be NULL. - * @param flags [in] Reserved, must be zero. - * @return TDX_ATTEST_SUCCESS: Successfully generated the Quote. - * @return TDX_ATTEST_ERROR_UNEXPECTED: An unexpected internal error occurred. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: The parameter is incorrect - * @return TDX_ATTEST_ERROR_DEVICE_FAILURE: Failed to acess tdx attest device. - * @return TDX_ATTEST_ERROR_REPORT_FAILURE: Failed to get TD report. - * @return TDX_ATTEST_ERROR_VSOCK_FAILURE: Failed read/write in vsock mode - * @return TDX_ATTEST_ERROR_QUOTE_FAILURE: Failed to get quote from QGS - * @return TDX_ATTEST_UNSUPPORTED_ATT_KEY_ID: The platform Quoting - * infrastructure does not support any of the keys described in - * att_key_id_list. - * @return TDX_ATTEST_OUT_OF_MEMORY: Heap memory allocation error in library or - * enclave. - */ -tdx_attest_error_t tdx_att_get_quote( - const tdx_report_data_t *p_tdx_report_data, - const tdx_uuid_t att_key_id_list[], - uint32_t list_size, - tdx_uuid_t *p_att_key_id, - uint8_t **pp_quote, - uint32_t *p_quote_size, - uint32_t flags); - - -/** - * @brief Free the Quote buffer allocated by tdx_att_get_quote. - * - * @param p_quote [in] The value of *p_quote returned by tdx_att_get_quote. - * @return TDX_ATTEST_SUCCESS: Successfully freed the p_quote. - */ -tdx_attest_error_t tdx_att_free_quote( - uint8_t *p_quote); - -/** - * @brief Request a TDX Report of the calling TD. - * - * The caller provides data intended to be cryptographically bound to the - * resulting Report. - * - * @param p_tdx_report_data [in] Pointer to data that the caller/TD wants to - * cryptographically bind to the Quote, typically - * a hash. Cannot be NULL. - * @param p_tdx_report [out] Pointer to the buffer that will contain the - * generated TDX Report. Must not be NULL. - * @return TDX_ATTEST_SUCCESS: Successfully generated the Report. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: p_tdx_report == NULL - * @return TDX_ATTEST_ERROR_DEVICE_FAILURE: Failed to acess tdx attest device. - * @return TDX_ATTEST_ERROR_REPORT_FAILURE: Failed to get TD report. - */ -tdx_attest_error_t tdx_att_get_report( - const tdx_report_data_t *p_tdx_report_data, - tdx_report_t* p_tdx_report); - - -/** - * @brief Extend one of the TDX runtime measurement registers (RTMRs). - * - * RTMR[rtmr_index] = SHA384(RTMR[rtmr_index] || extend_data) - * rtmr_index and extend_data are fields in the structure that is an input of - * this API. - * This API does not return either the new or old value of the specified RTMR. - * The tdx_att_get_report API may be used for this. - * The input to this API includes a description of the “extend data”. This is - * intended to facilitate reconstruction of the RTMR value. This, in turn, - * suggests maintenance of an event log by the callee. Currently, event_data is - * not supported. - * - * @param p_rtmr_event [in] Pointer to structure that contains the index of the - * RTMR to extend, the data with which to extend it and - * a description of the data. - * @return TDX_ATTEST_SUCCESS: Successfully extended the RTMR. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: p_rtmr_event == NULL - * @return TDX_ATTEST_ERROR_UNEXPECTED: An unexpected internal error occurred. - * @return TDX_ATTEST_ERROR_DEVICE_FAILURE: Failed to acess tdx attest device. - * @return TDX_ATTEST_ERROR_EXTEND_FAILURE: Failed to extend data. - * @return TDX_ATTEST_ERROR_NOT_SUPPORTED: p_rtmr_event->event_data_size != 0 - */ -tdx_attest_error_t tdx_att_extend( - const tdx_rtmr_event_t *p_rtmr_event); - - -/** - * @brief Retrieve the list of attestation key IDs supported by the platform. - * - * Specify p_att_key_id_list = NULL to learn the number of entries in the list. - * - * @param p_att_key_id_list [out] List of the attestation key IDs that the - * platform supports. May be NULL. If NULL, the - * API will return the number of entries in the - * list in the uint32_t pointed to by p_list_size - * @param p_list_size [in/out] As input, pointer to a uint32_t specifying the - * size of p_att_key_id_list in entries. As output, - * this function will place the required size, in - * entries, in the uint32_t pointed to by the - * p_list_size parameter. If this value changes, the - * new value will be the required size - * @return TDX_ATTEST_SUCCESS: att_key_id_list populated and p_list_size points - * to a uint32_t that indicates the number of - * entries. - * @return TDX_ATTEST_ERROR_INVALID_PARAMETER: The parameter is incorrect - */ -tdx_attest_error_t tdx_att_get_supported_att_key_ids( - tdx_uuid_t *p_att_key_id_list, - uint32_t *p_list_size); - -#else -__attribute__ ((visibility("default"))) tdx_attest_error_t tdx_att_get_quote_by_report ( - const void *p_tdx_report, - uint32_t tdx_report_size, - void *p_quote, - uint32_t *p_quote_size); -#endif - -#if defined(__cplusplus) -} -#endif - - -#endif - diff --git a/tdx-attest-sys/src/lib.rs b/tdx-attest-sys/src/lib.rs deleted file mode 100644 index abe54931c..000000000 --- a/tdx-attest-sys/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(non_upper_case_globals)] -#![allow(clippy::missing_safety_doc)] - -// SPDX-FileCopyrightText: © 2024 Phala Network -// -// SPDX-License-Identifier: Apache-2.0 - -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/tdx-attest/Cargo.toml b/tdx-attest/Cargo.toml index 1a193024f..3470519d1 100644 --- a/tdx-attest/Cargo.toml +++ b/tdx-attest/Cargo.toml @@ -13,7 +13,6 @@ license.workspace = true [dependencies] anyhow.workspace = true hex.workspace = true -num_enum.workspace = true scale.workspace = true serde.workspace = true serde-human-bytes.workspace = true @@ -22,9 +21,7 @@ thiserror.workspace = true fs-err.workspace = true serde_json = { workspace = true, features = ["alloc"] } sha2.workspace = true - -[target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] -tdx-attest-sys.workspace = true +log.workspace = true [dev-dependencies] insta.workspace = true diff --git a/tdx-attest/src/dummy.rs b/tdx-attest/src/dummy.rs index 12ee199b7..0c595533e 100644 --- a/tdx-attest/src/dummy.rs +++ b/tdx-attest/src/dummy.rs @@ -3,61 +3,26 @@ // SPDX-License-Identifier: Apache-2.0 use cc_eventlog::TdxEventLog; -use num_enum::FromPrimitive; use thiserror::Error; -use crate::{TdxReport, TdxReportData, TdxUuid}; +use crate::TdxReportData; type Result = std::result::Result; -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Error)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] pub enum TdxAttestError { - #[error("unexpected")] - Unexpected, - #[error("invalid parameter")] - InvalidParameter, - #[error("out of memory")] - OutOfMemory, - #[error("vsock failure")] - VsockFailure, - #[error("report failure")] - ReportFailure, - #[error("extend failure")] - ExtendFailure, #[error("not supported")] NotSupported, - #[error("quote failure")] - QuoteFailure, - #[error("busy")] - Busy, - #[error("device failure")] - DeviceFailure, - #[error("invalid rtmr index")] - InvalidRtmrIndex, - #[error("unsupported att key id")] - UnsupportedAttKeyId, - #[num_enum(catch_all)] - #[error("unknown error ({0})")] - UnknownError(u32), } pub fn extend_rtmr(_index: u32, _event_type: u32, _digest: [u8; 48]) -> Result<()> { Err(TdxAttestError::NotSupported) } + pub fn log_rtmr_event(_log: &TdxEventLog) -> Result<()> { Err(TdxAttestError::NotSupported) } -pub fn get_report(_report_data: &TdxReportData) -> Result { - Err(TdxAttestError::NotSupported) -} -pub fn get_quote( - _report_data: &TdxReportData, - _att_key_id_list: Option<&[TdxUuid]>, -) -> Result<(TdxUuid, Vec)> { - let _ = _report_data; - Err(TdxAttestError::NotSupported) -} -pub fn get_supported_att_key_ids() -> Result> { + +pub fn get_quote(_report_data: &TdxReportData) -> Result> { Err(TdxAttestError::NotSupported) } diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index 3cfa4fdc8..0d372704d 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -17,9 +17,6 @@ pub use cc_eventlog as eventlog; pub type Result = std::result::Result; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TdxUuid(pub [u8; 16]); - pub type TdxReportData = [u8; 64]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -27,11 +24,15 @@ pub struct TdxReport(pub [u8; 1024]); pub fn extend_rtmr3(event: &str, payload: &[u8]) -> anyhow::Result<()> { use anyhow::Context; - // This code is not defined in the TCG specification. - // See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf - let event_type = 0x08000001; + let event_type = eventlog::DSTACK_RUNTIME_EVENT_TYPE; let index = 3; - let log = eventlog::TdxEventLog::new(index, event_type, event.to_string(), payload.to_vec()); - extend_rtmr(index, event_type, log.digest).context("Failed to extend RTMR")?; + let log = + eventlog::TdxEventLogEntry::new(index, event_type, event.to_string(), payload.to_vec()); + let digest = log + .digest() + .try_into() + .ok() + .context("Invalid digest size")?; + extend_rtmr(index, event_type, digest).context("Failed to extend RTMR")?; log_rtmr_event(&log).context("Failed to log RTMR event") } diff --git a/tdx-attest/src/linux.rs b/tdx-attest/src/linux.rs index 49ebbbfe1..e25670046 100644 --- a/tdx-attest/src/linux.rs +++ b/tdx-attest/src/linux.rs @@ -2,170 +2,106 @@ // // SPDX-License-Identifier: Apache-2.0 -use anyhow::Context; -use cc_eventlog::TdxEventLog; - -use tdx_attest_sys as sys; +use anyhow::{bail, Context}; +use cc_eventlog::TdxEventLogEntry; use std::io::Write; -use std::ptr; -use std::slice; - -use sys::*; +use std::path::PathBuf; use fs_err as fs; -use num_enum::FromPrimitive; use thiserror::Error; -use crate::TdxReport; +use crate::Result; use crate::TdxReportData; -use crate::{Result, TdxUuid}; -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, Error)] -pub enum TdxAttestError { - #[error("TDX_ATTEST_ERROR_UNEXPECTED")] - Unexpected = _tdx_attest_error_t::TDX_ATTEST_ERROR_UNEXPECTED, - #[error("TDX_ATTEST_ERROR_INVALID_PARAMETER")] - InvalidParameter = _tdx_attest_error_t::TDX_ATTEST_ERROR_INVALID_PARAMETER, - #[error("TDX_ATTEST_ERROR_OUT_OF_MEMORY")] - OutOfMemory = _tdx_attest_error_t::TDX_ATTEST_ERROR_OUT_OF_MEMORY, - #[error("TDX_ATTEST_ERROR_VSOCK_FAILURE")] - VsockFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_VSOCK_FAILURE, - #[error("TDX_ATTEST_ERROR_REPORT_FAILURE")] - ReportFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_REPORT_FAILURE, - #[error("TDX_ATTEST_ERROR_EXTEND_FAILURE")] - ExtendFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_EXTEND_FAILURE, - #[error("TDX_ATTEST_ERROR_NOT_SUPPORTED")] - NotSupported = _tdx_attest_error_t::TDX_ATTEST_ERROR_NOT_SUPPORTED, - #[error("TDX_ATTEST_ERROR_QUOTE_FAILURE")] - QuoteFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_QUOTE_FAILURE, - #[error("TDX_ATTEST_ERROR_BUSY")] - Busy = _tdx_attest_error_t::TDX_ATTEST_ERROR_BUSY, - #[error("TDX_ATTEST_ERROR_DEVICE_FAILURE")] - DeviceFailure = _tdx_attest_error_t::TDX_ATTEST_ERROR_DEVICE_FAILURE, - #[error("TDX_ATTEST_ERROR_INVALID_RTMR_INDEX")] - InvalidRtmrIndex = _tdx_attest_error_t::TDX_ATTEST_ERROR_INVALID_RTMR_INDEX, - #[error("TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID")] - UnsupportedAttKeyId = _tdx_attest_error_t::TDX_ATTEST_ERROR_UNSUPPORTED_ATT_KEY_ID, - #[num_enum(catch_all)] - #[error("unknown tdx attest error ({0})")] - UnknownError(u32), -} +mod configfs; -pub fn get_quote( - report_data: &TdxReportData, - att_key_id_list: Option<&[TdxUuid]>, -) -> Result<(TdxUuid, Vec)> { - let mut att_key_id = TdxUuid([0; TDX_UUID_SIZE as usize]); - let mut quote_ptr = ptr::null_mut(); - let mut quote_size = 0; - - let error = unsafe { - let key_id_list_ptr = att_key_id_list - .map(|list| list.as_ptr() as *const tdx_uuid_t) - .unwrap_or(ptr::null()); - tdx_att_get_quote( - report_data as *const TdxReportData as *const tdx_report_data_t, - key_id_list_ptr, - att_key_id_list.map_or(0, |list| list.len() as u32), - &mut att_key_id as *mut TdxUuid as *mut tdx_uuid_t, - &mut quote_ptr, - &mut quote_size, - 0, - ) - }; - - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } +/// TSM measurements sysfs paths for RTMR extend (kernel 6.17+) +const TSM_MEASUREMENTS_PATH: &str = "/sys/class/misc/tdx_guest/measurements"; - let quote = unsafe { slice::from_raw_parts(quote_ptr, quote_size as usize).to_vec() }; - - unsafe { - tdx_att_free_quote(quote_ptr); - } - - Ok((att_key_id, quote)) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] +pub enum TdxAttestError { + #[error("unexpected error")] + Unexpected, + #[error("invalid parameter")] + InvalidParameter, + #[error("out of memory")] + OutOfMemory, + #[error("not supported")] + NotSupported, + #[error("quote generation failed")] + QuoteFailure, + #[error("busy")] + Busy, + #[error("device failure")] + DeviceFailure, } -pub fn get_report(report_data: &TdxReportData) -> Result { - let mut report = TdxReport([0; TDX_REPORT_SIZE as usize]); - - let error = unsafe { - tdx_att_get_report( - report_data as *const TdxReportData as *const tdx_report_data_t, - &mut report as *mut TdxReport as *mut tdx_report_t, - ) - }; - - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } - - Ok(report) +/// Get TDX quote using configfs interface +/// +/// This uses the kernel's TSM (Trusted Security Module) configfs interface +/// at `/sys/kernel/config/tsm/report/` to generate attestation quotes. +pub fn get_quote(report_data: &TdxReportData) -> Result> { + configfs::get_quote(report_data).map_err(|e| { + log::error!("failed to get quote via configfs: {}", e); + TdxAttestError::QuoteFailure + }) } -pub fn log_rtmr_event(log: &TdxEventLog) -> anyhow::Result<()> { - // Append to event log - let logline = serde_json::to_string(&log).context("Failed to serialize event log")?; +pub fn log_rtmr_event(log: &TdxEventLogEntry) -> anyhow::Result<()> { + let logline = serde_json::to_string(&log).context("failed to serialize event log")?; let logfile_path = std::path::Path::new(cc_eventlog::RUNTIME_EVENT_LOG_FILE); let logfile_dir = logfile_path .parent() - .context("Failed to get event log directory")?; - fs::create_dir_all(logfile_dir).context("Failed to create event log directory")?; + .context("failed to get event log directory")?; + fs::create_dir_all(logfile_dir).context("failed to create event log directory")?; let mut logfile = fs::OpenOptions::new() .append(true) .create(true) .open(logfile_path) - .context("Failed to open event log file")?; + .context("failed to open event log file")?; logfile .write_all(logline.as_bytes()) - .context("Failed to write to event log file")?; + .context("failed to write to event log file")?; logfile .write_all(b"\n") - .context("Failed to write to event log file")?; + .context("failed to write to event log file")?; Ok(()) } -pub fn extend_rtmr(index: u32, event_type: u32, digest: [u8; 48]) -> Result<()> { - let event = tdx_rtmr_event_t { - version: 1, - rtmr_index: index as u64, - extend_data: digest, - event_type, - event_data_size: 0, - event_data: Default::default(), - }; - let error = unsafe { tdx_att_extend(&event) }; - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); +/// Find the TSM measurements sysfs directory +fn find_tsm_measurements_dir() -> Option { + let path = PathBuf::from(TSM_MEASUREMENTS_PATH); + if !path.exists() { + return None; } - Ok(()) + Some(path) } -pub fn get_supported_att_key_ids() -> Result> { - let mut list_size = 0; - let error = unsafe { tdx_att_get_supported_att_key_ids(ptr::null_mut(), &mut list_size) }; - - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); - } - - let mut att_key_id_list = vec![TdxUuid([0; TDX_UUID_SIZE as usize]); list_size as usize]; - - let error = unsafe { - tdx_att_get_supported_att_key_ids( - att_key_id_list.as_mut_ptr() as *mut tdx_uuid_t, - &mut list_size, - ) +/// Extend RTMR using TSM measurements sysfs interface (kernel 6.17+) +fn extend_rtmr_tsm(index: u32, digest: &[u8; 48]) -> anyhow::Result<()> { + let Some(measurements_dir) = find_tsm_measurements_dir() else { + bail!("TSM measurements sysfs not found") }; - if error != _tdx_attest_error_t::TDX_ATTEST_SUCCESS { - return Err(error.into()); + let rtmr_file = measurements_dir.join(format!("rtmr{}:sha384", index)); + + if !rtmr_file.exists() { + bail!("RTMR{} sysfs file not found: {:?}", index, rtmr_file); } - Ok(att_key_id_list) + fs::write(&rtmr_file, digest)?; + Ok(()) +} + +/// Extend RTMR with automatic fallback +/// +/// Uses TSM measurements sysfs (kernel 6.17+) +pub fn extend_rtmr(index: u32, _event_type: u32, digest: [u8; 48]) -> Result<()> { + extend_rtmr_tsm(index, &digest).map_err(|e| { + log::error!("failed to extend RTMR{}: {}", index, e); + TdxAttestError::NotSupported + }) } diff --git a/tdx-attest/src/linux/configfs.rs b/tdx-attest/src/linux/configfs.rs new file mode 100644 index 000000000..3f37a7b98 --- /dev/null +++ b/tdx-attest/src/linux/configfs.rs @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Pure Rust implementation of TDX quote generation using Linux TSM configfs interface +//! +//! This module provides a native Rust implementation that directly uses the kernel's +//! TSM (Trusted Security Module) configfs interface at `/sys/kernel/config/tsm/report/`. + +use std::io::Read; +use std::path::Path; +use std::time::Duration; + +use anyhow::{bail, Context, Result}; +use fs_err as fs; + +use crate::TdxReportData; + +const CONFIGFS_PATH: &str = "/sys/kernel/config/tsm/report/com.intel.dcap"; +const QUOTE_BUF_SIZE: usize = 8 * 1024; // 8KB +const QUOTE_MIN_SIZE: usize = 1020; +const MAX_RETRIES: usize = 3; +const RETRY_DELAY: Duration = Duration::from_millis(100); + +/// Read generation counter from configfs +fn read_generation() -> Result { + let generation_str = fs::read_to_string(format!("{CONFIGFS_PATH}/generation")) + .context("failed to read generation")?; + let generation = generation_str + .trim() + .parse::() + .context("failed to parse generation")?; + Ok(generation) +} + +/// Get TDX quote via configfs interface +/// +/// This function uses generation counters to detect concurrent access: +/// 1. Read generation counter (gen1) +/// 2. Write report_data to inblob +/// 3. Wait for generation to increment (gen2 = gen1 + 1) +/// 4. Read quote from outblob +/// 5. Verify generation hasn't changed (gen3 = gen2) +pub fn get_quote(report_data: &TdxReportData) -> Result> { + // Verify configfs exists + if !Path::new(CONFIGFS_PATH).exists() { + bail!("TSM configfs not found at {CONFIGFS_PATH}. Is TSM_REPORT enabled in kernel?"); + } + + // Read initial generation + let gen1 = read_generation().context("failed to read generation (1)")?; + + // Write report_data to inblob with retry on EBUSY + let mut last_err = None; + for retry in 0..MAX_RETRIES { + match fs::write(format!("{CONFIGFS_PATH}/inblob"), report_data) { + Ok(_) => { + last_err = None; + break; + } + Err(e) => { + last_err = Some(e); + if retry < MAX_RETRIES - 1 { + std::thread::sleep(RETRY_DELAY); + } + } + } + } + + if let Some(err) = last_err { + bail!("failed to write inblob after {MAX_RETRIES} retries: {err}"); + } + + // Wait for generation to increment + let gen2 = loop { + let gen = read_generation().context("failed to read generation (2)")?; + if gen == gen1 { + // Generation not updated yet, sleep briefly + std::thread::sleep(Duration::from_micros(10)); + continue; + } + break gen; + }; + + // Verify generation incremented by exactly 1 + if gen2 != gen1 + 1 { + bail!("concurrent quote generation detected: gen1={gen1}, gen2={gen2}"); + } + + // Read quote from outblob with retry + let mut quote = vec![0u8; QUOTE_BUF_SIZE]; + let mut quote_len = 0; + let mut last_err = None; + + for retry in 0..MAX_RETRIES { + match fs::File::open(format!("{CONFIGFS_PATH}/outblob")) { + Ok(mut file) => match file.read(&mut quote) { + Ok(len) => { + quote_len = len; + last_err = None; + break; + } + Err(e) => { + last_err = Some(e); + if retry < MAX_RETRIES - 1 { + std::thread::sleep(RETRY_DELAY); + } + } + }, + Err(e) => { + last_err = Some(e); + if retry < MAX_RETRIES - 1 { + std::thread::sleep(RETRY_DELAY); + } + } + } + } + + if let Some(err) = last_err { + bail!("failed to read outblob after {MAX_RETRIES} retries: {err}"); + } + + // Validate quote size + if quote_len == 0 { + bail!("empty quote returned from configfs"); + } + + if quote_len < QUOTE_MIN_SIZE { + bail!("quote too small: got {quote_len} bytes, minimum {QUOTE_MIN_SIZE}"); + } + + if quote_len == QUOTE_BUF_SIZE { + bail!("quote may be truncated: exactly {QUOTE_BUF_SIZE} bytes"); + } + + // Verify generation hasn't changed (no concurrent access) + let gen3 = read_generation().context("failed to read generation (3)")?; + if gen3 != gen2 { + bail!("concurrent quote generation detected after read: gen2={gen2}, gen3={gen3}"); + } + + // Truncate to actual size + quote.truncate(quote_len); + + Ok(quote) +} From 4382970d2ba360506a7bab4aa5b02a9cbff30e85 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 17:17:06 +0800 Subject: [PATCH 009/264] tpm: Add tpm crates --- Cargo.toml | 6 + tpm-attest/Cargo.toml | 26 + tpm-attest/src/esapi_impl.rs | 665 +++++++++++++++++++++++++ tpm-attest/src/gcp_ak.rs | 388 +++++++++++++++ tpm-attest/src/lib.rs | 341 +++++++++++++ tpm-attest/tests/tpm_quote_sample.bin | Bin 0 -> 117 bytes tpm-qvl/Cargo.toml | 45 ++ tpm-qvl/certs/gcp-root-ca.pem | 35 ++ tpm-qvl/src/collateral.rs | 282 +++++++++++ tpm-qvl/src/lib.rs | 78 +++ tpm-qvl/src/verify.rs | 670 ++++++++++++++++++++++++++ tpm-types/Cargo.toml | 17 + tpm-types/src/lib.rs | 105 ++++ 13 files changed, 2658 insertions(+) create mode 100644 tpm-attest/Cargo.toml create mode 100644 tpm-attest/src/esapi_impl.rs create mode 100644 tpm-attest/src/gcp_ak.rs create mode 100644 tpm-attest/src/lib.rs create mode 100644 tpm-attest/tests/tpm_quote_sample.bin create mode 100644 tpm-qvl/Cargo.toml create mode 100644 tpm-qvl/certs/gcp-root-ca.pem create mode 100644 tpm-qvl/src/collateral.rs create mode 100644 tpm-qvl/src/lib.rs create mode 100644 tpm-qvl/src/verify.rs create mode 100644 tpm-types/Cargo.toml create mode 100644 tpm-types/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 652a7dfe2..1ae5a7f7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,9 @@ members = [ "ra-rpc", "ra-tls", "tdx-attest", + "tpm-attest", + "tpm-types", + "tpm-qvl", "dstack-util", "iohash", "guest-agent", @@ -67,6 +70,9 @@ cc-eventlog = { path = "cc-eventlog" } supervisor = { path = "supervisor" } supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } +tpm-attest = { path = "tpm-attest" } +tpm-types = { path = "tpm-types" } +tpm-qvl = { path = "tpm-qvl" } certbot = { path = "certbot" } rocket-vsock-listener = { path = "rocket-vsock-listener" } host-api = { path = "host-api", default-features = false } diff --git a/tpm-attest/Cargo.toml b/tpm-attest/Cargo.toml new file mode 100644 index 000000000..01857e319 --- /dev/null +++ b/tpm-attest/Cargo.toml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm-attest" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +hex = { workspace = true, features = ["alloc"] } +serde = { workspace = true, features = ["derive"] } +serde-human-bytes.workspace = true +serde_json = { workspace = true, features = ["std"] } +sha2 = { workspace = true, features = ["oid"] } +tempfile.workspace = true +tracing.workspace = true +fs-err.workspace = true +tpm-types.workspace = true +dstack-types.workspace = true + +# TPM 2.0 TSS ESAPI for low-level TPM operations +tss-esapi = "7.5" diff --git a/tpm-attest/src/esapi_impl.rs b/tpm-attest/src/esapi_impl.rs new file mode 100644 index 000000000..b274fe51d --- /dev/null +++ b/tpm-attest/src/esapi_impl.rs @@ -0,0 +1,665 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Pure tss-esapi implementation of TPM operations +//! +//! This module provides a clean implementation using only tss-esapi, +//! without relying on tpm2-tools command-line utilities. + +use anyhow::{bail, Context as _, Result}; +use std::convert::TryFrom; +use tracing::{debug, warn}; +use tss_esapi::{ + abstraction::nv, + constants::SessionType, + handles::{NvIndexTpmHandle, PersistentTpmHandle, SessionHandle, TpmHandle}, + interface_types::{algorithm::HashingAlgorithm, resource_handles::Hierarchy}, + structures::{DigestValues, PcrSelectionListBuilder, PcrSlot, SymmetricDefinition}, + tcti_ldr::{DeviceConfig, TctiNameConf}, + traits::{Marshall, UnMarshall}, + Context as TssContext, +}; + +use crate::{PcrSelection, PcrValue}; + +/// TPM context using tss-esapi +pub struct EsapiContext { + context: TssContext, +} + +impl EsapiContext { + /// Create a new ESAPI context with the given TCTI path + pub fn new(tcti_path: Option<&str>) -> Result { + use std::str::FromStr; + + let tcti_str = tcti_path.unwrap_or("/dev/tpmrm0"); + + // Strip "device:" prefix if present (tss-esapi expects path without prefix) + let device_path = tcti_str.strip_prefix("device:").unwrap_or(tcti_str); + + let device_config = + DeviceConfig::from_str(device_path).context("failed to parse device config")?; + let tcti = TctiNameConf::Device(device_config); + + let context = TssContext::new(tcti).context("failed to create TSS context")?; + + Ok(Self { context }) + } + + // ==================== NV Operations ==================== + + /// Check if an NV index exists + pub fn nv_exists(&mut self, index: u32) -> Result { + let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; + let nv_index = self.context.tr_from_tpm_public(TpmHandle::NvIndex(handle)); + + match nv_index { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } + + /// Read data from an NV index + pub fn nv_read(&mut self, index: u32) -> Result>> { + use tss_esapi::interface_types::resource_handles::NvAuth; + + let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; + + // Get NV index handle from TPM + let nv_auth_handle = TpmHandle::NvIndex(handle); + let nv_auth_handle = match self.context.execute_without_session(|ctx| { + ctx.tr_from_tpm_public(nv_auth_handle) + .map(|v| NvAuth::NvIndex(v.into())) + }) { + Ok(h) => h, + Err(e) => { + warn!("failed to get NV index handle for 0x{index:08x}: {e}"); + return Ok(None); + } + }; + + // Read NV data with null auth session + match self + .context + .execute_with_nullauth_session(|ctx| nv::read_full(ctx, nv_auth_handle, handle)) + { + Ok(data) => Ok(Some(data.to_vec())), + Err(e) => { + warn!("nv_read failed for index 0x{index:08x}: {e}"); + Ok(None) + } + } + } + + /// Write data to an NV index + pub fn nv_write(&mut self, index: u32, data: &[u8]) -> Result { + use tss_esapi::handles::NvIndexHandle; + use tss_esapi::interface_types::resource_handles::NvAuth; + use tss_esapi::structures::MaxNvBuffer; + + let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; + + // Get NV index handle from TPM + let nv_index = self + .context + .tr_from_tpm_public(TpmHandle::NvIndex(handle)) + .context("failed to get NV index handle")?; + let nv_index_handle = NvIndexHandle::from(nv_index); + let nv_auth = NvAuth::NvIndex(nv_index.into()); + + // Write data in chunks (TPM has max buffer size) + let max_chunk = 1024usize; // Conservative chunk size + let mut offset = 0u16; + + while offset < data.len() as u16 { + let chunk_end = ((offset as usize) + max_chunk).min(data.len()); + let chunk = &data[(offset as usize)..chunk_end]; + + let nv_data = + MaxNvBuffer::try_from(chunk.to_vec()).context("data exceeds NV buffer size")?; + + self.context + .execute_with_nullauth_session(|ctx| { + ctx.nv_write(nv_auth, nv_index_handle, nv_data, offset) + }) + .with_context(|| { + format!("failed to write to NV index 0x{index:08x} at offset {offset}") + })?; + + offset = chunk_end as u16; + } + + debug!("wrote {} bytes to NV index 0x{index:08x}", data.len()); + Ok(true) + } + + /// Define a new NV index + pub fn nv_define(&mut self, index: u32, size: usize, owner_read_write: bool) -> Result { + use tss_esapi::attributes::NvIndexAttributesBuilder; + use tss_esapi::interface_types::resource_handles::Provision; + use tss_esapi::structures::NvPublicBuilder; + + let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; + + // Build NV index attributes + let mut attributes = NvIndexAttributesBuilder::new(); + if owner_read_write { + attributes = attributes.with_owner_write(true).with_owner_read(true); + } + let attributes = attributes + .build() + .context("failed to build NV attributes")?; + + // Build NV public structure + let nv_public = NvPublicBuilder::new() + .with_nv_index(handle) + .with_index_name_algorithm(HashingAlgorithm::Sha256) + .with_index_attributes(attributes) + .with_data_area_size(size) + .build() + .context("failed to build NV public")?; + + // Define NV space + match self.context.execute_with_nullauth_session(|ctx| { + ctx.nv_define_space(Provision::Owner, None, nv_public) + }) { + Ok(_) => { + debug!("defined NV index 0x{index:08x} with size {size}"); + Ok(true) + } + Err(e) => { + warn!("nv_define failed for index 0x{index:08x}: {e}"); + Ok(false) + } + } + } + + /// Undefine (delete) an NV index + pub fn nv_undefine(&mut self, index: u32) -> Result { + use tss_esapi::handles::NvIndexHandle; + use tss_esapi::interface_types::resource_handles::Provision; + + let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; + + // Get NV index handle + let nv_idx = match self.context.tr_from_tpm_public(TpmHandle::NvIndex(handle)) { + Ok(h) => NvIndexHandle::from(h), + Err(e) => { + warn!("failed to get NV index handle for 0x{index:08x}: {e}"); + return Ok(false); + } + }; + + // Undefine NV space + match self + .context + .execute_with_nullauth_session(|ctx| ctx.nv_undefine_space(Provision::Owner, nv_idx)) + { + Ok(_) => { + debug!("undefined NV index 0x{index:08x}"); + Ok(true) + } + Err(e) => { + warn!("nv_undefine failed for index 0x{index:08x}: {e}"); + Ok(false) + } + } + } + + // ==================== PCR Operations ==================== + + /// Read PCR values for the given selection + pub fn pcr_read(&mut self, pcr_selection: &PcrSelection) -> Result> { + let hash_alg = match pcr_selection.bank.as_str() { + "sha256" => HashingAlgorithm::Sha256, + "sha384" => HashingAlgorithm::Sha384, + "sha512" => HashingAlgorithm::Sha512, + _ => bail!( + "unsupported hash algorithm: {bank}", + bank = pcr_selection.bank + ), + }; + + let mut pcr_values = Vec::new(); + + // Read each PCR individually to ensure correct index mapping + for pcr_idx in &pcr_selection.pcrs { + let bit_mask = 1u32 << pcr_idx; + let pcr_slot = PcrSlot::try_from(bit_mask) + .with_context(|| format!("invalid PCR index: {pcr_idx}"))?; + + let pcr_selection_list = PcrSelectionListBuilder::new() + .with_selection(hash_alg, &[pcr_slot]) + .build() + .context("failed to build PCR selection list")?; + + let (_update_counter, _pcr_sel_out, digest_list) = self + .context + .execute_without_session(|ctx| ctx.pcr_read(pcr_selection_list)) + .context("failed to read PCR")?; + + if let Some(digest) = digest_list.value().first() { + pcr_values.push(PcrValue { + index: *pcr_idx, + algorithm: pcr_selection.bank.clone(), + value: digest.value().to_vec(), + }); + } + } + + Ok(pcr_values) + } + + /// Extend a PCR with a hash value + pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { + use tss_esapi::handles::{PcrHandle, PcrTpmHandle}; + use tss_esapi::structures::Digest; + + let hash_alg = match bank { + "sha256" => HashingAlgorithm::Sha256, + "sha384" => HashingAlgorithm::Sha384, + "sha512" => HashingAlgorithm::Sha512, + _ => bail!("unsupported hash algorithm: {bank}"), + }; + + // Create PCR handle from index + let pcr_tpm_handle = + PcrTpmHandle::new(pcr).with_context(|| format!("invalid PCR index: {pcr}"))?; + let object_handle = self + .context + .tr_from_tpm_public(TpmHandle::Pcr(pcr_tpm_handle)) + .context("failed to get PCR handle")?; + + // Create digest from hash bytes + let digest = + Digest::try_from(hash.to_vec()).context("failed to create digest from hash")?; + + // Create DigestValues with the hash algorithm + let mut digest_values = DigestValues::new(); + digest_values.set(hash_alg, digest); + + // Extend PCR - convert object_handle via Into trait + let pcr_handle: PcrHandle = object_handle + .try_into() + .context("failed to convert object handle to PCR handle")?; + self.context + .execute_with_nullauth_session(|ctx| ctx.pcr_extend(pcr_handle, digest_values)) + .with_context(|| format!("failed to extend PCR {pcr} with {bank}"))?; + + debug!("extended PCR {pcr} with {bank} hash"); + Ok(()) + } + + // ==================== Random Number Generation ==================== + + /// Generate random bytes using the TPM's hardware RNG + pub fn get_random(&mut self) -> Result<[u8; N]> { + let random_bytes = self + .context + .get_random(N) + .context("failed to get random bytes from TPM")?; + + let bytes: [u8; N] = random_bytes + .as_slice() + .try_into() + .context("insufficient random bytes from TPM")?; + + Ok(bytes) + } + + // ==================== Primary Key Operations ==================== + + /// Check if a persistent handle exists + pub fn handle_exists(&mut self, handle: u32) -> Result { + let persistent = PersistentTpmHandle::new(handle).context("invalid persistent handle")?; + + match self + .context + .tr_from_tpm_public(TpmHandle::Persistent(persistent)) + { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + } + + /// Create a primary key in the owner hierarchy + /// Uses RSA 2048 storage key template for sealing operations + pub fn create_primary(&mut self) -> Result { + use tss_esapi::attributes::ObjectAttributesBuilder; + use tss_esapi::interface_types::{algorithm::PublicAlgorithm, key_bits::RsaKeyBits}; + use tss_esapi::structures::{ + PublicBuilder, PublicKeyRsa, PublicRsaParametersBuilder, RsaScheme, + SymmetricDefinitionObject, + }; + + // Build RSA 2048 storage key template (standard SRK template) + let object_attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_sensitive_data_origin(true) + .with_user_with_auth(true) + .with_decrypt(true) + .with_restricted(true) + .build() + .context("failed to build object attributes")?; + + let rsa_params = PublicRsaParametersBuilder::new() + .with_symmetric(SymmetricDefinitionObject::AES_128_CFB) + .with_scheme(RsaScheme::Null) + .with_key_bits(RsaKeyBits::Rsa2048) + .with_exponent(Default::default()) // Use default RSA exponent (65537) + .with_is_signing_key(false) + .with_is_decryption_key(true) + .with_restricted(true) + .build() + .context("failed to build RSA parameters")?; + + let public = PublicBuilder::new() + .with_public_algorithm(PublicAlgorithm::Rsa) + .with_name_hashing_algorithm(HashingAlgorithm::Sha256) + .with_object_attributes(object_attributes) + .with_rsa_parameters(rsa_params) + .with_rsa_unique_identifier(PublicKeyRsa::default()) + .build() + .context("failed to build public structure")?; + + // Create primary key in owner hierarchy + let primary_key = self + .context + .execute_with_nullauth_session(|ctx| { + ctx.create_primary( + Hierarchy::Owner, + public, + None, // auth_value + None, // sensitive_data + None, // outside_info + None, // creation_pcr + ) + }) + .context("failed to create primary key")?; + + debug!("created primary key in owner hierarchy"); + Ok(primary_key.key_handle) + } + + /// Make a key persistent at a given handle + pub fn evict_control( + &mut self, + transient_handle: tss_esapi::handles::KeyHandle, + persistent_handle: u32, + ) -> Result { + use tss_esapi::interface_types::resource_handles::Provision; + + let persistent = + PersistentTpmHandle::new(persistent_handle).context("invalid persistent handle")?; + + self.context + .execute_with_nullauth_session(|ctx| { + ctx.evict_control(Provision::Owner, transient_handle.into(), persistent.into()) + }) + .context("failed to make key persistent")?; + + debug!("made key persistent at 0x{persistent_handle:08x}"); + Ok(true) + } + + /// Ensure a persistent primary key exists at the given handle + pub fn ensure_primary_key(&mut self, handle: u32) -> Result { + if self.handle_exists(handle)? { + return Ok(true); + } + + debug!("creating TPM primary key at 0x{handle:08x}..."); + let transient = self.create_primary()?; + self.evict_control(transient, handle) + } + + // ==================== Seal/Unseal Operations ==================== + + /// Seal data to TPM with PCR policy + pub fn seal( + &mut self, + data: &[u8], + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result<(Vec, Vec)> { + use tss_esapi::attributes::ObjectAttributesBuilder; + use tss_esapi::interface_types::algorithm::PublicAlgorithm; + use tss_esapi::structures::{PublicBuilder, SensitiveData}; + + // Get parent key handle + let parent = PersistentTpmHandle::new(parent_handle).context("invalid parent handle")?; + let parent_key = self + .context + .tr_from_tpm_public(TpmHandle::Persistent(parent)) + .context("failed to get parent key handle")?; + + // Build PCR policy + let hash_alg = match pcr_selection.bank.as_str() { + "sha256" => HashingAlgorithm::Sha256, + "sha384" => HashingAlgorithm::Sha384, + "sha512" => HashingAlgorithm::Sha512, + _ => bail!( + "unsupported hash algorithm: {bank}", + bank = pcr_selection.bank + ), + }; + + // Build PCR selection list + let pcr_slots: Result> = pcr_selection + .pcrs + .iter() + .map(|&idx| { + let bit_mask = 1u32 << idx; + PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {idx}")) + }) + .collect(); + let pcr_slots = pcr_slots?; + + let pcr_selection_list = PcrSelectionListBuilder::new() + .with_selection(hash_alg, &pcr_slots) + .build() + .context("failed to build PCR selection list")?; + + // Create session for PCR policy + let session = self + .context + .start_auth_session( + None, + None, + None, + SessionType::Policy, + SymmetricDefinition::AES_128_CFB, + hash_alg, + ) + .context("failed to start policy session")? + .ok_or_else(|| anyhow::anyhow!("no session returned"))?; + + let policy_session = session.try_into()?; + + // Apply PCR policy - requires digest of current PCR values + let (_update_counter, _pcr_sel_out, digest_list) = self + .context + .pcr_read(pcr_selection_list.clone()) + .context("failed to read PCR values")?; + + // Calculate PCR digest + let pcr_digest = if let Some(digest) = digest_list.value().first() { + digest.clone() + } else { + bail!("no PCR digest found"); + }; + + self.context + .policy_pcr(policy_session, pcr_digest, pcr_selection_list.clone()) + .context("failed to set PCR policy")?; + + // Get policy digest + let policy_digest = self + .context + .policy_get_digest(policy_session) + .context("failed to get policy digest")?; + + // Flush session + let session_handle: SessionHandle = session.into(); + self.context + .flush_context(session_handle.into()) + .context("failed to flush policy session")?; + + // Build sealed data object attributes + let object_attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_user_with_auth(false) + .with_admin_with_policy(true) + .build() + .context("failed to build object attributes")?; + + // Build public structure for sealed object + let public = PublicBuilder::new() + .with_public_algorithm(PublicAlgorithm::KeyedHash) + .with_name_hashing_algorithm(hash_alg) + .with_object_attributes(object_attributes) + .with_auth_policy(policy_digest) + .build() + .context("failed to build public structure")?; + + // Create sealed data (sensitive data) + let sensitive_data = + SensitiveData::try_from(data.to_vec()).context("failed to create sensitive data")?; + + // Create sealed object + let create_result = self + .context + .execute_with_nullauth_session(|ctx| { + ctx.create( + parent_key.into(), + public, + None, // auth_value + Some(sensitive_data), + None, // outside_info + None, // creation_pcr + ) + }) + .context("failed to seal data")?; + + debug!("sealed {} bytes to TPM with PCR policy", data.len()); + + // Marshal public and private parts + let pub_bytes = create_result + .out_public + .marshall() + .context("failed to marshal public part")?; + let priv_bytes = create_result.out_private.value().to_vec(); + + Ok((pub_bytes, priv_bytes)) + } + + /// Unseal data from TPM with PCR policy + pub fn unseal( + &mut self, + pub_bytes: &[u8], + priv_bytes: &[u8], + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result> { + use tss_esapi::structures::{Private, Public, SymmetricDefinition}; + + // Get parent key handle + let parent = PersistentTpmHandle::new(parent_handle).context("invalid parent handle")?; + let parent_key = self + .context + .tr_from_tpm_public(TpmHandle::Persistent(parent)) + .context("failed to get parent key handle")?; + + // Unmarshal public and private parts + let public = Public::unmarshall(pub_bytes).context("failed to unmarshal public part")?; + let private = + Private::try_from(priv_bytes.to_vec()).context("failed to create private structure")?; + + // Load sealed object + let sealed_handle = self + .context + .execute_with_nullauth_session(|ctx| ctx.load(parent_key.into(), private, public)) + .context("failed to load sealed object")?; + + // Build PCR policy for unsealing + let hash_alg = match pcr_selection.bank.as_str() { + "sha256" => HashingAlgorithm::Sha256, + "sha384" => HashingAlgorithm::Sha384, + "sha512" => HashingAlgorithm::Sha512, + _ => bail!( + "unsupported hash algorithm: {bank}", + bank = pcr_selection.bank + ), + }; + + let pcr_slots: Result> = pcr_selection + .pcrs + .iter() + .map(|&idx| { + let bit_mask = 1u32 << idx; + PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {idx}")) + }) + .collect(); + let pcr_slots = pcr_slots?; + + let pcr_selection_list = PcrSelectionListBuilder::new() + .with_selection(hash_alg, &pcr_slots) + .build() + .context("failed to build PCR selection list")?; + + // Create policy session + let session = self + .context + .start_auth_session( + None, + None, + None, + SessionType::Policy, + SymmetricDefinition::AES_128_CFB, + hash_alg, + ) + .context("failed to start policy session")? + .ok_or_else(|| anyhow::anyhow!("no session returned"))?; + + let policy_session = session.try_into()?; + + // Apply PCR policy - requires digest of current PCR values + let (_update_counter, _pcr_sel_out, digest_list) = self + .context + .pcr_read(pcr_selection_list.clone()) + .context("failed to read PCR values")?; + + let pcr_digest = if let Some(digest) = digest_list.value().first() { + digest.clone() + } else { + bail!("no PCR digest found"); + }; + + self.context + .policy_pcr(policy_session, pcr_digest, pcr_selection_list) + .context("failed to set PCR policy")?; + + // Unseal with policy session + let unsealed = self + .context + .execute_with_session(Some(session), |ctx| ctx.unseal(sealed_handle.into())) + .context("failed to unseal data (PCR values may have changed)")?; + + // Flush handles + self.context + .flush_context(sealed_handle.into()) + .context("failed to flush sealed object handle")?; + let session_handle: SessionHandle = session.into(); + self.context + .flush_context(session_handle.into()) + .context("failed to flush policy session")?; + + let data = unsealed.to_vec(); + debug!("unsealed {} bytes from TPM", data.len()); + + Ok(data) + } +} diff --git a/tpm-attest/src/gcp_ak.rs b/tpm-attest/src/gcp_ak.rs new file mode 100644 index 000000000..4630cac7e --- /dev/null +++ b/tpm-attest/src/gcp_ak.rs @@ -0,0 +1,388 @@ +//! GCP vTPM pre-provisioned AK loading using tss-esapi +//! +//! This module provides native Rust implementation for loading GCP's +//! pre-provisioned Attestation Key using the TSS2 ESAPI. + +use std::str::FromStr; + +use anyhow::{Context as _, Result}; +use sha2::Digest as _; +use tracing::debug; +use tss_esapi::{ + handles::{KeyHandle, NvIndexTpmHandle, TpmHandle}, + interface_types::resource_handles::{Hierarchy, NvAuth}, + structures::Public, + tcti_ldr::{DeviceConfig, TctiNameConf}, + traits::UnMarshall, + Context as TssContext, +}; + +use crate::TpmEventLog; + +/// GCP vTPM NV indices for pre-provisioned AK +pub mod gcp_nv_index { + /// RSA AK certificate (DER format) + pub const AK_RSA_CERT: u32 = 0x01C10000; + /// RSA AK template (TPM2B_PUBLIC format) + pub const AK_RSA_TEMPLATE: u32 = 0x01C10001; + /// ECC AK certificate (DER format) + pub const AK_ECC_CERT: u32 = 0x01C10002; + /// ECC AK template (TPM2B_PUBLIC format) + pub const AK_ECC_TEMPLATE: u32 = 0x01C10003; +} + +/// Load GCP pre-provisioned ECC AK using tss-esapi +/// +/// This function: +/// 1. Reads the AK template from NV index 0x01C10003 +/// 2. Creates a primary key under Endorsement hierarchy with the template +/// 3. TPM deterministically recreates the same key pair (same template + same parent) +/// +/// # Parameters +/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) +/// +/// # Returns +/// - `Ok((TssContext, KeyHandle))` - TSS context and handle to the loaded AK +/// - `Err(_)` - Failed to load AK (not on GCP vTPM, or access error) +pub fn load_gcp_ak_ecc(tcti_path: Option<&str>) -> Result<(TssContext, KeyHandle)> { + debug!("loading GCP pre-provisioned ECC AK with tss-esapi..."); + + // Create TSS context + use std::str::FromStr; + let tcti_str = tcti_path.unwrap_or("/dev/tpmrm0"); + let device_path = tcti_str.trim_start_matches("device:"); + let device_config = + DeviceConfig::from_str(device_path).context("failed to parse device config")?; + let tcti = TctiNameConf::Device(device_config); + let mut context = TssContext::new(tcti).context("failed to create TSS context")?; + + // Read AK template from NV + let template_bytes = read_nv_data(&mut context, gcp_nv_index::AK_ECC_TEMPLATE) + .context("failed to read ECC AK template from NV 0x01C10003")?; + + debug!( + "read ECC AK template from NV: {} bytes", + template_bytes.len() + ); + + // Parse template as TPM2B_PUBLIC + let public = Public::unmarshall(&template_bytes) + .context("failed to parse ECC AK template as TPM2B_PUBLIC")?; + + // Create primary key under Endorsement hierarchy with null auth session + let ak_handle = context + .execute_with_nullauth_session(|ctx| { + ctx.create_primary(Hierarchy::Endorsement, public, None, None, None, None) + }) + .context("failed to create primary ECC AK")? + .key_handle; + + debug!("✓ successfully loaded GCP pre-provisioned ECC AK (handle: {ak_handle:?})"); + + Ok((context, ak_handle)) +} + +/// Load GCP pre-provisioned RSA AK using tss-esapi +/// +/// This function: +/// 1. Reads the AK template from NV index 0x01C10001 +/// 2. Creates a primary key under Endorsement hierarchy with the template +/// 3. TPM deterministically recreates the same key pair (same template + same parent) +/// +/// # Parameters +/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) +/// +/// # Returns +/// - `Ok((TssContext, KeyHandle))` - TSS context and handle to the loaded AK +/// - `Err(_)` - Failed to load AK (not on GCP vTPM, or access error) +pub fn load_gcp_ak_rsa(tcti_path: Option<&str>) -> Result<(TssContext, KeyHandle)> { + debug!("loading GCP pre-provisioned RSA AK with tss-esapi..."); + + // Create TSS context + use std::str::FromStr; + // Strip "device:" prefix if present (from TpmContext tcti format) + let tcti_str = tcti_path.unwrap_or("/dev/tpmrm0"); + let device_path = tcti_str.trim_start_matches("device:"); + let device_config = + DeviceConfig::from_str(device_path).context("failed to parse device config")?; + let tcti = TctiNameConf::Device(device_config); + let mut context = TssContext::new(tcti).context("failed to create TSS context")?; + + // Read AK template from NV + let template_bytes = read_nv_data(&mut context, gcp_nv_index::AK_RSA_TEMPLATE) + .context("failed to read AK template from NV 0x01C10001")?; + + debug!("read AK template from NV: {} bytes", template_bytes.len()); + + // Parse template as TPM2B_PUBLIC + let public = Public::unmarshall(&template_bytes) + .context("failed to parse AK template as TPM2B_PUBLIC")?; + + // Create primary key under Endorsement hierarchy with null auth session + // This recreates the pre-provisioned AK because TPM CreatePrimary is deterministic + let ak_handle = context + .execute_with_nullauth_session(|ctx| { + ctx.create_primary( + Hierarchy::Endorsement, + public, + None, // auth_value + None, // sensitive_data + None, // outside_info + None, // creation_pcr + ) + }) + .context("failed to create primary AK")? + .key_handle; + + debug!("✓ successfully loaded GCP pre-provisioned AK (handle: {ak_handle:?})"); + + Ok((context, ak_handle)) +} + +/// Key algorithm preference for quote generation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyAlgorithm { + /// Prefer ECC, fallback to RSA + Auto, + /// Use ECC only (fails if not available) + Ecc, + /// Use RSA only (fails if not available) + Rsa, +} + +impl FromStr for KeyAlgorithm { + type Err = anyhow::Error; + + /// Parse from string ("auto", "ecc", or "rsa") + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "auto" => Ok(KeyAlgorithm::Auto), + "ecc" | "ecdsa" => Ok(KeyAlgorithm::Ecc), + "rsa" | "rsassa" => Ok(KeyAlgorithm::Rsa), + _ => anyhow::bail!("invalid key algorithm: {s}. Use 'auto', 'ecc', or 'rsa'"), + } + } +} + +/// Generate a TPM quote using GCP pre-provisioned AK (prefers ECC) +/// +/// This function: +/// 1. Loads the GCP pre-provisioned AK (tries ECC first, then falls back to RSA) +/// 2. Reads the specified PCR values +/// 3. Generates a quote signed by the AK +/// 4. Reads the AK certificate from NV +/// 5. Returns a complete TpmQuote structure +/// +/// # Parameters +/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) +/// - `qualifying_data`: Nonce/challenge data to include in quote +/// - `pcr_selection`: PCR registers to include in quote +/// +/// # Returns +/// - `Ok(TpmQuote)` - Complete quote with signature and certificate +/// - `Err(_)` - Failed to generate quote +pub fn create_quote_with_gcp_ak( + tcti_path: Option<&str>, + qualifying_data: &[u8; 32], + pcr_selection: &crate::PcrSelection, +) -> Result { + create_quote_with_gcp_ak_algo( + tcti_path, + qualifying_data, + pcr_selection, + KeyAlgorithm::Auto, + ) +} + +/// Generate a TPM quote using GCP pre-provisioned AK with manual algorithm selection +/// +/// This function allows specifying which key algorithm to use (ECC, RSA, or Auto). +/// +/// # Parameters +/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) +/// - `qualifying_data`: Nonce/challenge data to include in quote +/// - `pcr_selection`: PCR registers to include in quote +/// - `key_algo`: Key algorithm preference (Auto, Ecc, or Rsa) +/// +/// # Returns +/// - `Ok(TpmQuote)` - Complete quote with signature and certificate +/// - `Err(_)` - Failed to generate quote +pub fn create_quote_with_gcp_ak_algo( + tcti_path: Option<&str>, + qualifying_data: &[u8; 32], + pcr_selection: &crate::PcrSelection, + key_algo: KeyAlgorithm, +) -> Result { + use tss_esapi::interface_types::algorithm::HashingAlgorithm; + use tss_esapi::structures::{Data, PcrSelectionListBuilder, PcrSlot, SignatureScheme}; + use tss_esapi::traits::Marshall; + + let platform = dstack_types::Platform::detect().context("Unsupported platform")?; + + debug!("generating TPM quote with GCP pre-provisioned AK..."); + + // Load GCP pre-provisioned AK based on algorithm preference + let (mut context, ak_handle, ak_cert_nv_index) = match key_algo { + KeyAlgorithm::Auto => { + // Try ECC first (better performance), fallback to RSA + match load_gcp_ak_ecc(tcti_path) { + Ok((ctx, handle)) => { + debug!("✓ using ECC AK for quote"); + (ctx, handle, gcp_nv_index::AK_ECC_CERT) + } + Err(e) => { + debug!("ECC AK not available, falling back to RSA: {e}"); + let (ctx, handle) = load_gcp_ak_rsa(tcti_path)?; + debug!("✓ using RSA AK for quote"); + (ctx, handle, gcp_nv_index::AK_RSA_CERT) + } + } + } + KeyAlgorithm::Ecc => { + // Use ECC only + let (ctx, handle) = load_gcp_ak_ecc(tcti_path).context( + "failed to load ECC AK (use --key-algo=rsa or --key-algo=auto for fallback)", + )?; + debug!("✓ using ECC AK for quote"); + (ctx, handle, gcp_nv_index::AK_ECC_CERT) + } + KeyAlgorithm::Rsa => { + // Use RSA only + let (ctx, handle) = load_gcp_ak_rsa(tcti_path).context("failed to load RSA AK")?; + debug!("✓ using RSA AK for quote"); + (ctx, handle, gcp_nv_index::AK_RSA_CERT) + } + }; + + // Build PCR selection list for tss-esapi + let pcr_selection_list = PcrSelectionListBuilder::new(); + + // Convert hash algorithm string to HashingAlgorithm enum + let hash_alg = match pcr_selection.bank.as_str() { + "sha256" => HashingAlgorithm::Sha256, + "sha384" => HashingAlgorithm::Sha384, + "sha512" => HashingAlgorithm::Sha512, + _ => anyhow::bail!( + "unsupported hash algorithm: {bank}", + bank = pcr_selection.bank + ), + }; + + // Build all PCR slots at once + // PcrSlot uses bit mask representation: PCR 0 = bit 0 (0x1), PCR 1 = bit 1 (0x2), etc. + let mut pcr_slots = Vec::new(); + for pcr_idx in &pcr_selection.pcrs { + let bit_mask = 1u32 << pcr_idx; + let pcr_slot = + PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {pcr_idx}"))?; + pcr_slots.push(pcr_slot); + } + + let pcr_selection_list = pcr_selection_list + .with_selection(hash_alg, &pcr_slots) + .build() + .context("failed to build PCR selection list")?; + + // Create qualifying data structure. + let qual_data = + Data::try_from(qualifying_data.to_vec()).context("failed to create qualifying data")?; + + // Use default signing scheme (RSASSA with SHA256) + let signing_scheme = SignatureScheme::Null; + + // Generate quote + debug!("calling TPM Quote command..."); + let (attest, signature) = context + .execute_with_nullauth_session(|ctx| { + ctx.quote( + ak_handle, + qual_data, + signing_scheme, + pcr_selection_list.clone(), + ) + }) + .context("failed to generate quote")?; + + debug!("✓ quote generated successfully"); + + // Marshall attest structure to bytes (TPMS_ATTEST) + let message = attest.marshall().context("failed to marshall attest")?; + + // Marshall signature to bytes (TPMT_SIGNATURE) + let signature = signature + .marshall() + .context("failed to marshall signature")?; + + // Read PCR values - read each PCR individually to ensure correct mapping + let mut pcr_values = Vec::new(); + for pcr_idx in &pcr_selection.pcrs { + // Build selection for single PCR + let bit_mask = 1u32 << pcr_idx; + let pcr_slot = + PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {pcr_idx}"))?; + + let single_pcr_sel = PcrSelectionListBuilder::new() + .with_selection(hash_alg, &[pcr_slot]) + .build() + .context("failed to build single PCR selection")?; + + let (_update_counter, _pcr_sel_out, digest_list) = context + .execute_without_session(|ctx| ctx.pcr_read(single_pcr_sel)) + .context("failed to read PCR value")?; + + // Get the first (and only) digest + if let Some(digest) = digest_list.value().first() { + pcr_values.push(crate::PcrValue { + index: *pcr_idx, + algorithm: pcr_selection.bank.clone(), + value: digest.value().to_vec(), + }); + } + } + + // Read AK certificate from NV (ECC or RSA depending on which was loaded) + let ak_cert = read_nv_data(&mut context, ak_cert_nv_index) + .context("failed to read AK certificate from NV")?; + + debug!( + "✓ AK certificate read from NV 0x{ak_cert_nv_index:08x}: {} bytes", + ak_cert.len() + ); + + let event_log = TpmEventLog::from_kernel_file() + .context("Failed to read TPM event log")? + .events; + + Ok(crate::TpmQuote { + message, + signature, + pcr_values, + ak_cert, + platform, + event_log, + }) +} + +/// Read data from TPM NV index +fn read_nv_data(context: &mut TssContext, nv_index: u32) -> Result> { + use tss_esapi::abstraction::nv; + + // Create NV index TPM handle + let nv_idx = NvIndexTpmHandle::new(nv_index).context("invalid NV index")?; + + // Get NV index handle from TPM + let nv_auth_handle = TpmHandle::NvIndex(nv_idx); + let nv_auth_handle = context + .execute_without_session(|ctx| { + ctx.tr_from_tpm_public(nv_auth_handle) + .map(|v| NvAuth::NvIndex(v.into())) + }) + .context("failed to get NV index handle")?; + + // Read NV data with null auth session + let data = context + .execute_with_nullauth_session(|ctx| nv::read_full(ctx, nv_auth_handle, nv_idx)) + .context("failed to read NV data")?; + + Ok(data.to_vec()) +} diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs new file mode 100644 index 000000000..73875062a --- /dev/null +++ b/tpm-attest/src/lib.rs @@ -0,0 +1,341 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 Attestation Library +//! +//! This module provides functionality for generating TPM attestation quotes +//! on the device side. It handles PCR operations, sealing, unsealing, NV storage, +//! and quote generation using the TPM2 Software Stack (tss-esapi). +//! +//! This follows the same architecture as tdx-attest: device-side attestation only. +//! For quote verification, see the tpm-qvl crate. + +use anyhow::{bail, Context, Result}; +use std::path::Path; +use tracing::{debug, warn}; + +// Re-export tpm-types +pub use tpm_types::{PcrSelection, PcrValue, TpmEvent, TpmEventLog, TpmQuote}; + +mod esapi_impl; +use esapi_impl::EsapiContext; + +pub const PRIMARY_KEY_HANDLE: u32 = 0x81000100; +pub const SEALED_NV_INDEX: u32 = 0x01801101; + +/// PCR selection for DStack +/// 0: The firmware version and NonHostInfo (representing the memory encryption technology) +/// 2: The uki image (kernel + initrd + initramfs) +/// 14: The app compose hash +pub const APP_PCR: u32 = 14; +pub fn dstack_pcr_policy() -> PcrSelection { + PcrSelection::sha256(&[0, 2, APP_PCR]) +} + +pub struct TpmContext { + tcti: String, +} + +impl std::fmt::Debug for TpmContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TpmContext") + .field("tcti", &self.tcti) + .finish() + } +} + +#[derive(Debug, Clone)] +pub struct SealedBlob { + pub data: Vec, +} + +impl SealedBlob { + pub fn new(data: Vec) -> Self { + Self { data } + } + + pub fn from_parts(pub_data: &[u8], priv_data: &[u8]) -> Self { + let mut data = Vec::with_capacity(pub_data.len() + priv_data.len()); + data.extend_from_slice(pub_data); + data.extend_from_slice(priv_data); + Self { data } + } + + pub fn split(&self) -> Result<(Vec, Vec)> { + if self.data.len() < 4 { + bail!("sealed blob too small"); + } + + let pub_size = u16::from_be_bytes([self.data[0], self.data[1]]) as usize; + if self.data.len() < 2 + pub_size + 2 { + bail!("sealed blob truncated at pub"); + } + + let priv_offset = 2 + pub_size; + let priv_size = + u16::from_be_bytes([self.data[priv_offset], self.data[priv_offset + 1]]) as usize; + if self.data.len() < priv_offset + 2 + priv_size { + bail!("sealed blob truncated at priv"); + } + + let pub_data = self.data[..2 + pub_size].to_vec(); + let priv_data = self.data[priv_offset..priv_offset + 2 + priv_size].to_vec(); + + Ok((pub_data, priv_data)) + } +} + +impl TpmContext { + pub fn open(tcti: Option<&str>) -> Result { + match tcti { + Some(t) => Self::new(t), + None => Self::detect(), + } + } + + pub fn detect() -> Result { + let tcti = if Path::new("/dev/tpmrm0").exists() { + "/dev/tpmrm0" + } else if Path::new("/dev/tpm0").exists() { + "/dev/tpm0" + } else { + bail!("TPM device not found"); + }; + Self::new(tcti) + } + + pub fn new(tcti: &str) -> Result { + Ok(Self { + tcti: tcti.to_string(), + }) + } + + fn create_esapi_context(&self) -> Result { + EsapiContext::new(Some(&self.tcti)) + } + + pub fn nv_exists(&self, index: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_exists(index) + } + + pub fn nv_define(&self, index: u32, size: usize, _attributes: &str) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_define(index, size, true) + } + + pub fn nv_undefine(&self, index: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_undefine(index) + } + + pub fn nv_read(&self, index: u32) -> Result>> { + let mut ctx = self.create_esapi_context()?; + ctx.nv_read(index) + } + + pub fn nv_write(&self, index: u32, data: &[u8]) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.nv_write(index, data) + } + + pub fn handle_exists(&self, handle: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.handle_exists(handle) + } + + pub fn ensure_primary_key(&self, handle: u32) -> Result { + let mut ctx = self.create_esapi_context()?; + ctx.ensure_primary_key(handle) + } + + pub fn pcr_extend(&self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { + let mut ctx = self.create_esapi_context()?; + ctx.pcr_extend(pcr, hash, bank) + } + + pub fn pcr_extend_sha256(&self, pcr: u32, hash: &[u8; 32]) -> Result<()> { + self.pcr_extend(pcr, hash, "sha256") + } + + pub fn dump_pcr_values(&self, selection: &PcrSelection) { + match self + .create_esapi_context() + .and_then(|mut ctx| ctx.pcr_read(selection)) + { + Ok(values) => { + debug!("PCR values ({}):", selection.to_arg()); + for pv in values { + debug!(" PCR[{}] = {}", pv.index, hex::encode(&pv.value)); + } + } + Err(e) => { + warn!("failed to read PCR values: {e}"); + } + } + } + + pub fn get_random(&self) -> Result<[u8; N]> { + let mut ctx = self.create_esapi_context()?; + ctx.get_random::() + } + + pub fn seal( + &self, + data: &[u8], + nv_index: u32, + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result<()> { + let mut ctx = self.create_esapi_context()?; + + ctx.ensure_primary_key(parent_handle)?; + + let (pub_bytes, priv_bytes) = ctx.seal(data, parent_handle, pcr_selection)?; + + let sealed_blob = SealedBlob::from_parts(&pub_bytes, &priv_bytes); + + if !ctx.nv_exists(nv_index)? { + ctx.nv_define(nv_index, sealed_blob.data.len(), true)?; + } + + ctx.nv_write(nv_index, &sealed_blob.data)?; + + debug!("sealed data to NV index 0x{nv_index:08x}"); + Ok(()) + } + + pub fn unseal_to_vec( + &self, + nv_index: u32, + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result>> { + let mut ctx = self.create_esapi_context()?; + + let sealed_data = match ctx.nv_read(nv_index)? { + Some(data) => data, + None => return Ok(None), + }; + + let sealed_blob = SealedBlob::new(sealed_data); + let (pub_bytes, priv_bytes) = sealed_blob.split()?; + + let data = ctx.unseal(&pub_bytes, &priv_bytes, parent_handle, pcr_selection)?; + + debug!("unsealed data from NV index 0x{nv_index:08x}"); + Ok(Some(data)) + } + + pub fn unseal( + &self, + nv_index: u32, + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result> { + match self.unseal_to_vec(nv_index, parent_handle, pcr_selection)? { + Some(data) => { + let array: [u8; N] = data + .try_into() + .ok() + .context("unsealed data size mismatch")?; + Ok(Some(array)) + } + None => Ok(None), + } + } + + pub fn create_quote( + &self, + qualifying_data: &[u8; 32], + pcr_selection: &PcrSelection, + ) -> Result { + gcp_ak::create_quote_with_gcp_ak(Some(&self.tcti), qualifying_data, pcr_selection) + } + + pub fn create_quote_with_algo( + &self, + qualifying_data: &[u8; 32], + pcr_selection: &PcrSelection, + key_algo: KeyAlgorithm, + ) -> Result { + gcp_ak::create_quote_with_gcp_ak_algo( + Some(&self.tcti), + qualifying_data, + pcr_selection, + key_algo, + ) + } + + pub fn read_ak_cert(&self) -> Result>> { + const AK_RSA_CERT_NV_INDEX: u32 = 0x01C10000; + const AK_ECC_CERT_NV_INDEX: u32 = 0x01C10002; + + let mut ctx = self.create_esapi_context()?; + + if let Some(cert) = ctx.nv_read(AK_RSA_CERT_NV_INDEX)? { + debug!( + "read AK certificate from NV index 0x{AK_RSA_CERT_NV_INDEX:08x} ({} bytes)", + cert.len() + ); + return Ok(Some(cert)); + } + + if let Some(cert) = ctx.nv_read(AK_ECC_CERT_NV_INDEX)? { + debug!( + "read AK certificate from NV index 0x{AK_ECC_CERT_NV_INDEX:08x} ({} bytes)", + cert.len() + ); + return Ok(Some(cert)); + } + + warn!("AK certificate not found in TPM NV storage (expected on GCP vTPM)"); + Ok(None) + } + + pub fn read_event_log(&self, pcr_index: u32) -> Result> { + let event_log = + TpmEventLog::from_kernel_file().context("Failed to read TPM Event Log from kernel")?; + + Ok(event_log.filter_by_pcr(pcr_index)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pcr_selection_to_string() { + let sel = PcrSelection::sha256(&[0, 1, 2, 7]); + assert_eq!(sel.to_arg(), "sha256:0,1,2,7"); + } + + #[test] + fn test_sealed_blob_split() { + let pub_data = vec![0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]; + let priv_data = vec![0x00, 0x03, 0xAA, 0xBB, 0xCC]; + let mut blob_data = Vec::new(); + blob_data.extend_from_slice(&pub_data); + blob_data.extend_from_slice(&priv_data); + + let blob = SealedBlob::new(blob_data); + let (pub_part, priv_part) = blob.split().unwrap(); + + assert_eq!(pub_part, pub_data); + assert_eq!(priv_part, priv_data); + } + + #[test] + fn test_default_pcr_policy() { + let policy = dstack_pcr_policy(); + assert_eq!(policy.to_arg(), "sha256:0,1,2,3,4,5,6,7,8,9,14"); + } +} + +mod gcp_ak; +pub use gcp_ak::{ + create_quote_with_gcp_ak, create_quote_with_gcp_ak_algo, gcp_nv_index, load_gcp_ak_ecc, + load_gcp_ak_rsa, KeyAlgorithm, +}; diff --git a/tpm-attest/tests/tpm_quote_sample.bin b/tpm-attest/tests/tpm_quote_sample.bin new file mode 100644 index 0000000000000000000000000000000000000000..4866ce803850cb44fe152e80276bb4500a642b8e GIT binary patch literal 117 zcmew#;_Tia!Jx#z9p{@16y6@sAi-giOLJ z7=VD)_u_jXB>-YGDu}TPGKgt_*$mvw>_CRXjeFXUUM!Y8d*4>(;bMU=aRz^lgF@B& O{7+w<6(7-2<^cd#O(mlM literal 0 HcmV?d00001 diff --git a/tpm-qvl/Cargo.toml b/tpm-qvl/Cargo.toml new file mode 100644 index 000000000..6f66be59f --- /dev/null +++ b/tpm-qvl/Cargo.toml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm-qvl" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +hex.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true, features = ["std"] } +tracing.workspace = true +dstack-types.workspace = true + +# Cryptographic verification +rsa = "0.9" +p256 = { version = "0.13", features = ["ecdsa", "pem"] } +x509-parser = "0.16" +nom = "7.1" +base64 = "0.22" +sha2 = { workspace = true, features = ["oid"] } + +# Certificate chain verification +rustls-webpki = { version = "0.103.8", features = ["alloc", "ring"] } +rustls-pki-types = "1.13.1" +dcap-qvl-webpki = { version = "0.103", features = ["alloc", "ring"] } +pem = "3.0" + +# TPM quote data structures +tpm-types.workspace = true + +# CRL download (optional) +reqwest = { version = "0.12", default-features = false, features = [ + "rustls-tls", + "blocking", +], optional = true } + +[features] +default = ["crl-download"] +crl-download = ["reqwest"] diff --git a/tpm-qvl/certs/gcp-root-ca.pem b/tpm-qvl/certs/gcp-root-ca.pem new file mode 100644 index 000000000..080fdeafb --- /dev/null +++ b/tpm-qvl/certs/gcp-root-ca.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +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 +av16U4E= +-----END CERTIFICATE----- diff --git a/tpm-qvl/src/collateral.rs b/tpm-qvl/src/collateral.rs new file mode 100644 index 000000000..2c08b6b56 --- /dev/null +++ b/tpm-qvl/src/collateral.rs @@ -0,0 +1,282 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Collateral retrieval module +//! +//! This module implements the first step of dcap-qvl architecture: +//! extracting certificate chain information and downloading CRLs. + +use anyhow::{bail, Context, Result}; +use tracing::{debug, warn}; +use x509_parser::{extensions::DistributionPointName, prelude::*}; + +use tpm_types::TpmQuote; + +use crate::{get_root_ca, verify::VerifiedReport, QuoteCollateral}; + +pub async fn get_collateral_and_verify(quote: &TpmQuote) -> Result { + let root_ca_pem = get_root_ca(quote.platform).context("failed to get root CA")?; + let collateral = get_collateral(quote, root_ca_pem).await?; + crate::verify::verify_quote_with_ca(quote, &collateral, root_ca_pem).map_err(Into::into) +} + +pub async fn get_collateral(quote: &TpmQuote, root_ca_pem: &str) -> Result { + debug!("fetching quote collateral (intermediate cert chain + CRLs)"); + + let ak_cert_der = "e.ak_cert; + debug!("AK certificate (leaf) found: {} bytes", ak_cert_der.len()); + + // Build certificate chain from device (via AIA) + let chain_ders = build_cert_chain(ak_cert_der)?; + // Download CRLs from device-provided cert chain + let crls = download_crls_for_certs(&chain_ders)?; + + // Download CRL from verifier-provided root CA + let root_ca_crl = { + let root_ca_der = + extract_certs_webpki(root_ca_pem.as_bytes()).context("failed to parse root CA PEM")?; + if root_ca_der.len() != 1 { + bail!("expected 1 root CA, found {}", root_ca_der.len()); + } + download_crl_for_cert(&root_ca_der[0])? + }; + + debug!( + "✓ collateral fetched: {} intermediate CRL(s), root CA CRL: {}", + crls.len(), + if root_ca_crl.is_some() { "yes" } else { "no" } + ); + let cert_chain_pem = ders_to_pem(&chain_ders)?; + Ok(QuoteCollateral { + cert_chain_pem, + crls, + root_ca_crl, + }) +} + +/// Build certificate chain by following AIA links (stops before root) +fn build_cert_chain(leaf_cert_der: &[u8]) -> Result>> { + let mut chain_ders = Vec::new(); + chain_ders.push(leaf_cert_der.to_vec()); + let mut current_cert_der = leaf_cert_der.to_vec(); + + loop { + let Some(url) = extract_aia_ca_issuers(¤t_cert_der)? else { + debug!("no AIA found - reached end of AIA chain"); + break; + }; + debug!("downloading parent cert from: {url}"); + let parent_der = download_cert(&url)?; + // Stop if we hit a self-signed cert (root CA) + if is_self_signed(&parent_der)? { + debug!("found self-signed cert - stopping (root CA should be provided by verifier)"); + break; + } + chain_ders.push(parent_der.clone()); + current_cert_der = parent_der; + } + + debug!("built chain with {} certificate(s)", chain_ders.len()); + Ok(chain_ders) +} + +/// Download CRLs for given certificates +fn download_crls_for_certs(certs: &[Vec]) -> Result>> { + debug!("downloading CRLs from device-provided cert chain..."); + + let mut crls = Vec::new(); + + for cert_der in certs { + let Some(crl) = download_crl_for_cert(cert_der).context("failed to download CRL")? else { + continue; + }; + crls.push(crl); + } + Ok(crls) +} + +/// Download CRL for verifier-provided root CA +fn download_crl_for_cert(cert: &[u8]) -> Result>> { + let crl_urls = extract_crl_urls(cert)?; + if crl_urls.is_empty() { + debug!("verifier root CA has no CRL DP - will skip root CA CRL check"); + return Ok(None); + } + + download_first_available_crl(&crl_urls).map(Some) +} + +/// Download first available CRL from a list of URLs +fn download_first_available_crl(urls: &[String]) -> Result> { + for url in urls { + debug!("downloading CRL from {url}"); + match download_crl(url) { + Ok(crl) => { + return Ok(crl); + } + Err(e) => { + warn!("✗ failed to download CRL from {url}: {e:?}"); + continue; + } + } + } + bail!("failed to download CRL") +} + +/// Convert DER certificates to PEM format +fn ders_to_pem(ders: &[Vec]) -> Result { + let mut pem = String::new(); + for der in ders.iter() { + pem.push_str(&der_to_pem(der, "CERTIFICATE")?); + } + Ok(pem) +} + +/// Check if certificate is self-signed +fn is_self_signed(cert_der: &[u8]) -> Result { + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + Ok(cert.subject() == cert.issuer()) +} + +fn extract_certs_webpki(cert_pem: &[u8]) -> Result>> { + use ::pem::parse_many; + + let pem_items = parse_many(cert_pem).context("failed to parse PEM")?; + + let certs = pem_items + .into_iter() + .map(|pem| rustls_pki_types::CertificateDer::from(pem.into_contents())) + .collect(); + + Ok(certs) +} + +fn download_crl(url: &str) -> Result> { + debug!("downloading CRL from {url}"); + + let response = + reqwest::blocking::get(url).context(format!("failed to download CRL from {url}"))?; + + if !response.status().is_success() { + bail!("CRL download failed with status: {}", response.status()); + } + + let crl_bytes = response + .bytes() + .context("failed to read CRL response body")? + .to_vec(); + + debug!("downloaded {} bytes CRL from {}", crl_bytes.len(), url); + + Ok(crl_bytes) +} + +fn extract_crl_urls(cert_der: &[u8]) -> Result> { + use x509_parser::extensions::ParsedExtension; + + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + + let mut crl_urls = Vec::new(); + + for ext in cert.extensions() { + let ParsedExtension::CRLDistributionPoints(crl_dist_points) = ext.parsed_extension() else { + continue; + }; + for dist_point in crl_dist_points.points.iter() { + let Some(dist_point_name) = &dist_point.distribution_point else { + continue; + }; + + let DistributionPointName::FullName(names) = dist_point_name else { + continue; + }; + for name in names.iter() { + let x509_parser::extensions::GeneralName::URI(uri) = name else { + continue; + }; + crl_urls.push(uri.to_string()); + debug!("found CRL URL: {uri}"); + } + } + } + + if crl_urls.is_empty() { + debug!("no CRL Distribution Points found in certificate"); + } + + Ok(crl_urls) +} + +fn extract_aia_ca_issuers(cert_der: &[u8]) -> Result> { + use x509_parser::extensions::ParsedExtension; + + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + + for ext in cert.extensions() { + let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() else { + continue; + }; + + for access_desc in &aia.accessdescs { + const OID_CA_ISSUERS: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 48, 2]; + let oid_bytes: Vec = match access_desc.access_method.iter() { + Some(iter) => iter.collect(), + None => continue, + }; + + if oid_bytes == OID_CA_ISSUERS { + if let x509_parser::extensions::GeneralName::URI(uri) = &access_desc.access_location + { + debug!("found AIA CA Issuers URL: {uri}"); + return Ok(Some(uri.to_string())); + } + } + } + } + + debug!("no AIA CA Issuers URL found in certificate"); + Ok(None) +} + +fn download_cert(url: &str) -> Result> { + debug!("downloading certificate from {url}"); + + let response = reqwest::blocking::get(url) + .context(format!("failed to download certificate from {url}"))?; + + if !response.status().is_success() { + bail!( + "certificate download failed with status: {}", + response.status() + ); + } + + let cert_bytes = response + .bytes() + .context("failed to read certificate response body")? + .to_vec(); + + debug!( + "downloaded {} bytes certificate from {}", + cert_bytes.len(), + url + ); + + Ok(cert_bytes) +} + +fn der_to_pem(der: &[u8], label: &str) -> Result { + use base64::Engine; + + let b64 = base64::engine::general_purpose::STANDARD.encode(der); + + let mut pem = format!("-----BEGIN {label}-----\n"); + for chunk in b64.as_bytes().chunks(64) { + pem.push_str(std::str::from_utf8(chunk)?); + pem.push('\n'); + } + pem.push_str(&format!("-----END {label}-----\n")); + + Ok(pem) +} diff --git a/tpm-qvl/src/lib.rs b/tpm-qvl/src/lib.rs new file mode 100644 index 000000000..7e8392897 --- /dev/null +++ b/tpm-qvl/src/lib.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Quote Verification Library (QVL) +//! +//! This module provides quote verification and collateral management for TPM attestation. +//! It follows the dcap-qvl architecture for Intel TDX verification. +//! +//! # Architecture +//! - **Step 1**: `get_collateral()` - Extract cert chain and download CRLs +//! - **Step 2**: `verify_quote()` - Verify quote with collateral +//! +//! This crate is designed to run on the verifier side, while tpm-attest runs on the device side. + +use anyhow::{bail, Result}; +use dstack_types::Platform; +use serde::{Deserialize, Serialize}; + +/// GCP TPM Root CA certificate (embedded, valid 2022-2122) +/// +/// Subject: CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US +/// Valid: 2022-07-08 to 2122-07-08 (100 years) +pub const GCP_ROOT_CA: &str = include_str!("../certs/gcp-root-ca.pem"); + +/// Get TPM root CA certificate for the given platform +pub fn get_root_ca(platform: Platform) -> Result<&'static str> { + match platform { + Platform::Gcp => Ok(GCP_ROOT_CA), + Platform::Dstack => bail!("dstack platform does not use TPM attestation"), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuoteCollateral { + /// Intermediate certificate chain (PEM format) from device + /// Does NOT include root CA (which must be provided independently by verifier) + pub cert_chain_pem: String, + /// All CRLs extracted from device-provided cert chain + pub crls: Vec>, + /// Root CA CRL extracted from verifier-provided root CA + pub root_ca_crl: Option>, +} + +#[derive(Debug)] +pub struct VerificationError { + pub status: VerificationStatus, + pub error: anyhow::Error, +} + +impl std::fmt::Display for VerificationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "verification failed: {}", self.error) + } +} + +impl std::error::Error for VerificationError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.error.source() + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct VerificationStatus { + pub ak_verified: bool, + pub signature_verified: bool, + pub pcr_verified: bool, +} + +#[cfg(feature = "crl-download")] +pub use collateral::{get_collateral, get_collateral_and_verify}; + +pub use verify::verify_quote; + +pub mod verify; + +#[cfg(feature = "crl-download")] +pub mod collateral; diff --git a/tpm-qvl/src/verify.rs b/tpm-qvl/src/verify.rs new file mode 100644 index 000000000..9cd591ebc --- /dev/null +++ b/tpm-qvl/src/verify.rs @@ -0,0 +1,670 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Quote Verification Module + +use ::pem::parse_many; +use anyhow::{anyhow, bail, Context, Result}; +use dstack_types::Platform; +use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; +use rsa::RsaPublicKey; +use sha2::{Digest, Sha256}; +use tracing::{debug, warn}; +use x509_parser::prelude::*; + +use rustls_pki_types::{CertificateDer, UnixTime}; +use webpki::{BorrowedCertRevocationList, CertRevocationList, EndEntityCert}; + +use tpm_types::{PcrValue, TpmEvent, TpmQuote}; + +use crate::{get_root_ca, QuoteCollateral, VerificationError, VerificationStatus}; + +#[derive(Clone)] +pub struct VerifiedReport { + pub attest: TpmAttest, + pub platform: Platform, + pub pcr_values: Vec, +} + +impl VerifiedReport { + pub fn get_pcr(&self, index: u32) -> Result> { + self.pcr_values + .iter() + .find(|p| p.index == index) + .map(|p| p.value.clone()) + .ok_or(anyhow!("PCR {} not found", index)) + } +} + +#[derive(Debug)] +enum PublicKey { + Rsa(RsaPublicKey), + Ecc(VerifyingKey), +} + +/// Verify quote with collateral and library-bundled root CA +pub fn verify_quote( + quote: &TpmQuote, + collateral: &QuoteCollateral, +) -> Result { + let ca = get_root_ca(quote.platform).map_err(|e| VerificationError { + status: VerificationStatus::default(), + error: e, + })?; + verify_quote_with_ca(quote, collateral, ca) +} + +/// Verify quote with collateral and user-provided root CA (recommended for security) +/// +/// The root CA is provided by the verifier as an independent trust anchor, +/// not derived from device-provided collateral. This prevents attacks where +/// a malicious device provides a fake certificate chain including a fake root CA. +pub fn verify_quote_with_ca( + quote: &TpmQuote, + collateral: &QuoteCollateral, + root_ca_pem: &str, +) -> Result { + let mut status = VerificationStatus::default(); + + let attest = match parse_tpm_attest("e.message) { + Ok(a) => a, + Err(e) => { + return Err(VerificationError { + status, + error: e.context("failed to parse TPMS_ATTEST"), + }); + } + }; + + let attested_pcr_indices: Vec = attest + .attested_quote_info + .pcr_selections + .iter() + .flat_map(|s| s.pcr_indices.iter().copied()) + .collect(); + let provided_pcr_indices: Vec = quote.pcr_values.iter().map(|p| p.index).collect(); + + if attested_pcr_indices != provided_pcr_indices { + return Err(VerificationError { + status, + error: anyhow!( + "PCR selection mismatch: TPMS_ATTEST has {:?}, but pcr_values has {:?}", + attested_pcr_indices, + provided_pcr_indices + ), + }); + } + + let computed_pcr_digest = + compute_pcr_digest("e.pcr_values).map_err(|e| VerificationError { + status: status.clone(), + error: e, + })?; + if attest.attested_quote_info.pcr_digest != computed_pcr_digest { + return Err(VerificationError { + status, + error: anyhow!("PCR digest mismatch"), + }); + } + + verify_event_log("e.pcr_values, "e.event_log).map_err(|e| VerificationError { + status: status.clone(), + error: e.context("event log verification failed"), + })?; + debug!("✓ Event Log replay verification successful"); + + status.pcr_verified = true; + + let ak_public_key = match extract_ak_public_key_from_cert("e.ak_cert) { + Ok(key) => { + debug!("extracted AK public key from certificate"); + key + } + Err(e) => { + return Err(VerificationError { + status, + error: e.context("failed to extract AK public key from certificate"), + }); + } + }; + + match verify_signature_with_key("e.message, "e.signature, &ak_public_key) { + Ok(true) => status.signature_verified = true, + Ok(false) => { + return Err(VerificationError { + status, + error: anyhow!("signature verification failed"), + }); + } + Err(e) => { + return Err(VerificationError { + status, + error: e.context("signature verification error"), + }); + } + } + + match verify_ak_chain_with_collateral("e.ak_cert, collateral, root_ca_pem) { + Ok(()) => {} + Err(e) => { + return Err(VerificationError { + status, + error: e.context("AK certificate chain verification error"), + }); + } + } + + Ok(VerifiedReport { + attest, + platform: quote.platform, + pcr_values: quote.pcr_values.clone(), + }) +} + +#[derive(Debug, Clone)] +pub struct TpmAttest { + pub magic: u32, + pub type_: u16, + pub qualified_signer: Vec, + pub qualified_data: Vec, + pub clock_info: ClockInfo, + pub firmware_version: u64, + pub attested_quote_info: QuoteInfo, +} + +#[derive(Debug, Clone)] +pub struct ClockInfo { + pub clock: u64, + pub reset_count: u32, + pub restart_count: u32, + pub safe: u8, +} + +/// PCR selection entry from TPM quote +#[derive(Debug, Clone)] +pub struct PcrSelection { + /// Hash algorithm (e.g., 0x000B for SHA-256) + pub hash_alg: u16, + /// Selected PCR indices + pub pcr_indices: Vec, +} + +#[derive(Debug, Clone)] +pub struct QuoteInfo { + /// PCR selections from the quote + pub pcr_selections: Vec, + /// PCR digest + pub pcr_digest: Vec, +} + +fn parse_tpm_attest(data: &[u8]) -> Result { + use nom::bytes::complete::take; + use nom::number::complete::{be_u16, be_u32, be_u64, be_u8}; + use nom::IResult; + + fn parse_sized_buffer(input: &[u8]) -> IResult<&[u8], Vec> { + let (input, size) = be_u16(input)?; + let (input, data) = take(size)(input)?; + Ok((input, data.to_vec())) + } + + fn parse_attest(input: &[u8]) -> IResult<&[u8], TpmAttest> { + let (input, magic) = be_u32(input)?; + let (input, type_) = be_u16(input)?; + let (input, qualified_signer) = parse_sized_buffer(input)?; + let (input, qualified_data) = parse_sized_buffer(input)?; + + let (input, clock) = be_u64(input)?; + let (input, reset_count) = be_u32(input)?; + let (input, restart_count) = be_u32(input)?; + let (input, safe) = be_u8(input)?; + + let (input, firmware_version) = be_u64(input)?; + + let (input, pcr_select_count) = be_u32(input)?; + + let mut pcr_selections = Vec::new(); + let mut current_input = input; + for _ in 0..pcr_select_count { + let (input, hash_alg) = be_u16(current_input)?; + let (input, sizeof_select) = be_u8(input)?; + let (input, pcr_bitmap) = take(sizeof_select)(input)?; + + // Parse PCR bitmap into indices + let mut pcr_indices = Vec::new(); + for (byte_idx, &byte) in pcr_bitmap.iter().enumerate() { + for bit_idx in 0..8 { + if (byte & (1 << bit_idx)) != 0 { + pcr_indices.push((byte_idx * 8 + bit_idx) as u32); + } + } + } + + pcr_selections.push(PcrSelection { + hash_alg, + pcr_indices, + }); + + current_input = input; + } + + let input = current_input; + let (input, pcr_digest) = parse_sized_buffer(input)?; + + Ok(( + input, + TpmAttest { + magic, + type_, + qualified_signer, + qualified_data, + clock_info: ClockInfo { + clock, + reset_count, + restart_count, + safe, + }, + firmware_version, + attested_quote_info: QuoteInfo { + pcr_selections, + pcr_digest, + }, + }, + )) + } + + let (_, attest) = parse_attest(data).map_err(|e| anyhow!("parse error: {e}"))?; + + if attest.magic != 0xff544347 { + bail!("invalid magic number: 0x{magic:08x}", magic = attest.magic); + } + + if attest.type_ != 0x8018 { + bail!("invalid attest type: 0x{type_:04x}", type_ = attest.type_); + } + + Ok(attest) +} + +fn compute_pcr_digest(pcr_values: &[PcrValue]) -> Result> { + let mut hasher = Sha256::new(); + for pcr in pcr_values { + hasher.update(&pcr.value); + } + Ok(hasher.finalize().to_vec()) +} + +fn verify_event_log(pcr_values: &[PcrValue], event_log: &[TpmEvent]) -> Result<()> { + for pcr in pcr_values { + let pcr_events: Vec<&TpmEvent> = event_log + .iter() + .filter(|e| e.pcr_index == pcr.index) + .collect(); + + if pcr_events.is_empty() { + continue; + } + + // Replay PCR extension to verify Event Log matches quote + let mut replayed_pcr = vec![0u8; 32]; + for event in &pcr_events { + let mut hasher = Sha256::new(); + hasher.update(&replayed_pcr); + hasher.update(&event.digest); + replayed_pcr = hasher.finalize().to_vec(); + } + + if replayed_pcr != pcr.value { + bail!( + "PCR {} replay mismatch: expected {}, got {}", + pcr.index, + hex::encode(&pcr.value), + hex::encode(&replayed_pcr) + ); + } + + debug!( + "✓ PCR {} replay verification successful ({} events)", + pcr.index, + pcr_events.len() + ); + + // For PCR 2: Extract Event 28 (UKI measurement) for image verification + // NOTE: Extracting the 3rd event (index 2) is GCP OVMF-specific behavior. + // On GCP, PCR 2 events are: [0]=EV_SEPARATOR, [1]=EV_EFI_GPT_EVENT, + // [2]=UKI (Event 28), [3]=Linux kernel (Event 41) + // Other platforms may have different event ordering. + if pcr.index == 2 && pcr_events.len() >= 3 { + let uki_digest = hex::encode(&pcr_events[2].digest); + debug!("Event 28 (UKI hash): {}", uki_digest); + debug!("To verify image: compare this against expected UKI Authenticode hash"); + } + } + + Ok(()) +} + +fn extract_ak_public_key_from_cert(ak_cert_der: &[u8]) -> Result { + let (_, cert) = + X509Certificate::from_der(ak_cert_der).context("failed to parse AK certificate")?; + + let spki = cert.public_key(); + + let algo_oid = &spki.algorithm.algorithm; + + const OID_RSA_ENCRYPTION: &[u64] = &[1, 2, 840, 113549, 1, 1, 1]; + const OID_EC_PUBLIC_KEY: &[u64] = &[1, 2, 840, 10045, 2, 1]; + + let oid_bytes: Vec = algo_oid + .iter() + .ok_or_else(|| anyhow::anyhow!("invalid OID"))? + .collect(); + + if oid_bytes == OID_RSA_ENCRYPTION { + use rsa::pkcs1::DecodeRsaPublicKey; + use rsa::traits::PublicKeyParts; + + let public_key = RsaPublicKey::from_pkcs1_der(spki.subject_public_key.data.as_ref()) + .context("failed to decode RSA public key from certificate")?; + + debug!( + "extracted RSA AK public key from certificate ({} bits)", + public_key.size() * 8 + ); + + Ok(PublicKey::Rsa(public_key)) + } else if oid_bytes == OID_EC_PUBLIC_KEY { + let public_key_bytes = spki.subject_public_key.data.as_ref(); + + let verifying_key = VerifyingKey::from_sec1_bytes(public_key_bytes) + .context("failed to decode ECC public key from certificate")?; + + debug!("extracted ECC P-256 AK public key from certificate"); + + Ok(PublicKey::Ecc(verifying_key)) + } else { + bail!("unsupported public key algorithm: {:?}", oid_bytes); + } +} + +fn verify_signature_with_key( + message: &[u8], + signature: &[u8], + public_key: &PublicKey, +) -> Result { + if signature.len() < 4 { + bail!("signature too short: {} bytes", signature.len()); + } + + let sig_alg = u16::from_be_bytes([signature[0], signature[1]]); + let hash_alg = u16::from_be_bytes([signature[2], signature[3]]); + + if hash_alg != 0x000B { + bail!("unsupported hash algorithm: 0x{hash_alg:04x}"); + } + + let actual_signature = &signature[4..]; + + debug!( + "message ({} bytes): {}", + message.len(), + hex::encode(message) + ); + debug!( + "signature ({} bytes): {}", + actual_signature.len(), + hex::encode(actual_signature) + ); + + let mut hasher = Sha256::new(); + hasher.update(message); + let message_hash = hasher.finalize(); + + debug!("message hash: {}", hex::encode(message_hash)); + + match public_key { + PublicKey::Rsa(rsa_key) => { + if sig_alg != 0x0014 { + bail!("expected RSASSA (0x0014), got 0x{sig_alg:04x}"); + } + + if actual_signature.len() < 2 { + bail!("RSA signature too short for size field"); + } + let rsa_sig_size = + u16::from_be_bytes([actual_signature[0], actual_signature[1]]) as usize; + if actual_signature.len() < 2 + rsa_sig_size { + bail!("RSA signature too short for signature data"); + } + let rsa_sig_data = &actual_signature[2..2 + rsa_sig_size]; + + debug!("RSA signature parsed: {rsa_sig_size} bytes"); + + let padding = rsa::Pkcs1v15Sign::new::(); + match rsa_key.verify(padding, &message_hash, rsa_sig_data) { + Ok(_) => { + debug!("✓ RSA signature verification successful"); + Ok(true) + } + Err(e) => { + warn!("RSA signature verification failed: {e}"); + Ok(false) + } + } + } + PublicKey::Ecc(ecc_key) => { + if sig_alg != 0x0018 { + bail!("expected ECDSA (0x0018), got 0x{sig_alg:04x}"); + } + + if actual_signature.len() < 2 { + bail!("ECDSA signature too short for signatureR size"); + } + let r_size = u16::from_be_bytes([actual_signature[0], actual_signature[1]]) as usize; + if actual_signature.len() < 2 + r_size { + bail!("ECDSA signature too short for signatureR data"); + } + let r_data = &actual_signature[2..2 + r_size]; + + let s_offset = 2 + r_size; + if actual_signature.len() < s_offset + 2 { + bail!("ECDSA signature too short for signatureS size"); + } + let s_size = + u16::from_be_bytes([actual_signature[s_offset], actual_signature[s_offset + 1]]) + as usize; + if actual_signature.len() < s_offset + 2 + s_size { + bail!("ECDSA signature too short for signatureS data"); + } + let s_data = &actual_signature[s_offset + 2..s_offset + 2 + s_size]; + + let mut sig_bytes = Vec::with_capacity(r_size + s_size); + sig_bytes.extend_from_slice(r_data); + sig_bytes.extend_from_slice(s_data); + + debug!("ECDSA signature parsed: r={r_size} bytes, s={s_size} bytes",); + + let signature = + Signature::from_slice(&sig_bytes).context("failed to parse ECDSA signature")?; + + match ecc_key.verify_prehash(&message_hash, &signature) { + Ok(_) => { + debug!("✓ ECC signature verification successful"); + Ok(true) + } + Err(e) => { + warn!("ECC signature verification failed: {e}"); + Ok(false) + } + } + } + } +} + +fn extract_certs_webpki(cert_pem: &[u8]) -> Result>> { + let pem_items = parse_many(cert_pem).context("failed to parse PEM")?; + + let certs = pem_items + .into_iter() + .map(|pem| CertificateDer::from(pem.into_contents())) + .collect(); + + Ok(certs) +} + +fn verify_ak_chain_with_collateral( + ak_cert_der: &[u8], + collateral: &QuoteCollateral, + root_ca_pem: &str, +) -> Result<()> { + debug!( + "verifying AK certificate chain with webpki ({} bytes leaf, {} intermediate CRLs, root CRL: {})", + ak_cert_der.len(), + collateral.crls.len(), + if collateral.root_ca_crl.is_some() { "yes" } else { "no" } + ); + + let ak_cert_der_owned = CertificateDer::from(ak_cert_der.to_vec()); + let ak_cert = + EndEntityCert::try_from(&ak_cert_der_owned).context("failed to parse AK certificate")?; + + // Load intermediate certs from device-provided collateral + let intermediate_certs = extract_certs_webpki(collateral.cert_chain_pem.as_bytes())?; + + debug!( + "loaded {} intermediate certificate(s) from collateral", + intermediate_certs.len() + ); + for (i, cert_der) in intermediate_certs.iter().enumerate() { + if let Ok((_, cert)) = X509Certificate::from_der(cert_der.as_ref()) { + debug!( + " intermediate[{i}]: subject={}, issuer={}", + cert.subject(), + cert.issuer() + ); + } + } + + // Load root CA from verifier-provided trust anchor (CRITICAL: independent from device) + let root_ca_certs = extract_certs_webpki(root_ca_pem.as_bytes())?; + if root_ca_certs.is_empty() { + bail!("failed to parse root CA PEM - no certificates found"); + } + let root_cert_der = &root_ca_certs[0]; + + if let Ok((_, cert)) = X509Certificate::from_der(root_cert_der.as_ref()) { + debug!( + "trust anchor (verifier-provided): subject={}, issuer={}", + cert.subject(), + cert.issuer() + ); + } + + let trust_anchor = webpki::anchor_from_trusted_cert(root_cert_der) + .context("failed to create trust anchor from verifier root CA")?; + + debug!( + "trust anchor created, {} intermediate(s)", + intermediate_certs.len() + ); + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .context("failed to get current time")?; + let time = UnixTime::since_unix_epoch(now); + + let trust_anchors = [trust_anchor]; + + // Check root CA against CRL if CRL was provided + if let Some(root_ca_crl) = &collateral.root_ca_crl { + debug!("checking root CA against its CRL (dcap-qvl-webpki)"); + let crl_refs = vec![root_ca_crl.as_slice()]; + dcap_qvl_webpki::check_single_cert_crl(root_cert_der.as_ref(), &crl_refs, time) + .context("root CA revoked or invalid CRL")?; + debug!("✓ root CA CRL check passed"); + } else { + debug!("root CA has no CRL - skipping root CA CRL check"); + } + + let result = if !collateral.crls.is_empty() { + debug!( + "parsing {} intermediate CRL(s) for revocation checking", + collateral.crls.len() + ); + let crls: Vec = collateral + .crls + .iter() + .enumerate() + .map(|(i, der)| { + BorrowedCertRevocationList::from_der(der) + .map(|crl| crl.into()) + .with_context(|| format!("failed to parse intermediate CRL #{i}")) + }) + .collect::>>()?; + let crl_refs: Vec<&CertRevocationList> = crls.iter().collect(); + + debug!("creating revocation options (CRL enforcement)"); + let revocation_builder = webpki::RevocationOptionsBuilder::new(&crl_refs) + .map_err(|_| anyhow::anyhow!("failed to create RevocationOptionsBuilder"))?; + + let revocation = revocation_builder + .with_depth(webpki::RevocationCheckDepth::Chain) + .with_status_policy(webpki::UnknownStatusPolicy::Allow) + .with_expiration_policy(webpki::ExpirationPolicy::Enforce) + .build(); + + debug!("verifying certificate chain with CRL revocation checking"); + + const TCG_KP_AIK_CERTIFICATE: &[u8] = &[0x67, 0x81, 0x05, 0x08, 0x01]; + let key_usage = webpki::KeyUsage::required_if_present(TCG_KP_AIK_CERTIFICATE); + + ak_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &trust_anchors, + &intermediate_certs, + time, + key_usage, + Some(revocation), + None, + ) + .context("certificate chain verification failed") + } else { + debug!("no CRLs available (no certificates have CRL Distribution Points)"); + debug!("verifying certificate chain WITHOUT CRL checking"); + + const TCG_KP_AIK_CERTIFICATE: &[u8] = &[0x67, 0x81, 0x05, 0x08, 0x01]; + let key_usage = webpki::KeyUsage::required_if_present(TCG_KP_AIK_CERTIFICATE); + + ak_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &trust_anchors, + &intermediate_certs, + time, + key_usage, + None, + None, + ) + .context("certificate chain verification failed") + }; + + match result { + Ok(_) => { + if collateral.crls.is_empty() { + debug!("✓ AK certificate chain verification successful (webpki, no CRLs)"); + } else { + debug!( + "✓ AK certificate chain verification successful (webpki + {} intermediate CRL(s))", + collateral.crls.len() + ); + } + Ok(()) + } + Err(e) => { + warn!("✗ AK certificate chain verification failed: {e:?}"); + Err(e) + } + } +} diff --git a/tpm-types/Cargo.toml b/tpm-types/Cargo.toml new file mode 100644 index 000000000..cb81483ca --- /dev/null +++ b/tpm-types/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm-types" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde-human-bytes.workspace = true +dstack-types.workspace = true +scale = { workspace = true, features = ["derive"] } +cc-eventlog.workspace = true diff --git a/tpm-types/src/lib.rs b/tpm-types/src/lib.rs new file mode 100644 index 000000000..18dd23f28 --- /dev/null +++ b/tpm-types/src/lib.rs @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Types - Common TPM-related type definitions +//! +//! This crate contains type definitions shared across TPM-related crates: +//! - tpm-attest (device side - generates quotes) +//! - tpm-qvl (verifier side - verifies quotes) +//! - ra-tls (uses TPM quotes in attestation) + +use dstack_types::Platform; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use serde_human_bytes as hex_bytes; + +/// TPM Quote structure containing attestation data +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct TpmQuote { + /// TPMS_ATTEST message + #[serde(with = "hex_bytes")] + pub message: Vec, + + /// Quote signature + #[serde(with = "hex_bytes")] + pub signature: Vec, + + /// PCR values included in the quote + pub pcr_values: Vec, + + /// Attestation Key (AK) certificate (DER format) + #[serde(with = "hex_bytes")] + pub ak_cert: Vec, + + /// Platform where quote was generated + pub platform: Platform, + + /// Event Log (optional, used for PCR replay verification) + pub event_log: Vec, +} + +impl TpmQuote { + pub fn from_scale(mut input: &[u8]) -> Result { + Self::decode(&mut input) + } + + pub fn to_scale(&self) -> Vec { + self.encode() + } +} + +/// PCR (Platform Configuration Register) value +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct PcrValue { + /// PCR index (0-23) + pub index: u32, + + /// Hash algorithm (e.g., "sha256", "sha384") + pub algorithm: String, + + /// PCR value (hash) + #[serde(with = "hex_bytes")] + pub value: Vec, +} + +/// PCR selection specifying which PCRs to include +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PcrSelection { + /// Hash bank (e.g., "sha256") + pub bank: String, + + /// List of PCR indices + pub pcrs: Vec, +} + +impl PcrSelection { + pub fn new(bank: &str, pcrs: &[u32]) -> Self { + Self { + bank: bank.to_string(), + pcrs: pcrs.to_vec(), + } + } + + pub fn sha256(pcrs: &[u32]) -> Self { + Self::new("sha256", pcrs) + } + + pub fn to_arg(&self) -> String { + let pcr_list: Vec = self.pcrs.iter().map(|p| p.to_string()).collect(); + format!( + "{}:{pcr_list_joined}", + self.bank, + pcr_list_joined = pcr_list.join(",") + ) + } +} + +impl Default for PcrSelection { + fn default() -> Self { + Self::sha256(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + } +} + +// Re-export TPM Event types from cc-eventlog +pub use cc_eventlog::tpm::{TpmEvent, TpmEventLog}; From 9ef4609f67c5a3a43d849de6a4a09b7436b1566b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 17:45:43 +0800 Subject: [PATCH 010/264] vmm: Support for config product info --- guest-agent/src/http_routes.rs | 4 +++ guest-agent/src/models.rs | 2 ++ guest-agent/src/rpc_service.rs | 8 +++++ guest-agent/templates/dashboard.html | 6 ++++ vmm/src/app/qemu.rs | 45 ++++++++++++++++++++++++++++ vmm/src/config.rs | 37 +++++++++++++++++++++++ vmm/vmm.toml | 31 +++++++++++++++++++ 7 files changed, 133 insertions(+) diff --git a/guest-agent/src/http_routes.rs b/guest-agent/src/http_routes.rs index 4e0c2facf..c8fa44df2 100644 --- a/guest-agent/src/http_routes.rs +++ b/guest-agent/src/http_routes.rs @@ -48,6 +48,8 @@ async fn index(state: &State) -> Result, String> { tcb_info, app_cert: _, vm_config: _, + cloud_vendor, + cloud_product, } = handler .info() .await @@ -70,6 +72,8 @@ async fn index(state: &State) -> Result, String> { public_sysinfo, public_logs, public_tcbinfo, + cloud_vendor, + cloud_product, }; match model.render() { Ok(html) => Ok(RawHtml(html)), diff --git a/guest-agent/src/models.rs b/guest-agent/src/models.rs index 12a0f887e..a50cf340d 100644 --- a/guest-agent/src/models.rs +++ b/guest-agent/src/models.rs @@ -48,6 +48,8 @@ pub struct Dashboard { pub public_sysinfo: bool, pub public_logs: bool, pub public_tcbinfo: bool, + pub cloud_vendor: String, + pub cloud_product: String, } #[derive(Template)] diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 8be3f7569..7480e5c76 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -39,6 +39,12 @@ use tracing::error; use crate::config::Config; +fn read_dmi_file(name: &str) -> String { + fs::read_to_string(format!("/sys/class/dmi/id/{name}")) + .map(|s| s.trim().to_string()) + .unwrap_or_default() +} + #[derive(Clone)] pub struct AppState { inner: Arc, @@ -193,6 +199,8 @@ pub async fn get_info(state: &AppState, external: bool) -> Result { .clone(), tcb_info, vm_config, + cloud_vendor: read_dmi_file("sys_vendor"), + cloud_product: read_dmi_file("product_name"), }) } diff --git a/guest-agent/templates/dashboard.html b/guest-agent/templates/dashboard.html index a81e3a45f..212df06ac 100644 --- a/guest-agent/templates/dashboard.html +++ b/guest-agent/templates/dashboard.html @@ -168,6 +168,12 @@

Node Information

Key Provider Info
{{key_provider_info}}
+ {% if !cloud_vendor.is_empty() || !cloud_product.is_empty() %} +
+
Machine Provider
+
{{cloud_vendor}} ({{cloud_product}})
+
+ {% endif %} {% if public_sysinfo %}
Operating System
diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index 167fb2b09..c6dcb6b3c 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -531,6 +531,7 @@ impl VmConfig { command.arg("-device").arg("virtio-net-pci,netdev=net0"); self.configure_machine(&mut command, &workdir, cfg)?; + self.configure_smbios(&mut command, cfg); command .arg("-device") @@ -811,6 +812,50 @@ impl VmConfig { command.arg("-object").arg(tdx_object); Ok(()) } + + fn configure_smbios(&self, command: &mut Command, cfg: &CvmConfig) { + let p = &cfg.product; + + fn cfg_if(ty: &mut Vec, name: &str, v: &Option) { + if let Some(v) = v { + ty.push(format!("{name}={v}")); + } + } + + let mut types = [const { Vec::new() }; 4]; + // SMBIOS type=0 (BIOS Information) + cfg_if(&mut types[0], "vendor", &p.bios_vendor); + cfg_if(&mut types[0], "version", &p.bios_version); + cfg_if(&mut types[0], "date", &p.bios_date); + cfg_if(&mut types[0], "release", &p.bios_release); + // SMBIOS type=1 (System Information) + cfg_if(&mut types[1], "manufacturer", &p.sys_vendor); + cfg_if(&mut types[1], "product", &p.product_name); + cfg_if(&mut types[1], "version", &p.product_version); + cfg_if(&mut types[1], "serial", &p.product_serial); + cfg_if(&mut types[1], "uuid", &p.product_uuid); + cfg_if(&mut types[1], "family", &p.product_family); + cfg_if(&mut types[1], "sku", &p.product_sku); + // SMBIOS type=2 (Baseboard Information) + cfg_if(&mut types[2], "manufacturer", &p.board_vendor); + cfg_if(&mut types[2], "product", &p.board_name); + cfg_if(&mut types[2], "version", &p.board_version); + cfg_if(&mut types[2], "serial", &p.board_serial); + cfg_if(&mut types[2], "asset", &p.board_asset_tag); + // SMBIOS type=3 (Chassis Information) + cfg_if(&mut types[3], "manufacturer", &p.chassis_vendor); + cfg_if(&mut types[3], "version", &p.chassis_version); + cfg_if(&mut types[3], "serial", &p.chassis_serial); + cfg_if(&mut types[3], "asset", &p.chassis_asset_tag); + + for (i, t) in types.iter().enumerate() { + if !t.is_empty() { + command + .arg("-smbios") + .arg(format!("type={i},{}", t.join(","))); + } + } + } } /// Round up a value to the nearest multiple of another value. diff --git a/vmm/src/config.rs b/vmm/src/config.rs index bbf6612d4..8a593fc00 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -199,6 +199,43 @@ pub struct CvmConfig { /// The guest kernel will use this vsock port to communicate with the host QGS. /// Default is None (disabled), common value is 4050. pub qgs_port: Option, + + /// SMBIOS product information for cloud environment detection + #[serde(default)] + pub product: ProductConfig, +} + +/// SMBIOS product information configuration. +/// Field names correspond to /sys/class/dmi/id/ entries in guest. +#[derive(Debug, Clone, Default, Deserialize)] +pub struct ProductConfig { + // SMBIOS type=0 (BIOS Information) + pub bios_vendor: Option, + pub bios_version: Option, + pub bios_date: Option, + pub bios_release: Option, + + // SMBIOS type=1 (System Information) + pub sys_vendor: Option, + pub product_name: Option, + pub product_version: Option, + pub product_serial: Option, + pub product_uuid: Option, + pub product_family: Option, + pub product_sku: Option, + + // SMBIOS type=2 (Baseboard Information) + pub board_vendor: Option, + pub board_name: Option, + pub board_version: Option, + pub board_serial: Option, + pub board_asset_tag: Option, + + // SMBIOS type=3 (Chassis Information) + pub chassis_vendor: Option, + pub chassis_version: Option, + pub chassis_serial: Option, + pub chassis_asset_tag: Option, } #[derive(Debug, Clone, Deserialize)] diff --git a/vmm/vmm.toml b/vmm/vmm.toml index a861a1909..1f14e13fd 100644 --- a/vmm/vmm.toml +++ b/vmm/vmm.toml @@ -46,6 +46,37 @@ host_share_mode = "vvfat" # Default is None (disabled), common value is 4050. qgs_port = 4050 +# SMBIOS product information for cloud environment detection. +# Field names correspond to /sys/class/dmi/id/ entries in guest. +[cvm.product] +## type=0 (BIOS Information) +# bios_vendor = "" +# bios_version = "" +# bios_date = "" +# bios_release = "" + +## type=1 (System Information) +sys_vendor = "dstack" +product_name = "dstack" +# product_version = "" +# product_serial = "" +# product_uuid = "" +# product_family = "" +# product_sku = "" + +## type=2 (Baseboard Information) +# board_vendor = "dstack" +# board_name = "dstack" +# board_version = "" +# board_serial = "" +# board_asset_tag = "" + +# type=3 (Chassis Information) +# chassis_vendor = "" +# chassis_version = "" +# chassis_serial = "" +# chassis_asset_tag = "" + [cvm.networking] mode = "user" From e9d2a734fed8bf5f0eefb15b48cae8d43b4433a3 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 17:56:36 +0800 Subject: [PATCH 011/264] Add key-provider kind tpm --- docs/security-guide/cvm-boundaries.md | 2 + dstack-types/src/lib.rs | 12 ++ dstack-types/src/mr_config.rs | 1 + .../src/system_setup/config_id_verifier.rs | 2 +- vmm/src/console_v1.html | 119 ++++++++++-------- vmm/ui/src/components/CreateVmDialog.ts | 22 ++-- 6 files changed, 100 insertions(+), 58 deletions(-) diff --git a/docs/security-guide/cvm-boundaries.md b/docs/security-guide/cvm-boundaries.md index b15c844c9..eb0d71b1a 100644 --- a/docs/security-guide/cvm-boundaries.md +++ b/docs/security-guide/cvm-boundaries.md @@ -33,6 +33,7 @@ This is the main configuration file for the application in JSON format: | kms_enabled | 0.3.1 | boolean | Enable/disable KMS | | gateway_enabled | 0.3.1 | boolean | Enable/disable gateway | | local_key_provider_enabled | 0.3.1 | boolean | Use a local key provider | +| key_provider_id | 0.5.1 | string | Key provider ID. | | public_logs | 0.3.3 | boolean | Whether logs are publicly visible | | public_sysinfo | 0.3.3 | boolean | Whether system info is public | | public_tcbinfo | 0.5.1 | boolean | Whether TCB info is public | @@ -43,6 +44,7 @@ This is the main configuration file for the application in JSON format: | init_script | 0.5.5 | string | Bash script that executed prior to dockerd startup | | storage_fs | 0.5.5 | string | Filesystem type for the data disk of the CVM. Supported values: "zfs", "ext4". default to "zfs". **ZFS:** Ensures filesystem integrity with built-in data protection features. **ext4:** Provides better performance for database applications with lower overhead and faster I/O operations, but no strong integrity protection. | | swap_size | 0.5.5 | string/integer | The linux swap size. default to 0. Can be in byte or human-readable format (e.g., "1G", "256M"). | +| key_provider | 0.5.6 | string | Key provider type. Supported values: "none", "kms", "local", "tpm". | The hash of this file content is extended to RTMR3 as event name `compose-hash`. Remote verifier can extract the compose-hash during remote attestation. diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 5c7a8dc7b..4d3d7271c 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -69,6 +69,7 @@ pub enum KeyProviderKind { None, Kms, Local, + Tpm, } impl KeyProviderKind { @@ -79,6 +80,10 @@ impl KeyProviderKind { pub fn is_kms(&self) -> bool { matches!(self, KeyProviderKind::Kms) } + + pub fn is_tpm(&self) -> bool { + matches!(self, KeyProviderKind::Tpm) + } } #[derive(Deserialize, Serialize, Debug, Default, Clone)] @@ -190,6 +195,11 @@ pub enum KeyProvider { #[serde(with = "hex_bytes")] mr: Vec, }, + Tpm { + key: String, + #[serde(with = "hex_bytes")] + pubkey: Vec, + }, Kms { url: String, #[serde(with = "hex_bytes")] @@ -204,6 +214,7 @@ impl KeyProvider { match self { KeyProvider::None { .. } => KeyProviderKind::None, KeyProvider::Local { .. } => KeyProviderKind::Local, + KeyProvider::Tpm { .. } => KeyProviderKind::Tpm, KeyProvider::Kms { .. } => KeyProviderKind::Kms, } } @@ -212,6 +223,7 @@ impl KeyProvider { match self { KeyProvider::None { .. } => &[], KeyProvider::Local { mr, .. } => mr, + KeyProvider::Tpm { pubkey, .. } => pubkey, KeyProvider::Kms { pubkey, .. } => pubkey, } } diff --git a/dstack-types/src/mr_config.rs b/dstack-types/src/mr_config.rs index 6546a5526..b4766ecbe 100644 --- a/dstack-types/src/mr_config.rs +++ b/dstack-types/src/mr_config.rs @@ -37,6 +37,7 @@ impl MrConfig<'_> { KeyProviderKind::None => 0_u8, KeyProviderKind::Local => 1, KeyProviderKind::Kms => 2, + KeyProviderKind::Tpm => 3, }; let mut hasher = Keccak256::new(); hasher.update(compose_hash); diff --git a/dstack-util/src/system_setup/config_id_verifier.rs b/dstack-util/src/system_setup/config_id_verifier.rs index ab8eb48dc..c62f665c3 100644 --- a/dstack-util/src/system_setup/config_id_verifier.rs +++ b/dstack-util/src/system_setup/config_id_verifier.rs @@ -27,7 +27,7 @@ fn read_mr_config_id() -> Result<[u8; 48]> { /// Where the instance info is a concatenated bytes of the following fields: /// - compose_hash: [u8; 32] /// - app_id: [u8; 20] -/// - key_provider_type: u8 // 0: none, 1: local, 2: kms +/// - key_provider_type: u8 // 0: none, 1: local, 2: kms, 3: tpm /// - key_provider_id: [u8] // the ca pubkey for KMS or the MR enclave for local-sgx provider, empty for none pub fn verify_mr_config_id( compose_hash: &[u8; 32], diff --git a/vmm/src/console_v1.html b/vmm/src/console_v1.html index ab0b54c83..63163772a 100644 --- a/vmm/src/console_v1.html +++ b/vmm/src/console_v1.html @@ -2008,11 +2008,24 @@

Deploy a new instance

/>
+
+ + +
+ +
+ + +
+
- - @@ -2023,11 +2036,6 @@

Deploy a new instance

-
- - -
-
@@ -2284,8 +2292,7 @@

Derive VM

encryptedEnvs: [], storage_fs: '', app_id: null, - kms_enabled: true, - local_key_provider_enabled: false, + key_provider: 'kms', key_provider_id: '', gateway_enabled: true, public_logs: true, @@ -2473,10 +2480,19 @@

Derive VM

errorMessage.value = String(err); } } - function configGpu(form) { + function configGpu(form, isUpdate = false) { if (form.attachAllGpus) { return { attach_mode: 'all' }; } + // For updates, always return a config when GPUs are being explicitly updated + // Empty array means no GPUs should be attached + if (isUpdate) { + return { + attach_mode: 'listed', + gpus: (form.selectedGpus || []).map((slot) => ({ slot })), + }; + } + // For creation, return undefined if no GPUs are selected if (form.selectedGpus && form.selectedGpus.length > 0) { return { attach_mode: 'listed', @@ -2715,9 +2731,8 @@

Derive VM

} return 'running'; }; - const kmsEnabled = (vm) => { var _a, _b, _c; return ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.kms_enabled) || ((_c = (_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.features) === null || _c === void 0 ? void 0 : _c.includes('kms')); }; - const gatewayEnabled = (vm) => { var _a, _b, _c, _d; return ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.gateway_enabled) || ((_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.tproxy_enabled) || ((_d = (_c = vm.appCompose) === null || _c === void 0 ? void 0 : _c.features) === null || _d === void 0 ? void 0 : _d.includes('tproxy-net')); }; - const defaultTrue = (v) => (v === undefined ? true : v); + const kmsEnabled = (vm) => getKeyProvider(vm) === 'kms'; + const gatewayEnabled = (vm) => { var _a; return (_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.gateway_enabled; }; function formatMemory(memoryMB) { if (!memoryMB) { return '0 MB'; @@ -2742,17 +2757,24 @@

Derive VM

name: vmForm.value.name, runner: 'docker-compose', docker_compose_file: vmForm.value.dockerComposeFile, - kms_enabled: vmForm.value.kms_enabled, gateway_enabled: vmForm.value.gateway_enabled, public_logs: vmForm.value.public_logs, public_sysinfo: vmForm.value.public_sysinfo, public_tcbinfo: vmForm.value.public_tcbinfo, - local_key_provider_enabled: vmForm.value.local_key_provider_enabled, key_provider_id: vmForm.value.key_provider_id, allowed_envs: vmForm.value.encryptedEnvs.map((env) => env.key), no_instance_id: !vmForm.value.gateway_enabled, secure_time: false, }; + if (vmForm.value.key_provider !== undefined) { + appCompose.key_provider = vmForm.value.key_provider; + if (vmForm.value.key_provider === 'kms') { + appCompose.kms_enabled = true; + } + if (vmForm.value.key_provider === 'local') { + appCompose.local_key_provider_enabled = true; + } + } if (vmForm.value.storage_fs) { appCompose.storage_fs = vmForm.value.storage_fs; } @@ -2768,16 +2790,6 @@

Derive VM

appCompose.launch_token_hash = await calcComposeHash(launchToken.value); } const imgFeatures = imageVersionFeatures(imageVersion(vmForm.value.image)); - if (imgFeatures.compose_version < 2) { - const features = []; - if (vmForm.value.kms_enabled) - features.push('kms'); - if (vmForm.value.gateway_enabled) - features.push('tproxy-net'); - appCompose.features = features; - appCompose.manifest_version = 1; - appCompose.version = '1.0.0'; - } if (imgFeatures.compose_version < 3) { appCompose.tproxy_enabled = appCompose.gateway_enabled; delete appCompose.gateway_enabled; @@ -2813,12 +2825,11 @@

Derive VM

() => vmForm.value.name, () => vmForm.value.dockerComposeFile, () => vmForm.value.preLaunchScript, - () => vmForm.value.kms_enabled, () => vmForm.value.gateway_enabled, () => vmForm.value.public_logs, () => vmForm.value.public_sysinfo, () => vmForm.value.public_tcbinfo, - () => vmForm.value.local_key_provider_enabled, + () => vmForm.value.key_provider, () => vmForm.value.key_provider_id, () => vmForm.value.encryptedEnvs, () => vmForm.value.storage_fs, @@ -2948,7 +2959,7 @@

Derive VM

try { vmForm.value.memory = convertMemoryToMB(vmForm.value.memoryValue, vmForm.value.memoryUnit); const composeFile = await makeAppComposeFile(); - const encryptedEnv = await encryptEnv(vmForm.value.encryptedEnvs, vmForm.value.kms_enabled, vmForm.value.app_id); + const encryptedEnv = await encryptEnv(vmForm.value.encryptedEnvs, vmForm.value.key_provider === 'kms', vmForm.value.app_id); const payload = buildCreateVmPayload({ name: vmForm.value.name, image: vmForm.value.image, @@ -3040,7 +3051,7 @@

Derive VM

body.user_config = updated.user_config; body.update_ports = true; body.ports = normalizePorts(updated.ports); - body.gpus = updateDialog.value.updateGpuConfig ? configGpu(updated) : undefined; + body.gpus = updateDialog.value.updateGpuConfig ? configGpu(updated, true) : undefined; await vmmRpc.updateVm(body); updateDialog.value.encryptedEnvs = []; updateDialog.value.show = false; @@ -3054,8 +3065,21 @@

Derive VM

alert('failed to upgrade VM'); } } + function getKeyProvider(vm) { + var _a, _b, _c, _d; + if ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.key_provider) { + return (_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.key_provider; + } + if ((_c = vm.appCompose) === null || _c === void 0 ? void 0 : _c.kms_enabled) { + return 'kms'; + } + if ((_d = vm.appCompose) === null || _d === void 0 ? void 0 : _d.local_key_provider_enabled) { + return 'local'; + } + return 'none'; + } async function showCloneConfig(vm) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m; const theVm = await ensureVmDetails(vm); if (!((_a = theVm === null || theVm === void 0 ? void 0 : theVm.configuration) === null || _a === void 0 ? void 0 : _a.compose_file)) { alert('Compose file not available for this VM. Please open its details first.'); @@ -3064,7 +3088,7 @@

Derive VM

const config = theVm.configuration; // Populate vmForm with current VM data, but clear envs and ports vmForm.value = { - name: `${config.name || vm.name}-cloned`, + name: `${config.name || vm.name}`, image: config.image || '', dockerComposeFile: ((_b = theVm.appCompose) === null || _b === void 0 ? void 0 : _b.docker_compose_file) || '', preLaunchScript: ((_c = theVm.appCompose) === null || _c === void 0 ? void 0 : _c.pre_launch_script) || '', @@ -3082,15 +3106,14 @@

Derive VM

ports: [], // Clear port mappings storage_fs: ((_g = theVm.appCompose) === null || _g === void 0 ? void 0 : _g.storage_fs) || 'ext4', app_id: config.app_id || '', - kms_enabled: !!((_h = theVm.appCompose) === null || _h === void 0 ? void 0 : _h.kms_enabled), kms_urls: config.kms_urls || [], - local_key_provider_enabled: !!((_j = theVm.appCompose) === null || _j === void 0 ? void 0 : _j.local_key_provider_enabled), - key_provider_id: ((_k = theVm.appCompose) === null || _k === void 0 ? void 0 : _k.key_provider_id) || '', - gateway_enabled: !!((_l = theVm.appCompose) === null || _l === void 0 ? void 0 : _l.gateway_enabled), + key_provider: getKeyProvider(theVm), + key_provider_id: ((_h = theVm.appCompose) === null || _h === void 0 ? void 0 : _h.key_provider_id) || '', + gateway_enabled: !!((_j = theVm.appCompose) === null || _j === void 0 ? void 0 : _j.gateway_enabled), gateway_urls: config.gateway_urls || [], - public_logs: !!((_m = theVm.appCompose) === null || _m === void 0 ? void 0 : _m.public_logs), - public_sysinfo: !!((_o = theVm.appCompose) === null || _o === void 0 ? void 0 : _o.public_sysinfo), - public_tcbinfo: !!((_p = theVm.appCompose) === null || _p === void 0 ? void 0 : _p.public_tcbinfo), + public_logs: !!((_k = theVm.appCompose) === null || _k === void 0 ? void 0 : _k.public_logs), + public_sysinfo: !!((_l = theVm.appCompose) === null || _l === void 0 ? void 0 : _l.public_sysinfo), + public_tcbinfo: !!((_m = theVm.appCompose) === null || _m === void 0 ? void 0 : _m.public_tcbinfo), pin_numa: !!config.pin_numa, hugepages: !!config.hugepages, no_tee: !!config.no_tee, @@ -3398,24 +3421,20 @@

Derive VM

} } function getVmFeatures(vm) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; + var _a, _b, _c, _d; const features = []; - // Check KMS - const kmsEnabled = ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.kms_enabled) || ((_c = (_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.features) === null || _c === void 0 ? void 0 : _c.includes('kms')) || - ((_e = (_d = vm.configuration) === null || _d === void 0 ? void 0 : _d.kms_urls) === null || _e === void 0 ? void 0 : _e.length) > 0; - if (kmsEnabled) - features.push("kms"); + const kp = getKeyProvider(vm); + if (kp && kp != 'none') + features.push(kp); // Check Gateway/TProxy - const gatewayEnabled = ((_f = vm.appCompose) === null || _f === void 0 ? void 0 : _f.gateway_enabled) || ((_g = vm.appCompose) === null || _g === void 0 ? void 0 : _g.tproxy_enabled) || - ((_j = (_h = vm.appCompose) === null || _h === void 0 ? void 0 : _h.features) === null || _j === void 0 ? void 0 : _j.includes('tproxy-net')) || ((_l = (_k = vm.configuration) === null || _k === void 0 ? void 0 : _k.gateway_urls) === null || _l === void 0 ? void 0 : _l.length) > 0; - if (gatewayEnabled) + if ((_a = vm.appCompose) === null || _a === void 0 ? void 0 : _a.gateway_enabled) features.push("gateway"); // Check other features from appCompose - if ((_m = vm.appCompose) === null || _m === void 0 ? void 0 : _m.public_logs) + if ((_b = vm.appCompose) === null || _b === void 0 ? void 0 : _b.public_logs) features.push("logs"); - if ((_o = vm.appCompose) === null || _o === void 0 ? void 0 : _o.public_sysinfo) + if ((_c = vm.appCompose) === null || _c === void 0 ? void 0 : _c.public_sysinfo) features.push("sysinfo"); - if ((_p = vm.appCompose) === null || _p === void 0 ? void 0 : _p.public_tcbinfo) + if ((_d = vm.appCompose) === null || _d === void 0 ? void 0 : _d.public_tcbinfo) features.push("tcbinfo"); return features.length > 0 ? features.join(', ') : 'None'; } diff --git a/vmm/ui/src/components/CreateVmDialog.ts b/vmm/ui/src/components/CreateVmDialog.ts index 21d2fa99c..8f13d36d7 100644 --- a/vmm/ui/src/components/CreateVmDialog.ts +++ b/vmm/ui/src/components/CreateVmDialog.ts @@ -123,11 +123,24 @@ const CreateVmDialogComponent = { /> +
+ + +
+ +
+ + +
+
- - @@ -138,11 +151,6 @@ const CreateVmDialogComponent = {
-
- - -
-
From ab49844c7d68e841eda993a5c733ebae18376bd2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:48:46 +0800 Subject: [PATCH 012/264] vmm: Support for key-provider tpm (it doesn't work) --- vmm/src/app/qemu.rs | 18 ++++++- vmm/ui/src/composables/useVmManager.ts | 70 ++++++++++++++------------ 2 files changed, 54 insertions(+), 34 deletions(-) diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index c6dcb6b3c..6feb42fda 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -25,7 +25,7 @@ use dstack_types::{ shared_filenames::{ APP_COMPOSE, ENCRYPTED_ENV, HOST_SHARED_DISK_LABEL, INSTANCE_INFO, USER_CONFIG, }, - AppCompose, + AppCompose, KeyProviderKind, }; use dstack_vmm_rpc as pb; use fs_err as fs; @@ -428,6 +428,7 @@ impl VmConfig { if !shared_dir.exists() { fs::create_dir_all(&shared_dir)?; } + let app_compose = workdir.app_compose().context("Failed to get app compose")?; let qemu = &cfg.qemu_path; let mut smp = self.manifest.vcpu.max(1); let mut mem = self.manifest.memory; @@ -533,6 +534,21 @@ impl VmConfig { self.configure_machine(&mut command, &workdir, cfg)?; self.configure_smbios(&mut command, cfg); + if matches!(app_compose.key_provider(), KeyProviderKind::Tpm) { + let tpm_path = if Path::new("/dev/tpmrm0").exists() { + "/dev/tpmrm0" + } else if Path::new("/dev/tpm0").exists() { + "/dev/tpm0" + } else { + bail!("TPM key provider requested but no TPM device found on host"); + }; + command + .arg("-tpmdev") + .arg(format!("passthrough,id=tpm0,path={tpm_path}")) + .arg("-device") + .arg("tpm-tis,tpmdev=tpm0"); + } + command .arg("-device") .arg(format!("vhost-vsock-pci,guest-cid={}", self.cid)); diff --git a/vmm/ui/src/composables/useVmManager.ts b/vmm/ui/src/composables/useVmManager.ts index ac5981365..023bd5608 100644 --- a/vmm/ui/src/composables/useVmManager.ts +++ b/vmm/ui/src/composables/useVmManager.ts @@ -32,7 +32,7 @@ type AppCompose = { pre_launch_script?: string; }; -type KeyProviderKind = 'none' | 'kms' | 'local'; +type KeyProviderKind = 'none' | 'kms' | 'local' | 'tpm'; const x25519 = require('../lib/x25519.js'); const { getVmmRpcClient } = require('../lib/vmmRpcClient'); @@ -95,8 +95,7 @@ type VmFormState = { encryptedEnvs: EncryptedEnvEntry[]; storage_fs: string; app_id: string | null; - kms_enabled: boolean; - local_key_provider_enabled: boolean; + key_provider?: KeyProviderKind; key_provider_id: string; gateway_enabled: boolean; public_logs: boolean; @@ -176,8 +175,7 @@ function createVmFormState(preLaunchScript: string): VmFormState { encryptedEnvs: [], storage_fs: '', app_id: null, - kms_enabled: true, - local_key_provider_enabled: false, + key_provider: 'kms', key_provider_id: '', gateway_enabled: true, public_logs: true, @@ -679,12 +677,9 @@ type CreateVmPayloadSource = { return 'running'; }; - const kmsEnabled = (vm: any) => vm.appCompose?.kms_enabled || vm.appCompose?.features?.includes('kms'); - - const gatewayEnabled = (vm: any) => - vm.appCompose?.gateway_enabled || vm.appCompose?.tproxy_enabled || vm.appCompose?.features?.includes('tproxy-net'); + const kmsEnabled = (vm: any) => getKeyProvider(vm) === 'kms'; - const defaultTrue = (v: boolean | undefined) => (v === undefined ? true : v); + const gatewayEnabled = (vm: any) => vm.appCompose?.gateway_enabled; function formatMemory(memoryMB?: number) { if (!memoryMB) { @@ -711,18 +706,28 @@ type CreateVmPayloadSource = { name: vmForm.value.name, runner: 'docker-compose', docker_compose_file: vmForm.value.dockerComposeFile, - kms_enabled: vmForm.value.kms_enabled, gateway_enabled: vmForm.value.gateway_enabled, public_logs: vmForm.value.public_logs, public_sysinfo: vmForm.value.public_sysinfo, public_tcbinfo: vmForm.value.public_tcbinfo, - local_key_provider_enabled: vmForm.value.local_key_provider_enabled, key_provider_id: vmForm.value.key_provider_id, allowed_envs: vmForm.value.encryptedEnvs.map((env) => env.key), no_instance_id: !vmForm.value.gateway_enabled, secure_time: false, }; + if (vmForm.value.key_provider !== undefined) { + appCompose.key_provider = vmForm.value.key_provider; + + // For backward compatibility + if (vmForm.value.key_provider === 'kms') { + appCompose.kms_enabled = true; + } + if (vmForm.value.key_provider === 'local') { + appCompose.local_key_provider_enabled = true; + } + } + if (vmForm.value.storage_fs) { appCompose.storage_fs = vmForm.value.storage_fs; } @@ -742,14 +747,6 @@ type CreateVmPayloadSource = { } const imgFeatures = imageVersionFeatures(imageVersion(vmForm.value.image)); - if (imgFeatures.compose_version < 2) { - const features: string[] = []; - if (vmForm.value.kms_enabled) features.push('kms'); - if (vmForm.value.gateway_enabled) features.push('tproxy-net'); - appCompose.features = features; - appCompose.manifest_version = 1; - appCompose.version = '1.0.0'; - } if (imgFeatures.compose_version < 3) { appCompose.tproxy_enabled = appCompose.gateway_enabled; delete appCompose.gateway_enabled; @@ -788,12 +785,11 @@ type CreateVmPayloadSource = { () => vmForm.value.name, () => vmForm.value.dockerComposeFile, () => vmForm.value.preLaunchScript, - () => vmForm.value.kms_enabled, () => vmForm.value.gateway_enabled, () => vmForm.value.public_logs, () => vmForm.value.public_sysinfo, () => vmForm.value.public_tcbinfo, - () => vmForm.value.local_key_provider_enabled, + () => vmForm.value.key_provider, () => vmForm.value.key_provider_id, () => vmForm.value.encryptedEnvs, () => vmForm.value.storage_fs, @@ -952,7 +948,7 @@ type CreateVmPayloadSource = { const composeFile = await makeAppComposeFile(); const encryptedEnv = await encryptEnv( vmForm.value.encryptedEnvs, - vmForm.value.kms_enabled, + vmForm.value.key_provider === 'kms', vmForm.value.app_id, ); const payload = buildCreateVmPayload({ @@ -1066,6 +1062,19 @@ type CreateVmPayloadSource = { } } + function getKeyProvider(vm: VmListItem): KeyProviderKind { + if (vm.appCompose?.key_provider) { + return vm.appCompose?.key_provider; + } + if (vm.appCompose?.kms_enabled) { + return 'kms'; + } + if (vm.appCompose?.local_key_provider_enabled) { + return 'local'; + } + return 'none'; + } + async function showCloneConfig(vm: VmListItem) { const theVm = await ensureVmDetails(vm); if (!theVm?.configuration?.compose_file) { @@ -1076,7 +1085,7 @@ type CreateVmPayloadSource = { // Populate vmForm with current VM data, but clear envs and ports vmForm.value = { - name: `${config.name || vm.name}-cloned`, + name: `${config.name || vm.name}`, image: config.image || '', dockerComposeFile: theVm.appCompose?.docker_compose_file || '', preLaunchScript: theVm.appCompose?.pre_launch_script || '', @@ -1094,9 +1103,8 @@ type CreateVmPayloadSource = { ports: [], // Clear port mappings storage_fs: theVm.appCompose?.storage_fs || 'ext4', app_id: config.app_id || '', - kms_enabled: !!theVm.appCompose?.kms_enabled, kms_urls: config.kms_urls || [], - local_key_provider_enabled: !!theVm.appCompose?.local_key_provider_enabled, + key_provider: getKeyProvider(theVm), key_provider_id: theVm.appCompose?.key_provider_id || '', gateway_enabled: !!theVm.appCompose?.gateway_enabled, gateway_urls: config.gateway_urls || [], @@ -1435,15 +1443,11 @@ type CreateVmPayloadSource = { function getVmFeatures(vm: VmListItem) { const features = []; - // Check KMS - const kmsEnabled = vm.appCompose?.kms_enabled || vm.appCompose?.features?.includes('kms') || - vm.configuration?.kms_urls?.length > 0; - if (kmsEnabled) features.push("kms"); + const kp = getKeyProvider(vm); + if (kp && kp != 'none') features.push(kp); // Check Gateway/TProxy - const gatewayEnabled = vm.appCompose?.gateway_enabled || vm.appCompose?.tproxy_enabled || - vm.appCompose?.features?.includes('tproxy-net') || vm.configuration?.gateway_urls?.length > 0; - if (gatewayEnabled) features.push("gateway"); + if (vm.appCompose?.gateway_enabled) features.push("gateway"); // Check other features from appCompose if (vm.appCompose?.public_logs) features.push("logs"); From f40384bebc32e38500179a90827571174e83f25a Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:49:53 +0800 Subject: [PATCH 013/264] vmm: Support for quote_generation_socket --- vmm/src/app/qemu.rs | 52 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index 6feb42fda..62bb37d1b 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -531,7 +531,7 @@ impl VmConfig { command.arg("-netdev").arg(netdev); command.arg("-device").arg("virtio-net-pci,netdev=net0"); - self.configure_machine(&mut command, &workdir, cfg)?; + self.configure_machine(&mut command, &workdir, cfg, &app_compose)?; self.configure_smbios(&mut command, cfg); if matches!(app_compose.key_provider(), KeyProviderKind::Tpm) { @@ -778,6 +778,7 @@ impl VmConfig { command: &mut Command, workdir: &VmWorkDir, cfg: &CvmConfig, + app_compose: &AppCompose, ) -> Result<()> { if self.manifest.no_tee { command @@ -792,8 +793,9 @@ impl VmConfig { let img_ver = self.image.info.version_tuple().unwrap_or_default(); let support_mr_config_id = img_ver >= (0, 5, 2); - let tdx_object = if cfg.use_mrconfigid && support_mr_config_id { - let app_compose = workdir.app_compose().context("Failed to get app compose")?; + + // Compute mrconfigid if needed + let mrconfigid = if cfg.use_mrconfigid && support_mr_config_id { let compose_hash = workdir .app_compose_hash() .context("Failed to get compose hash")?; @@ -820,12 +822,48 @@ impl VmConfig { key_provider_id, } }; - let mrconfigid = BASE64_STANDARD.encode(mr_config.to_mr_config_id()); - format!("tdx-guest,id=tdx,mrconfigid={mrconfigid}") + Some(BASE64_STANDARD.encode(mr_config.to_mr_config_id())) } else { - "tdx-guest,id=tdx".to_string() + None + }; + + // Build tdx-guest object with optional quote-generation-socket for kernel-level TSM support + #[derive(Serialize)] + struct QgsSocket { + r#type: &'static str, + cid: &'static str, + port: String, + } + + #[derive(Serialize)] + struct TdxGuestObject { + #[serde(rename = "qom-type")] + qom_type: &'static str, + id: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + mrconfigid: Option, + #[serde( + rename = "quote-generation-socket", + skip_serializing_if = "Option::is_none" + )] + quote_generation_socket: Option, + } + + let tdx_object = TdxGuestObject { + qom_type: "tdx-guest", + id: "tdx", + mrconfigid: mrconfigid.clone(), + quote_generation_socket: cfg.qgs_port.map(|port| QgsSocket { + r#type: "vsock", + cid: "2", + port: port.to_string(), + }), }; - command.arg("-object").arg(tdx_object); + + // Use JSON format when quote-generation-socket is needed, otherwise use simple format + let tdx_object_arg = + serde_json::to_string(&tdx_object).context("failed to serialize tdx-guest object")?; + command.arg("-object").arg(tdx_object_arg); Ok(()) } From 4502f62cd59aee79a153640208f29a11d6b2d631 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:50:43 +0800 Subject: [PATCH 014/264] vmm-cli: Support for key provider --- vmm/src/vmm-cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vmm/src/vmm-cli.py b/vmm/src/vmm-cli.py index 7bf4b3fa6..44cda20db 100755 --- a/vmm/src/vmm-cli.py +++ b/vmm/src/vmm-cli.py @@ -521,6 +521,8 @@ def create_app_compose(self, args) -> None: "no_instance_id": args.no_instance_id, "secure_time": args.secure_time, } + if args.key_provider: + app_compose["key_provider"] = args.key_provider if args.prelaunch_script: app_compose["pre_launch_script"] = open( args.prelaunch_script, 'rb').read().decode('utf-8') @@ -1160,6 +1162,9 @@ def main(): '--gateway', action='store_true', help='Enable dstack-gateway') compose_parser.add_argument( '--local-key-provider', action='store_true', help='Enable local key provider') + compose_parser.add_argument( + '--key-provider', choices=['none', 'kms', 'local', 'tpm'], default=None, + help='Override key provider type (none, kms, local, or tpm)') compose_parser.add_argument( '--key-provider-id', default=None, help='Key provider ID if you want to bind to a specific key provider') compose_parser.add_argument( From 9f771a15523fe64cad98643eb92159ce6b0f3ab0 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:01:03 +0800 Subject: [PATCH 015/264] eventlog: Refactor cc-eventlog --- cc-eventlog/src/lib.rs | 128 +++++++++++++++++++++++++------------- ra-tls/src/attestation.rs | 2 +- 2 files changed, 85 insertions(+), 45 deletions(-) diff --git a/cc-eventlog/src/lib.rs b/cc-eventlog/src/lib.rs index 50f491cc1..2590f6f33 100644 --- a/cc-eventlog/src/lib.rs +++ b/cc-eventlog/src/lib.rs @@ -4,8 +4,9 @@ use crate::codecs::VecOf; use anyhow::{Context, Result}; -use scale::Decode; +use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; +use sha2::Digest; use tcg::{TcgDigest, TcgEfiSpecIdEvent}; mod codecs; @@ -15,6 +16,10 @@ mod tcg; pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/tdx_mr3/tdx_events.log"; /// The path to boottime ccel file. const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL"; +/// The event type for dstack runtime events. +/// This code is not defined in the TCG specification. +/// See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf +pub const DSTACK_RUNTIME_EVENT_TYPE: u32 = 0x08000001; /// This is the common struct for tcg event logs to be delivered in different formats. /// Currently TCG supports several event log formats defined in TCG_PCClient Spec, @@ -39,15 +44,15 @@ pub struct TcgEventLog { /// which is one-based. /// /// As for RTMR3, the digest extended is calculated as `sha384(event_type.to_ne_bytes() || b":" || event || b":" || event_payload)`. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TdxEventLog { +#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] +pub struct TdxEventLogEntry { /// IMR index, starts from 0 pub imr: u32, /// Event type pub event_type: u32, /// Digest - #[serde(with = "serde_human_bytes")] - pub digest: [u8; 48], + #[serde(with = "serde_human_bytes", default)] + digest: Vec, /// Event name pub event: String, /// Event payload @@ -66,41 +71,59 @@ fn event_digest(ty: u32, event: &str, payload: &[u8]) -> [u8; 48] { hasher.finalize().into() } -impl TdxEventLog { +impl TdxEventLogEntry { pub fn new(imr: u32, event_type: u32, event: String, event_payload: Vec) -> Self { - let digest = event_digest(event_type, &event, &event_payload); Self { imr, event_type, - digest, + digest: vec![], event, event_payload, } } - pub fn new_str(imr: u32, event_type: u32, event: &str, event_payload: &str) -> Self { - Self::new( - imr, - event_type, - event.to_string(), - event_payload.as_bytes().to_vec(), - ) + /// Create a version of this event with payload stripped (for size reduction). + /// Only call this on events where can_strip_payload() returns true. + pub fn stripped(&self) -> Self { + if self.is_dstack_runtime_event() { + Self { + imr: self.imr, + event_type: self.event_type, + digest: Vec::new(), + event: self.event.clone(), + event_payload: self.event_payload.clone(), + } + } else { + Self { + imr: self.imr, + event_type: self.event_type, + digest: self.digest.clone(), + event: self.event.clone(), + event_payload: Vec::new(), + } + } } - pub fn validate(&self) -> Result<()> { - if self.imr != 3 { - // TODO: validate other imrs - return Ok(()); + pub fn digest(&self) -> Vec { + if self.is_dstack_runtime_event() { + return event_digest(self.event_type, &self.event, &self.event_payload).to_vec(); } - let digest = event_digest(self.event_type, &self.event, &self.event_payload); - if digest != self.digest { - return Err(anyhow::anyhow!("invalid digest")); - } - Ok(()) + self.digest.clone() + } + + pub fn is_dstack_runtime_event(&self) -> bool { + self.event_type == DSTACK_RUNTIME_EVENT_TYPE } } -impl TryFrom for TdxEventLog { +/// Strip large payloads from event logs to reduce size. +/// CCEL boot-time events only need their digest for RTMR replay, not the payload. +/// Runtime events need their payload for verification, so those are kept. +pub fn strip_event_log_payloads(events: &[TdxEventLogEntry]) -> Vec { + events.iter().map(|e| e.stripped()).collect() +} + +impl TryFrom for TdxEventLogEntry { type Error = anyhow::Error; fn try_from(value: TcgEventLog) -> Result { @@ -116,15 +139,12 @@ impl TryFrom for TdxEventLog { .into_iter() .next() .context("digest not found")? - .hash - .try_into() - .ok() - .context("invalid digest size")?; - Ok(TdxEventLog { + .hash; + Ok(TdxEventLogEntry { imr: value .imr_index .checked_sub(1) - .context("invalid imr index")?, + .context("invalid IMR index: must be >= 1")?, event_type: value.event_type, digest, event: Default::default(), @@ -210,18 +230,12 @@ impl EventLogs { Self::decode(&mut data.as_slice()) } - pub fn into_tdx_event_logs(self) -> Result> { - self.event_logs - .into_iter() - .map(TdxEventLog::try_from) - .collect() - } - - pub fn to_tdx_event_logs(&self) -> Result> { + pub fn to_tdx_event_logs(&self) -> Result> { self.event_logs .iter() + .filter(|log| log.imr_index > 0) // GCP fills some IMRs starting from 0 .cloned() - .map(TdxEventLog::try_from) + .map(TdxEventLogEntry::try_from) .collect() } } @@ -256,7 +270,8 @@ fn parse_spec_id_event_log( Ok((spec_id_header, spec_id_event)) } -fn read_runtime_event_logs() -> Result> { +/// Read runtime event logs from the runtime event log file +pub fn read_runtime_event_logs() -> Result> { let data = match fs_err::read_to_string(RUNTIME_EVENT_LOG_FILE) { Ok(data) => data, Err(e) => { @@ -271,20 +286,45 @@ fn read_runtime_event_logs() -> Result> { if line.trim().is_empty() { continue; } - let event_log = - serde_json::from_str::(line).context("Failed to decode user event log")?; + let event_log = serde_json::from_str::(line) + .context("Failed to decode user event log")?; event_logs.push(event_log); } Ok(event_logs) } /// Read both boottime and runtime event logs. -pub fn read_event_logs() -> Result> { +pub fn read_event_logs() -> Result> { let mut event_logs = EventLogs::decode_from_ccel_file()?.to_tdx_event_logs()?; event_logs.extend(read_runtime_event_logs()?); Ok(event_logs) } +/// Replay event logs +pub fn replay_event_logs( + eventlog: &[TdxEventLogEntry], + to_event: Option<&str>, + imr: u32, +) -> Result<[u8; 48]> { + let mut mr = [0u8; 48]; + + for event in eventlog.iter() { + if event.imr == imr { + let mut hasher = sha2::Sha384::new(); + hasher.update(mr); + hasher.update(event.digest()); + mr = hasher.finalize().into(); + } + if let Some(to_event) = to_event { + if event.event == to_event { + break; + } + } + } + + Ok(mr) +} + #[cfg(test)] mod tests { use super::*; diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index 103f7f62d..9efe3cfb8 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -17,7 +17,7 @@ use sha2::{Digest as _, Sha384}; use x509_parser::parse_x509_certificate; use crate::{oids, traits::CertExt}; -use cc_eventlog::TdxEventLog as EventLog; +use cc_eventlog::TdxEventLogEntry as EventLog; use or_panic::ResultOrPanic; use serde_human_bytes as hex_bytes; From b6deccb513b6bf40454621cc36f634a560910bc8 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:01:31 +0800 Subject: [PATCH 016/264] eventlog: Add tpm in cc-eventlog --- cc-eventlog/samples/tpm_eventlog.bin | Bin 0 -> 29812 bytes cc-eventlog/src/lib.rs | 1 + cc-eventlog/src/tpm.rs | 245 +++++++++++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 cc-eventlog/samples/tpm_eventlog.bin create mode 100644 cc-eventlog/src/tpm.rs diff --git a/cc-eventlog/samples/tpm_eventlog.bin b/cc-eventlog/samples/tpm_eventlog.bin new file mode 100644 index 0000000000000000000000000000000000000000..bf8459f05b8005ad4cf5f06a90b537fad812051b GIT binary patch literal 29812 zcmeFZ1yqz<+c!RR4c#3|*9?u)jdXW2bT^80cY`3(2$CX5OLqwl>Gj`@Vm3?Q8bz{kvjc_XYp}00ijYzk4x5FKW0zENJAVXr#R$ zP97lcpD=g;{PkxL0R#XH01W^L0R6mveevgwpY6Z?i}Bay-?V^#bM2om|kErpGtE3vH}2bDO&6(jY10TsXIceQ~1P~vd+%d4iGjPc_#}F0MXADjij@qi>C*K zM%u~R)(HZD1N;?sEeMf|ga5~OVuJQ06CYL(P-rI|>kFh_iGf_n0Biwn07rl~zzq5h z0jLAy0ZM?sqsj-94P$%&Z|C)<1^Z%*EE7!@}8- z)7k=J;$dNKV(a7qv39fd@a1&&a;o~u}aChXivI2uGxxw7#JbYXbE(?BhkN}v6 z+lrTmUx3$&pWlifY{d)x515ai+ftC5$3g(Y1roFX3Gi_V^6>HqSO{3~fgx5_Jm%bH zf|iybevkl!8^UYJF9-qiaa(Z(hY z;S;K(kGExTkjAG*A7A0q7OL-padz1>_&GRHDjRfkn_y;x)ck^pJw($W4g zgFFD9&>7?aZNtte8h{so>zC5F|I*aBBJ8#DEtl;Q#>)!RMMjSW#z|=mKrL1e{lZwO z+C14Zr-Z{5heEvSx=%>sW)$YQhV3^jY9OFXyzSfF!B}0=PiH#P8QKm5b?-K?J6}q9 zN7p$roC?O7AeAu;F-Oi68GCD1xQ>6PxzXE2lF#6m6oX0e!w@&AP?8oP z;Hia{P&%?VaH@C><#eHQ4{r28OaH6J*&S-T?~>aOSkRA3PO)cKKH^BH+Z@*5F5H8T z4y?NHKeYrfT#p>|r3z306f=1iG#MFJB%6_J@JzOa zJPH&He*y}I`;>>n7!xc265s-JfqB7TUi}|UK`zj*=HGlG7>@4GV&RYg2*GgJ0BHB<@WF6! zfRU!RsD7u8_7+>j@Pp<@7%79_6PI)R*rdn8;MCRS{LUpA+A)ixY7Rc)_w#K8*rq*; zb$(rN%bBN3<@po)zB5Ad#?;Qo@h%Zb5@WWs*eMU+d*-%-iD^_j_X2O7zq+fvj6Nf~ zl>@4NV^oG<5mj&@xR{$S>QyE#Tt%DFen4xL18d!Mvf-s9_; zqWMmh`RT-6swJB7=L9@lX>dy0SI?GwGAAcQIoc|ii&wZIiz(B{H<$b(t#s_(vdk@A z1$YEF09>I4NDV{@9cfA|L?T3jOw~{Xi9^m08NrN986IF6W7FYY5cT!#TZkkeqQJig zU|$39;ozI{Ooij; zhU{&-3{p25@gK*s&9qiL4A$$e$L--La3Ct)$)wyz0N>&}9R`J1-?>B0YEDaR%GP|$ z@r}u8$jU{RDcm~2Hq-&Ys7wD?l>lkerP0($Gwfb9s$+sQd5Hnf1%ncWP|#w_Z4TcB z{E2}Izjpxwvbd5#r1spZI?L`)N&+H2b}!w#aDSkHf)>#qg2t!6Wg8(D3S>(SaYd~y?5mu;E&9;3oWwcr^L_HHanIZh#qv?L2pe7Q+IMiRi10W9^emYQ zl+x$c+}ZSxMJvdQB&9}1rv@;b7XP}2yQX&b6QRSxcuCGXrSH+>>B>HbOC3-d>aKQsoz}1B*;Hk_yyA6 ztzx7NP%y&lpQ{+%41FT<2N(h7hX(~EfiXa6*XtM=A6`oXj1R)SZlL0$>zKLQ*g9Ez zI6HxHKv>roQSedJA(oELPL^OQ5XJQsbbLZ3TMIX5cV{aPsN3V_0yWbfw$M-91qoi? zN5JR$+xs+{Hnwh-G^%E99=NfxuuWL;W*z^=#Bfw^!;g)G%~!t)J`M zmx4D!!F+#j)q`cN27AYGdncf2S>J-o50_X4YZ>FI0AWge-|<4|^Q>&P_2Y)5X|%n((pjoQt_v!5zDsqhm)JcIJ05B%7A#FTW#wd(%Tu zj=OX#D$b>$h*D{%YW49MQP)i8@SBYu&ecbsZ^t0)#2v(Jb>BOe#Kpt#ua`}Bm|H`P z+CJt++{S@pab5iuf-iJMkH%@^C3DrR@@lTql|}d2Io71F@Jcr1?g%qW?uDCP!FE;h z-U%;$U?2R>ipfZh*XW)_a%Z)#Sg3DAz_NPjM$Q8i+XTX&%oPl$0v#y@5DqdLD>57+ zTrfW&93s@N0RF^h<`o(e(tFL!BFO z5C-%&qQpkL784G-L2G{|LTM{?$whx%-1ceZC%3dv@7|%aD+L)|f1Dao3j}4=Je549 zaD{6h6Y64GxH)h*{+`P}yh|5*+v}^GE^f}2o)#YNoX}|jo#W7{3!S&nNei8;CeXELtAG zTfh2RO77FvJQk^I37pm$AT2aOn*PxD+ICD%fvaKLXcBbXaopJKgHp))J^KTXcIZy< zP?b2n%VYb^?}^wm>C~(1snoSBz&M2~CT5%mUuXP^C%l@F27Gt16O$izeV=#scffw| zrc?SOk6OAQ_NuY=ED2Kj5lV@~i#+d3RMG=i?UCH@TSXDWC6~JXZ)~jzll+4Y0`ONc zW1@v3LbsR4+9X#i*+Wy}?O65wq|Vfz1r4xMv@EF418`5XggZ@Z7P;#SBPSrF=w#~^ zenp+Z5y}G3IWc<)BAhSdI9df?uCH}3tc)^-?xjlXF~=mzh~Et&x1RkbsRtRxY`p{| zs~maWeOh+=LRDPMcY$36-Hy@ANqsln_CYoF*9fH7(p{Ar1VPm2AjD_55m zrs7#wV~RkhQI+INIh`#|O&G4}Qu_&$7u&Cf$E`WT^zOdzc0nzLQ2|k7MDHf~Nqq*r zvq0{0(pj``X16U?Lm>qXQpV{JK^0)8=dgnS$J3rCkG%eo>7ijwuZ}X`Bv)wr6IkzV z32G0nUwwaX7iyt)A?v4Iz`qW>@d=0;%^ZV+6@GjOA7+Mu^#97W|Bm=sLCil%lo=J8k%`xopl47~gk@*1ws4N}yr7YXM- z#R#@lD?&UqR^Ov@WpZ*eykvcq`Y3njeOEjZPclBXMY~6VTPd@DE*C8`s$P$G4@#lM@bedN7^3jrwAXp}Q0LoNP7EBT zY`Lr&9D<^EsjD^yqfYDhcUtr08_P6il{jl347_ixUB4)I!SPO$mcOi=6eS+E$~t(+ z_Ys|KsUw1xJ>>&x`s9Y8=HhP-cZ80V?GF!t68qYN{7Jq4#OVv=ZmsDeSB+NL4{`J* zMGPf0BmR-o|B=Q+rNWT`nWAjZUi3yCR&Wfi<)9&eAU|o`7-R_2&(qD*4%hrAc6D}f z|H0({nilk2An2ro{`+fMUbms12lP$y`s(#`{K4mLzxf-==T;y~kooU6l+U&QZI8bx z{ilC)-N(Q0^KThG3?3Qp%(_ZNu&j}1&WOc_^nt+V$H58YOG3h2=*I23$m`ChaU`YW zp~u-nl`*JNSPXG&vnEcr=uyrL6d5?Yz>*wJ8w23Zdy@z74JBVZSJHQpmxj`Oq)NHH z-mogow9ZA`EmvZ$Opj`K+m%o*({1$Pa<}-&u~!3Y*i~kJu02Jz$k)$U#D$l9oQtDO znG|A=7={qyN_e*>(VxZ;Rnfu$s2H&uuxcg1+8k@A@moNiedT!Bjw4*$x)l`EB8@(sZ>h3n(ajzs5N=y&+6_Ezt5xA3u0wCiDalb zb2!-UoUIaHbiQcE-*6&WU++hV5qpFqfb8hf1~>32PMcByTjf?Mz1XJ~CU(i{qE?!a zdgO0Nw7GYid@)_@H99=I$Z_yrtT>~Uj!!gTxf(VMCahBI<7>T3X{s_39m1&5vy;Bp zy|~QigGm&0q09bUyqN@Ze1ESTl}hFGqVH1SBjp~5URAJg{<~-xy>(OtB4Ck0~ zq^PrtDV?hQPL8v$?c>wjEL8`0wW6Fl6$+5waMhPNLg4OyQP&|s0vR?kS>ZO^-*;6G zH#5EOsNHXXCQ`8X2J1=k6!oK%oadB2KK54|EQr-U)*|~X7*7|B;5H*3@q!X}XQb^P zkE|7}tPwx`_}9n;{UgWPW425W!Be#IrX zq3<`d3&=Q7`*Zp`E9GCDzQyg48*XHU+Tmo%!UpGNPL4UPZ|C?f| zzcZ2E_C zN%1#dc03S!ut?3sIC!?FZA`J|pX%SSFIDR+xca`jwSj?WLD_~5O zSJx{I^|Rgc0=T!ojhYwKsJVgg{uO6H0~4el_dvJ)xCVcna5?R?ZwT<#xOYg;PpO>G z@Bo{}CGb!23w~w0BwAar%j4L$DqyF#beYlQZ~kBW@?S(qe{0=%Q6;WGBV%8^fR#8l zy1dLXTwaWRdwAlE66E)uIR**H>EUY~ZtC)s5McyAGpY=J9~dHzoKQ!3fY5YEW<>r) zHSyaEtxyRj3YxasDm8-Mj*5&lT8hLE*caYU(%h3GhuYfIn~+p7=XIXsneAe(I?}%r z!zLCR7%RNjrBt1TM~SML(AMAD#40S$O3ig)M{BE+J>W0FbHRs99FssJ*6|{X-ji{3 z5TWqIXkwjD^L~?R{S@8=N_&gP{2Wz6;M-l2BTfP$d(KZ*u{F0=`$K1Uw>r6lE439! z*uU7Mp^T32yX{xCd<+lEWTRE&X5GiWw{H_spRC2@nQs?tCsas}TeMp_gIF~@E2fth zI+3PmoW2^ahKf~ap2;fy3&`N2c_*{7gPO@b2Hbb8JKh86KtGDCRL z)%4{s10fO77Fi-UttEQ8=kFe@BOtRBex*v3=>}j_M`o` z{Xp^3{$W2*Z`hB>{}C?c0)c*p7|j3jC4Yr9P-oB9%GSaRy6-3M>0#r1y&ZL(D+2vY zXMsUrL9hT1n1}0!+4$Myy>5bnLjOlp`MuR`bxvb9d*L%pMGZ{pf-ePN?lP$LXs>b-lZ5yRzu!-uf z2Cv`h)DB})1jYBJ!CPp~r7}sKPU%A8Q^SR#M(}n@JGRvHOon_{my(3&sSBiasLhQv$BibA)i4^EF~c>%5GW`D2FQUqS5HBATk`w%IeU_^9ci3eqjv_WPdtR5|`=$1mJnr%RpIok#z$Fj{*6@jVLK z?rF^O*pYvH)&2=aH0rtNAf;P?M&tG?i-l!jFr+4E^`K&us3=#s^m%rd#OuUR6HUsi zsI6v7*+l0WcV+Qhv0RhYe3n|=B==}qrv0%`?t&&F<%Xs^^9VSlQ)rt2JOt!GnOMV1ED6=Fr91J^n%VA{{@9@4|D9*!pMF zJul$H;O}g`c;~cYOUwT$XyU|^6v=ujMaP(#TyWqs`{L+|w7bFU$+7$8M5Mtoy049M zf?ilxJMbQ~u8Q7X;w?b;L_Q(We)PtNZ32mW`*kE-&NG}ZNb}A9Y!(g3*|%tWzWF#T z=@KLJu_oamj1;{)U~m4Gnj^MLBK>77+R$F49AV0ZgazGh!9;fAtpz&QLG=tDHH(Y> zs)Qb@tAb9me@M_it0 zjXM;mm!XO7nxJ&`NNul_rKCuum0W)JcF8ndW+5g@4 z0GfT`19O7~cm;lJZ*cSeXbM6%Podizihnf+|1RT32_pZ=^8~-y{f~g)zmer?t>vB2 z^sP%631V@2fj6C!0rC?tZ1+tjMaO1G|K7e(I?E+GS?=r{)Tu;lD?J_-$XMw20z6Xu zROy@KYkkfc67osibR@yTw1(D?zMXmF2}>GOtPJL*6jgjwD*5wG_{~82mfH|(xM!5& zI1PNA_8j#NI8(~$uX9=QzwG7{Vg#r4URgxr*C+E(fsM^@tBgjz;H)Y74!YIyL04f`*e4I6%}ck8O%Mr&jce%$AWeDxs$#M7Zz&RTm~LuaJxq;-2Yhy33~$m+Py${S(dqW0vdx&&-}X`0~nW@$p@)lkYKtAI%V* zUsqa;5_{b%m5hoz?K_AyPF!5EDJI-$oH7N9=}?~=^SYUR$^bj~<{cANDNyXf0r$S@ zU`v2VY)|tGIWm$PUy*-j4L6pMz)gsEQA&Jz31@L&*`B@h05xb_{(zr&{>wMFscy>d zL}{h9K;*W|RXf|U7yim%gHZR<4L4Cn-a$W_F+jG;f?^$??eJF~TwD*l`=zlQK@KIV zm855-GSbe^t=+rrm&G<%Y!hzdEHXRA5k-EE53ovscQ$x}7&{U~QJJCng@nUjJ+W2o z6AM$-h&KY6wx|#l+KS~3>tmwEg&+a{E=x( zx4Acu`BQjo?7vlH>P z^F`$ndXD^Hb|P42|DWHWxTb1BE-(-O&kc$nP44R^NclgbYU&?(1L&?e(H}1Pe?-=8 zbSzbSWZ*D-AGN31mlr-07L8=t7RPwV9~8uS?VQVx?GQ&Y##`Q?qLG>1!CZThCv$~X z`6=-B9A4mo$vdLmC5fC-P9-yVvuFwhN)mIy@C?^VkpLrCe1%OGKqs_v3Xp3IsbwR( z^4Q%9nG}rT=5>FVS8yh_Vx_A4>>^^rkS2s@r+f*c*F@w>jukhFVUNT~r#F|x{n6f{ zod;_RXMk!v<$GbTmx!w)960?U?$uydmDX#n^csPam!yRf6+sY&oI$K@VBp zhG*g>ZB1ukFVQVyp)t`2rBJYZhl4=Qb8a;r?DRye6qo-15<`ntEtFy|eApWDQB*6D za1vaNyM8*97rfXs{z50EUyHvYK)jtgUFeV@_1o;E*6rx%m*V>iL>;%1V_ne|?J!BT zzTSD9Oq-tn_7pufr+sjHC{p4=p>FSr5kzbD*DVR-uVfK{R-tND$awSF#eV0;k5}t3 z0HKBU+DT3Babge9^3dq0nktM()Pg3?R!olWtcne`W&;5dVf5^2_u$3LeBxJol}i@B zEX)TSKK7$3C5arLoVu^z6L}cThJ|6lH=n^VJzV*&kK^W+#P4FMJm|ga*Y%c!&y7t9 zRm7ind7$rYS!dpQ2wX~Src6ic2|fy-U_=_I%|ZQXbKu}$EzDmMG_n$40%+d&x<(3! zA@A-0p>ei?mN5NMBn3o~a`v=#FmneJg9xv2=s+wB`r)zz zS^s(iD#zUe;zk3F$8RQ+e&msUR78Qn+|W<~YK(pqNP&MfK>>d0mVd6Np9wS_2KO6oWd@|6CL`Xp(K7fKg)^T=8mf$$VL1 zGM6g)tr;1sk-$;ISW{M`WTRw;cezRc&_xF0BEYfFaVyXmZnD9OD&iBS<7@GZ(&B2M zh00gIF7=T-a9K-~(IYAuXD#<8FWa4&&I{VI<2cq3>oMT=>MRE+2REOvDG%@~U4;>b$}a5Q^$TdN-h@MX3Uuz? zj4K>_RkA!PRIkhSqzi$oIg8)D4 zH|ekIH-G0?5D<{jf7MK(BOpUld1$8La&Y0N5^$J^)I^jjE)XXT4MiGRh!X@_atE>e z^PKt9?u3~fOF;-GdTpit7YsMm_leve$M*kHEn1oM2&0ST`7tqK?5&p&eTH}*eG8cR z|D!lGAvzzKnz;e}-rL^GpmO@Hj)zMZcIkP(Db}Bnu;}sVSM*Pwk$u5CM2d?r{-SR< z9CyndLP^;4iOzmn-g>aGZ^m5Nu>KoDh*;2E#A6{*3QV=!R0w*gob^huaw5(6Ja)7in+5y~RJimK7^ zktA*1Eu6uBMEBaZZXTXc2KnJIURNtX_xwOKKZ>k*K-^${9)4~feG+WwBMgF|>kE8f zL2i&fNc3MKfxkrls^-K1qKG?rn7KjR!Q@a+=(<`Hi1(LHG(63rUHrRM-KZ4)nzn$I zG&d2w#5)sgNT$=VjUOkHVs-%WpID2e8$~USAyLWUJXN5wN!n9X>-rjCeMBnrH0n|U zd$DLt!H(HlY`}UaVRPZr#r{&I^!{Y2PD>0%VYIu}vd+{+S5Ly0>RGV7oYiw5)&f~A zIU!4nI-DWoMS)OlgXa zHPWv7nT~t1TcCGMG>Mgok-)Pj!06Nz1)TRN0I)*VOR*UfHm+pDIt8Zp@OTNhp|#Ra ziE`BMe<7V3jL24X9~yc4b;^(-%Dm`McKcYeoZ|5lfQwi@@0U@Zt={Lq69_@jk$MP| zKtTRsDSobVR1h+>dgi+L(f{`%hbC6tKrSG$-)(4OmFG{1Rg3Ex@S_yb{hE@mpKtk5 zllUXM>g5TsvURgCdvz)c^j$fSI&?Nr(jY2;9gsjCUmj05*T4Vtb@k;R-xFG2`9~Q%{yc}M=JuiNT-8`R?bL8B zufAdy%=-GlTV_d}@kVzeybvQvBIdyLK`fewlNgf}VNLNwuA%-7FJHYeG;zid9{&86 z1a~L=*k$?{gE19bVbasKd01k+!4_F_n&f+Ia76{(XR}Z7pE68Im6udjv2#aFa|h76 zPpA!VqYaD)zT?5t5ug&QU3W&KK+QxT-V$p9og!fcU`sd8{F_{dRRqmr2M zzAD;ggodFFEXSa*f>7-I9NEDD0+j9VVzmql9elNvXNlEpWqL`=b}GIwBgH@FX2NrO zh;IP<-3jDS+h8bU=pyI{OZN0E>J9736mhH;)#Rl+XKIvZ#SG!`ZX!V|p~3rDOCUr4 z%cJ_-ti~Bw-eUXZF5y7mZ~8&-7tl$(@{_la08eG&_F%>oMDx4!8v&nC3Sw?+=0x*n zR{+Wv*N=hlL7%kZ0}F6*@q&JAcm1gex(%iS(fptreA2&>aP3S$XY{{govDeCMC|vF z0!|ct)3oic{o#Hz;qM`ZC3xEgcgF_KMf2Shp3JNEB5)T0Sxu(J5iwtmr^)R`*H%ui z!tpDcj@I52Kztz;m|C6dr1_zgTBtz*^ocge8LiN&*^V`RI?K!(_-0q=wQEad>^_~L z2>pm79fw&)T6H#ulI}}?ANX#prn6cD#!n9mUI7Uz+;=i+2#t4lSBpJGeHL8DILAVE zHNc=u<+X2c1(A^@w^02s8#M0_rR^H~8Ye1$AAR;I_~InpRasuXX=<~kJYa@Jwm}2L z=r?DTiRL`ve;gnz@}|HpQV*V!Q+S|{>666wck05oyDr2saZkM3V_P6USqHivq4oOz zat+|FYx59sez={K^oZBCyNdfLtbR6$h>**(B3fR!-zvJiKwky zmggtS!;pJuWel{RH?@rxMc#Q1j|8+Ut!zXI`{PPPh;_eRaP^%7I4ruF_h)tuGh%S& zj_jPp)E;6yIXvhd%>6*0l2|m_0=F7}G^15o=P3@pR5n5&$?K}<{UozKM=_4~HfKXN zCnc6^RO+oqKPXG5So^jAI1+B%(INA<57`=%ZJd>Mo($5K&CN{d5%CTCzB*_3v4rjk zB)1-<9jr6G{yLjUW-R!m9unAg_#RJG)ytYWcLD>QDk8(s?ZY9Q$Gwjz&0d>!7a!?9 z$iUB3qxbF-wqmoEeTZuhaIP$9=vBHW`=V1j@g*?>QBS9$YO+{GnfN!p6iFrrx}Pz_ z^^6aF%(go<=Rg|bL}&9PQ2w$xNfoH`kjiw=uF<_sx3dwf2fjP1sB z`@;%?G|!|#7|Lz?yJ-(AoXE%S!{U42&E|(M4X~qCKVxwY;Bb$?6tVouri{90?Q0i` zjok)|C)){aRB(V409wJ~b@f@4jQ93B1SNK+LULw z!s0cOx(m*6Y2izrQO(g;c`xP~IiroDeXdn~MAts5m%IgwpJpKRr?hj8>AYPYSK!wI zL1D|g^T3||VcT$<>51{-1T6lw#SYVnf3wCHOZN7MSKNJtEFZ&jP4T}Iaj1-E}H8_1hCVTWtQ03-L5OSR)9%Ck2a7JYUDMo#qyc6CnoLaDAwq zL|b5_XPkd2EI*m%R&t~Wi^p-U*c#utR0iGkI10=Q%v^dM$eQ9D-|sO-dq@;+^B5LS ztb7NP5Ed^w zmYq8^I(dK1LK*Y&+Zh)FI5gha$M`mz%kSEUFD*P_@xdSN zj@6x@N{;1ZbWcQ#X6uQM3UAVP|GtF?~PM zDM2R1lGu&5S5LYL79SU15H-GzPJvSH4jE?pthPHPbfD_E5qVxYjny6~M*)kk+O7ht zG2*%Jraj}Z#ueNJmxWn_?r_s(p7NuWz17c$#rL4KRmgf)&VERKMpxfJavqzSFT+N? zSwz%i6wMvkbpea-BknAH{+T6+f~mfgTrp&3FvbAL$V6!dkFvd%+?5^xiyy@1wjyp^ z{}z&=i9eX}1;c%tAz(Hi*tFc%-e+C@AqEz2sblaC&nJt6pi=vUhg!Yb{uKC43gDyX zxw)^*ZaE_jiMM7%p5-XdjNtH*>3vQ`MlIcb+8Z18QaN=Yotea;PDF-cWdGxXa$@O_?re^4UsW~mV zrMYuh{NN5RBJzQgR@PM=0_M{8Crw@?Mc%SZQ-g$Y#0>U0Em-`ERb(FQc6*)h4DJ+^ z+}khhiy$b=#z~Yl(5;=(O&57sd@hN_t`2c`Y8}THbSsqSQY8lY)Gu{%zA41l?)QwE zcEaL6>Q!r_N+B=DTAVM0VT1BU@w{bNXp{A@YYe*d*6?od$%pDENRGk=0x!cfK=)cB zgZ*0pr@*HE#f;HbUAj@Q1FU?=VMiW$DEyRplN(3oS7YNxR1PFgDJlTtli`^GyW=FIgT$4uz08M z-!Tl}WMdwrD&kC)RuqgB(L_crA>~FR59{8ZX|93A=K!h{-`p4P51ErYl<4s4>U8G8 zKbo9Zod_DbTVEM=V?EBkS^Idh!tPO0%(rEJiATJ`j6q=d#E>tokX&M~{h*^hR!>Vdo2Fo=io=stgDhn^29sjuM~h4SR~&rRcd$wx`|n{~bNb%<`zL9q_Ii(dVJg(`^_Q}U*?xihZG;1Ej1*R~Cv8~QZQAFu3x ze~RluQF>0d(j!2-OhpSkW~Wr~_^A5jwO}i({ypmDp{DNh`eXDXB}(@9(cU40qRAlJ z>SD`1+e2j|H|+bDaFKzABhl1Kf^&?Rd$#iTV<9`OELsaK5?J1>v2#hV@*fNiYg&w9 z!%^3x8aFn3PgG2X^oZWmF%{~>KVaML^@YW600fO*VIk@SZ2;7K>Y9%WboQMHlW0r) z^i1rQ=#ejB@f3A)Lk9zR<4@~S;_+R-v!*9uub|L6ztd#0W_R#+CxXR`j4=+s*pU(# zS-sW7*t2gkXQ+3F-|9oAqOs(C(UW|8SiH?=8Ls|U6jlQwQ6E#bKI5elX0=P#b@j79 z?3d3f$diP{FDZcawTN8hgkuujU$8HdD=ltwl+%)3vOfmr9vh>+hQ+UUVce+;+KGf_ z`-++QgJu=<$+}b`8SK?v)_3Zj(F?%h{VM{?2nQ73RPR_sA-+?7Zd0n|t5)<@u*Ma{ zq{V6<3yWX3p(X^=Zc$l(RcNgl?Ola;VIz<2HF;^|!hT*RSAPVHpBoWc?7&*7d5#z{ z<(79Qd)(!$Q>gdq0b?K)fpSH24=f&D=gJkEg=i(}g-1$<7vj^=GBf7c(o37km(>=6 zh+rdF{GFuzZ>vlJ1&Tm2;kLGk0$T;nYb7MYCxpllgx*R;_ctU;aLi@9u_&wr7)z@PsU3R=B-L&`d zVdWd@B9YpKZSqG+yAZ$mwr#`IV-w6JMmEW@sfmygy# zVrAwbq0fSQtfEl>aMP5>hLw+Sa*sJRI`Y-k2Lxjbw5R;Qap1c#`|)^7AT0_wp*#W> zk2LE~=*pvM=4^m96Pi`u>B`cm0?(7`v%@0r&836j2A?Bu8&pm@sZ>F}72VSPwC6{% z%GP1h63}n$wU$Gk$wB}tAGL)pYIBoZiRMa_&|J-~#y=yQ&0%k;+@){6lQcK37#5Gl zv@aI(m41!Vde&8-^mD6vIy;l>7bo5bqPs*`;x8+2;J-zeVTX~%V!EGZKlP8;an^H? z$5%a1h-nTi;cQ7#fW@PGZedWmY!yu%dfaxbSLv@43DC3S1GUz2dd2^ zI`^dD%6l1=h6hX7vweSJS9IW}u~i4aCE!|q3X8`sEXaDT2AcV9a5Sk0u^8Q@j1WtI zHbR19o5I2&ua*UiC-4ThTas!{RnBc0P zJHA*XHHLi^5ElNVj*c6%LjkOa&1~fO^a(7UJTZx|^o{!}K2xdBtu}LtHLdw7%`?_B zx62OZaNOxeSUhEnZs%p=`xx$#&r~KEcfRtx!#X5h7BzaVBgC2)S$bprQWGbI@68(@ ze1Z#>ZpNw#;t3CjYsTcg63&ElYWVG@z{+O?hSuhUf3d);v3s_%-8g^x=^-j+utB8Y z6|ohg@|7$so_VB5Q@Ic{O_=29SR2 z9DyUMAcMt&$yj;~R6DXkFGfm9LFn+ zKMv=#vr+9D;-226!x?|S#RkLgv2<@J)RIi@&4d@v6FtH6^4nm~fK2 zWLy|2;6?uu{zk^DJqfL|&o}(HJL?Y8LoS;nKtjDtY6)}pNwwDe^R*_pUXD&8_Zr?C z{Bw6>eVZqaw=+3`wBI(=I`R>b_{7OLnwnxhylsk#yB@4QB7zM2jrsa-^Tn-OEboU3 z2}{y08?{Q!Z4B^>2e32U@IS@S`yBNeYEsLVG2lwt;O<;ef^@J$*7Z0gC|{WzH7CHz z7Y_@jb}Tn2<$2&!z-p3(?&&gQnDSIfHriJ>q`4;(=>~q{^KI`iy9J^JuRpU+!H2k5#xx9hw%a`rSTx4z*&$X=0KJQsU;fw=37j}Kl`dvCzO zNsMh&bwnuKb>7SM99AElwWK>|BkFW*UZd6E)7{7@O-p(Wk3F>e!86BMQ)%h2c-_P^ zI#=3$$Hd(y76blwAl#qxPWJN)M5%kE@w59{Z{%}~sIrDz?mdK8uu-3f*K=7Kn|3~- z`waK^^=`nd_(sUq^&8xG;%3#@z__sJ6q z<~H?B&CgeQ-p@j;B_Cx3k(po7vZV%b_r=}dH^-K(Ns-ua;Avfe$N5DujneW`Oe8*j z3nkUaoUCk$GOT>ZtqCrj`RA93Q(RT#9bR*$--bAshmBZrQx}1akwv#h6GSkWXsyyNdsF0?%$6p#H ziq~2V(!2_W#rqIj;?PEx);)KBQ+m9UI?H6~s*M)#i1)rOmAL}H=Z$=YPe@2nKX*&S zWV@QRaKA|MJYo4mwK-R9#wu?c$GBTJ;v=8B-J$YX}RdxGMZWRhD-Wzd> z*f`Wk1K;qk0^~Z%mSC>Y@r@gw;Q|qFL_kEr;>1 zkgN*=J*Cfytkb9*UkcGNvQ@*G#(8e6uP7xSLB0g8%#kGh4G)e(&`g2Vqa!`p*aJ3J z``R$L8~N~91o;_M9kSJDw4L}Ca<{lpxH4vK4ia^fAk2HcGzbkM%uYrL}Ze?j&r+7dvE)w|C0L~qa>s;doK7)cFqdnfBFI2Fi zC(dHK@vQvPOKEt6^KO~<(7?DIEI!r6=bN8ddV1`g&Bs|rx>o+-k!&XegKwj(i_%-} z9|^(Y(^pheRi^^qcbi|uGiycRMN?C(>V{2XMs+)PI&r-rPv))B180 z$eWf^DJ_I$erNytgM*G@pRi?E`5DDzXi`$K9IcJ#QRU=A;tEM8dBmjD4q~PD&LW34AHxA)SbTQ1ir0+&@pNfMdz*{b$t4zAP0#|1s*JS4l_ zcchCk>1hk=BkT&d*kJKR?|d41+VVAd-=dy2>2Y)wSDxc-J*XtG(u3ry(ARU4Q=l5WFR=aXLqn{b8#!6pU_Kw1~+T0;5zDX4M zkzPU!%^7AzC)>pZHNB0msFU{-L|5ER>|LpM_N6<;aqWH8`eL1V%72nLFL{me-Y>W9xoAvhTO8%(v5se+iP5g z%~>6ssqdcE6rCA}H2ONq^o+$XuI?hpUDm*Rwfr#o ztG%DsnW$}S1KACIUVXK?ySk;+K5OH2+l30jSrOqpVAQ&#*J?L)46R4%2ETR5WaxFK z`V$hDJxcp`|V>U&;;`!6Nk#4 zjfKd@%M^XtH~f^dD7(pWuzEwWr#yb^gLUOZ(1kDapW6V`F%N-#f^e1 zi)z?b|hm$jLwrzJc(rF{jZnlZsb>oc>uW8+n63b#gQ+Znv2BKiBLBt&jRO4 zI|bYZRDEycuSVi`oSJCa+N194kNHQx+F3-&C3DtC0wRNYWycHG99n{|<*cp9&?ijNGhmd@Oj(%DI!kAb0qZ6{Y!gY4%ZndpoTBwJ>M=0N+RKgDM3_ zTp2rDlkc87KUgy(=*t(mxI2100E^$ciVf>RZB{a{50p8cMkSV=@oj$ZZL=o-TJ|cN z+UiF9{yC)BO5P0Zsfu0S3eG&Bg}ESc0iKYk>TdKcrg(pj8}{Y%%);KmuGSM4-_Ly1 zkIbv@QawrnFJm-ZLMsQW(A}(I_4!hFqIMTk^PWu{eq~qEUTJTx1h9CnRs=&I`OF^^ zRTvh3knhM_uW2hD@f1^R$c9<(=+KaDVXN`d@@oZ4({a~5So|UN8?)ZcG#oL}K|jiZ z*|Om!7rlpE&GQM<_TKU)ajCHQqj<+z*Z8!%rUfc&uS(F>-}5(gH2h;=#Xp$K$~$?V$5Wi>7J?cbeY-(#Et=SPB=b1adla9 zc(&%!x=x;uJQW<+Ygg!oKnaWIFAC&Wsy2ro^96#VSsS16XMpj{h|uFfS}_l|eELgZ z@nSJC!pG$`lImpgy!7y3&vHDo=bvbimFy2>#|UUOnqcvosBj;7j5@nTj=(Prjyl$2 zrbz3(o5~l<#=i`A=V~Tg*Uw*!MzL9#mZc~sQq6>t3S>%zCPFZXYXJZJk*cg*tde(y2uhp z-9-&uspc^NA{z?+S?iHor>*d~?+UL>4(=pi5Ev4HMUuod+P{?MV=a}Mp zX87*x>i>fZ6nco%1sLg>ZSYDeQFl}WfzO<)FaVX1_bW~>UnTA{W@v$ut9ddsq)eu_ zmflRbeoEAknjQrLurVYY+6>-x6&0_Et@UWx$L+h$4Z6>L(}YxM5p-;q%%!8-KcJmV zd^GglbB@@@#|v)5`}zpf81zkdBb+FAvA(6uG>sB1Uqq=El2 zVXl9U_m$ zKe*|YvSt`34+IowaQ9brEj$A8q{If`M%@)+9Xj!k?3O5e8-DWjY^g8*_WdWbwvnjl zjtVOoZ_ZyPLVJbP`M0j7(0kXf-#SAp>Y;zYKc+bYKnr?$t_<`DeHwrh^w@Jp=yx%J z9^id_w7w;P9pD1}7y5nRfPmNH2tUsHhJ%A1K!S)sjPtJ}zprHnTpVtE#~Fpn$+mWp!dlO&n%p}%A6kTj zIw1|Q+_(J1K;_*znNu^W*IiwNH=?47X1FX9>Vi+op`conry-5NgR^|ERnBP6U=}8% zGxN(XRO$wvo1l2RkTfD=xBsEfuHeBWi5>M;RMPeTo{hu*f(>|q%Ujpvphp?inm1F8sB25q+C}h-bQTek zm>3kqmq+wBu$&W>AXZ1Ixzkzo6nDF4?3!CoZPIEwRoEGoyM*w1J89Q=qw{|Fs%w3` zBS7k8c3@iO$%O?(N0qjzMGuyUcZA=5I;#o6_H3tnV5d2a-<4g0WkdT}mp?F!h z%PqDSBcyHdnf+rW;qgiTH%^%RH%-Z>4p*Qc4`P3jxS%eNNw&^4Q{M5Scae5pycC%J zE6Mcn@d1}#^eQY$TwJHGxPSBsS~)TDk^qpZY>CH%&yLXW@@I<9&frD!&9$e|wge}K z&99fxdd~1-)bOxjlt0ha{ygdMoc(#yw;2Cf%){gUd1HsaasPSJ;U%5R;cj+9*fQ9YL(@owQSV_e{uY8V|GH+q3>7y*>PuxhKh;l59 zxz-LM1Qsdb?uNs%%IvQqv4!SKPonqoraZ)mI*lw;9J^*k=scRaD!4{xe8bSFqVSIq zJ{w@iU|Vof(ZC2U*I@=w3_Z`t2fIH#`9|4RF`#|$NzjJ3|5I9wQ1QKF)JwFS^w6U# zKJ~@d1%N*AR&_Jl^UFB_7fjvWw)~247Xu0wXyCVK2gxa9_ z5}$wzfc7N{0dHPFE+8(zf>;C%>T|%6h%2zZ@a6fAojTk;vT|w=@MIT_TM#_2Qag@@9ysE z6nhSX#vGlQ@(k6NjuOPREOR83Sg5JCeA*Thnpa2Jnd!y)QPi!@JsF$vNOz#NIoN^; zl((5zeAPx5cnNPx;J+rVF`KZKO?91tY9V|;WO5aKRbkJY)L%PrJMQ8wM8+FOCDnog`h8{YZ!* zEoRiKTc7((je3F%KQMEJZC&L+v4wAIL=`#X}BB0C<4(N^>`1Bb9)wBK|2kI_% zar}u_7$x2GqigldM%*0DE-(7=PO!Oqh&!~xu&Ya^>kC|wnS%Mk$Ddk zv~wp438@9FEkC`DqMkf9mhQZxg7Uzb{PGM&D14Paj*p)|9WL4!a~pg>~FRv`&VUx z9BEO|p7S1Qo}Mg^TxY8+nTR}`nmkb|w>FNXjvAAv7Q`l+Td7V6=lZ>tlV5eBOF>p$ z*}~xj^cpVFgZ~R3jR(yiUBM)M_;o%aZ!R|8B#z_pGDvq%>#Kz4pOswDuW5TFm0c*~ zAx7~05Hy=jyUgL6s03m$^5HJ?>#`Q75T66iG+qzH=3$K&{CFRl^F6#}q#KVb4l^T< z2MO2IaPRM*@6WtL_ahuyMB9OGqVC{*Ka;e&wgL;QqCHy5LUkIhn4A?x%3=Bs>t%qJD^+_@I;?d>CTnEAr<`m0n=1pGUSp(Z%I2-(S<|sOFbTSMr zNB272Ez!7s;z($P5)TqeC?pxq>F%4R?PMN*|A+g=BFVB=|3;yUmb4j_WEx`}3i8{l zL(&&I%t3H@+mwAbBH=bQ2W4pwW9E9Ak?RZJ4K`~3eIpy!;;{!QX359!^};_!LXgl- zt&k{;v_l(@q9#AF9$UQ)W_iDt!){kY%Y^M?K2ehOz(_`3|X2;LBYa8SJ$6AIEspFCO4}|N6bm!8J~a z5bKsZ<13+5=+ky6sHV5b085n8srPI3UL@$zX{RoG3L9*jLk8|3cV{#L;v(rx~lJHzeXv0{v)3}B+y@#72JJkKtCF5jP z(sSWG>m0Q0v8aYJZ4_j1-=xYJKlR5vCE@~&1baCB9n*-T{b$zE=jA;$c65{wp z_ueQTY7Y!atq~7JFN|8`aNq$q; znbA0&NvTM$_p_>;4y|}=oT7p3@WzGwxvM@XXpWn!#ky>~+(2mDGF$lLhuuaVPgz%T ziq$!mRUnwd$dPh8>9t5<$kR2bjV$SsqH( z)|I@KTxu2^qnX~<66GPQ<*UnHI{mv8tzd9DXDsF@7(djpy6JHtL<0Qp&jywX-)smf zHfZ^Iqj4LeL@f+Gj;|Cm=zz0C_~{ZHlWuv&=ZK=bwu&RxX+y~S3r+Yk?~P@CB-GrR zlU}QPTT~Ym`dB1|oTMbHE#;Uh)+d*|z@6n==0HIUngFG$Ku%U`0% za&cQ<($aWH4t8qGvpz8S=A+da&+-K>qn9JZeL&*uKA9W8fW5Y!qKc}D#R(lnbyaC; zZ6`~ct?ba{8*JPdb{UiP38R||S8fb51Q&d=dUh5GHIc~5DaWVU#~V3tH+zJ4%U>X3 zKg(-#KJDLqzkj?4WUx;9mEK$b zyM}Nl>$;&%ebq2Kz%&D}!SRAN&e82M8s~^}CD=f4;_$zOT1EPzcVJn;DLs*6vM;;S zL&{b)aNd!q2c`KJu1lAZ`O>Tn8VB`ArtkXO&5OA}c%_9TvwFInXOYnG za6LYVF$}C|nxL}k+5dE5(MZ9EODk`wZ{^Ur7yZ>_Y>~Uit35!f>SdhkR^n`kC2W=J zIN|8-w3SwW4NKivb*SZXZ*7O<>Qg_Fcz+Gfy9`LEM9)pBwU3bfOLFqm)jNWbu0DR% z=2@0z3LivOvIeHX^ICcliso}u6P%f1c@YliYz4ZC8hmIde@ZMv#0Q0@$vB+3O_y;4 mhbC62BVdsa=_>GjSO!>EHv*`;i1xs_+M^BKtU+lJm;VCod`j;C literal 0 HcmV?d00001 diff --git a/cc-eventlog/src/lib.rs b/cc-eventlog/src/lib.rs index 2590f6f33..6c18c75cc 100644 --- a/cc-eventlog/src/lib.rs +++ b/cc-eventlog/src/lib.rs @@ -11,6 +11,7 @@ use tcg::{TcgDigest, TcgEfiSpecIdEvent}; mod codecs; mod tcg; +pub mod tpm; /// The path to the userspace TDX event log file. pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/tdx_mr3/tdx_events.log"; diff --git a/cc-eventlog/src/tpm.rs b/cc-eventlog/src/tpm.rs new file mode 100644 index 000000000..2d70bd01a --- /dev/null +++ b/cc-eventlog/src/tpm.rs @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM Event Log parsing (binary_bios_measurements format) + +use crate::codecs::VecOf; +use crate::tcg::{TcgDigest, TcgEfiSpecIdEvent}; +use anyhow::{Context, Result}; +use scale::Decode; +use serde::{Deserialize, Serialize}; + +/// Simplified TPM event for PCR replay +#[derive(Clone, Debug, Serialize, Deserialize, scale::Encode, scale::Decode)] +pub struct TpmEvent { + /// PCR index this event was extended to + pub pcr_index: u32, + /// SHA-256 digest of the event data + #[serde(with = "serde_human_bytes")] + pub digest: Vec, +} + +/// TCG_PCR_EVENT2 format +/// +/// See TCG PC Client Platform Firmware Profile spec section 9.2.2 +#[derive(Clone, Decode)] +struct TpmRawEvent { + pcr_index: u32, + event_type: u32, + digests: VecOf, + event: VecOf, +} + +impl core::fmt::Debug for TpmRawEvent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TpmRawEvent") + .field("pcr_index", &self.pcr_index) + .field("event_type", &self.event_type) + .field( + "digests", + &self + .digests + .iter() + .map(|d| hex::encode(&d.hash)) + .collect::>(), + ) + .field("event", &hex::encode(&self.event)) + .finish() + } +} + +impl TpmRawEvent { + fn sha256_digest(&self) -> Option> { + self.digests + .iter() + .find(|d| d.algo_id == crate::tcg::TPM_ALG_SHA256) + .map(|d| d.hash.clone()) + } + + fn is_extended_to_pcr(&self) -> bool { + self.event_type != crate::tcg::EV_NO_ACTION + } + + fn to_simple_event(&self) -> Option { + if !self.is_extended_to_pcr() { + return None; + } + self.sha256_digest().map(|digest| TpmEvent { + pcr_index: self.pcr_index, + digest, + }) + } +} + +#[derive(Clone, Debug)] +pub struct TpmEventLog { + pub spec_id_header_event: TcgEfiSpecIdEvent, + pub events: Vec, +} + +impl TpmEventLog { + /// Decode from binary_bios_measurements format + /// + /// First event is TCG_PCClientPCREvent (legacy format with SHA-1). + /// Subsequent events are TCG_PCR_EVENT2 (crypto-agile format). + pub fn decode(input: &mut &[u8]) -> Result { + let (_spec_id_header, spec_id_header_event) = + parse_spec_id_event(input).context("Failed to parse spec id event")?; + + let mut events = vec![]; + loop { + let head_buffer = &mut &input[..]; + let pcr_index = match u32::decode(head_buffer) { + Ok(idx) => idx, + Err(_) => break, + }; + + if pcr_index == 0xFFFFFFFF { + break; + } + + let raw_event = TpmRawEvent::decode(input).context("Failed to decode TPM event")?; + + if let Some(event) = raw_event.to_simple_event() { + events.push(event); + } + } + + Ok(TpmEventLog { + spec_id_header_event, + events, + }) + } + + /// Read and decode TPM Event Log from kernel sysfs + pub fn from_kernel_file() -> Result { + const TPM_BINARY_BIOS_MEASUREMENTS: &str = + "/sys/kernel/security/tpm0/binary_bios_measurements"; + + let data = fs_err::read(TPM_BINARY_BIOS_MEASUREMENTS) + .context("Failed to read TPM binary_bios_measurements")?; + + Self::decode(&mut data.as_slice()) + } + + /// Filter events by PCR index + pub fn filter_by_pcr(&self, pcr_index: u32) -> Vec { + self.events + .iter() + .filter(|e| e.pcr_index == pcr_index) + .cloned() + .collect() + } + + /// Get all PCR 2 events (boot loader and OS measurements) + pub fn pcr2_events(&self) -> Vec { + self.filter_by_pcr(2) + } +} + +/// Parse Spec ID Event in legacy TCG_PCClientPCREvent format +fn parse_spec_id_event(input: &mut I) -> Result<(TpmRawEvent, TcgEfiSpecIdEvent)> { + #[derive(Decode)] + struct SpecIdHeader { + pcr_index: u32, + event_type: u32, + digest_sha1: [u8; 20], + event: VecOf, + } + + let header = SpecIdHeader::decode(input).context("failed to decode spec id header")?; + + let spec_id_event = TcgEfiSpecIdEvent::decode(&mut header.event.as_slice()) + .context("failed to decode TcgEfiSpecIdEvent")?; + + let digests = vec![TcgDigest { + algo_id: crate::tcg::TPM_ALG_SHA1, + hash: header.digest_sha1.to_vec(), + }]; + + let raw_event = TpmRawEvent { + pcr_index: header.pcr_index, + event_type: header.event_type, + digests: (digests.len() as u32, digests).into(), + event: header.event, + }; + + Ok((raw_event, spec_id_event)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decode_empty() { + let result = TpmEventLog::decode(&mut &[][..]); + assert!(result.is_err()); + } + + #[test] + fn test_decode_gcp_tpm_eventlog() { + let data = include_bytes!("../samples/tpm_eventlog.bin"); + let event_log = TpmEventLog::decode(&mut data.as_slice()).unwrap(); + + assert!(!event_log.events.is_empty()); + assert_eq!(event_log.spec_id_header_event.platform_class, 0); + + let pcr2_events = event_log.pcr2_events(); + assert_eq!(pcr2_events.len(), 4); + + assert_eq!( + hex::encode(&pcr2_events[0].digest), + "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119" + ); + + assert_eq!( + hex::encode(&pcr2_events[1].digest), + "00b8a357e652623798d1bbd16c375ec90fbed802b4269affa3e78e6eb19386cf" + ); + + // Event 28: UKI Authenticode hash + assert_eq!( + hex::encode(&pcr2_events[2].digest), + "9ab14a46f858662a89adc102d2a57a13f52f75c1769d65a4c34edbbfc8855f0f" + ); + + // Event 41: Linux kernel Authenticode hash + assert_eq!( + hex::encode(&pcr2_events[3].digest), + "ade943a0a7a3189a3201ba17d7df778eb380cbd33ce5e361176e974ccf7cdedb" + ); + } + + #[test] + fn test_filter_by_pcr() { + let data = include_bytes!("../samples/tpm_eventlog.bin"); + let event_log = TpmEventLog::decode(&mut data.as_slice()).unwrap(); + + let pcr0_events = event_log.filter_by_pcr(0); + assert!(!pcr0_events.is_empty()); + + let pcr2_events = event_log.filter_by_pcr(2); + assert_eq!(pcr2_events.len(), 4); + + let pcr99_events = event_log.filter_by_pcr(99); + assert_eq!(pcr99_events.len(), 0); + } + + #[test] + fn test_pcr2_uki_hash_extraction() { + let data = include_bytes!("../samples/tpm_eventlog.bin"); + let event_log = TpmEventLog::decode(&mut data.as_slice()).unwrap(); + + let pcr2_events = event_log.pcr2_events(); + assert!(pcr2_events.len() >= 3); + + let uki_hash = &pcr2_events[2].digest; + let expected_uki_hash = + hex::decode("9ab14a46f858662a89adc102d2a57a13f52f75c1769d65a4c34edbbfc8855f0f") + .unwrap(); + + assert_eq!(uki_hash, &expected_uki_hash); + } +} From c9fad02bd084171bb186051dfa95fc56df227f52 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:06:18 +0800 Subject: [PATCH 017/264] cvm: Detect data disk by label --- basefiles/dstack-prepare.sh | 221 ++++++++++++++++++++++++++++++++++-- 1 file changed, 214 insertions(+), 7 deletions(-) diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index fa00f38ba..4442832f3 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -6,6 +6,76 @@ set -e +log() { + printf '%s\n' "$*" >&2 +} + +CMDLINE=$(cat /proc/cmdline) + +get_cmdline_value() { + local key="$1" + for param in $CMDLINE; do + case "$param" in + "$key="*) + echo "${param#*=}" + return 0 + ;; + esac + done + return 1 +} + +read_uevent_property() { + local file="$1" + local key="$2" + while IFS='=' read -r name value; do + if [ "$name" = "$key" ]; then + printf "%s" "$value" + return 0 + fi + done <"$file" + return 1 +} + +find_block_by_property() { + local key="$1" + local value="$2" + for entry in /sys/class/block/*; do + [ -e "$entry/uevent" ] || continue + local current + current=$(read_uevent_property "$entry/uevent" "$key" || true) + if [ "$current" = "$value" ]; then + printf "/dev/%s" "$(basename "$entry")" + return 0 + fi + done + return 1 +} + +resolve_block_spec() { + local spec="$1" + local device="" + case "$spec" in + "") + return 1 + ;; + PARTLABEL=*) + device=$(find_block_by_property PARTNAME "${spec#PARTLABEL=}" || true) + ;; + PARTUUID=*) + device=$(find_block_by_property PARTUUID "${spec#PARTUUID=}" || true) + ;; + /dev/*) + device="$spec" + ;; + esac + if [ -n "$device" ] && [ -b "$device" ]; then + printf "%s" "$device" + return 0 + fi + return 1 +} + WORK_DIR="/var/volatile/dstack" DATA_MNT="$WORK_DIR/persistent" @@ -25,17 +95,154 @@ mount_overlay /bin $OVERLAY_TMP mount_overlay /home $OVERLAY_TMP # Make sure the system time is synchronized -echo "Syncing system time..." +log "Syncing system time..." # Let the chronyd correct the system time immediately chronyc makestep modprobe tdx-guest # Setup dstack system -echo "Preparing dstack system..." -dstack-util setup --work-dir $WORK_DIR --device /dev/vdb --mount-point $DATA_MNT +log "Preparing dstack system..." + +has_partition_table() { + local disk="$1" + local disk_name=$(basename "$disk") + # Check sysfs for any child partitions + for entry in /sys/class/block/${disk_name}/${disk_name}*; do + [ -e "$entry/partition" ] || continue + return 0 + done + return 1 +} + +has_luks_header() { + cryptsetup isLuks "$1" 2>/dev/null && return 0 + return 1 +} + +create_data_partition() { + local disk="$1" + log "Creating GPT partition table on ${disk}..." + if ! command -v sgdisk >/dev/null 2>&1; then + log "Error: sgdisk not available, cannot create partition table" + return 1 + fi + # Create GPT with single partition filling entire disk + sgdisk -Z "$disk" >/dev/null || true # Zap any existing data + sgdisk -n 1:1MiB:0 -c 1:dstack-data -t 1:8300 "$disk" >/dev/null || return 1 + # Trigger kernel to re-read partition table + blockdev --rereadpt "$disk" >/dev/null || true + udevadm settle >/dev/null || sleep 1 + part_device=$( + lsblk -nr -o PATH "$disk" 2>/dev/null | sed -n '2p' + ) + if [ -n "$part_device" ] && [ -b "$part_device" ]; then + log "Created partition: $part_device" + echo "$part_device" + return 0 + fi + log "Failed to create partition" + return 1 +} + +choose_data_device() { + local override="$1" + local dev="" + + # 1. Check explicit override first + if [ -n "$override" ]; then + dev=$(resolve_block_spec "$override" || true) + if [ -n "$dev" ]; then + echo "$dev" + return 0 + fi + log "Warning: dstack data device override '$override' not found" + fi + + # 2. Try to find partition with PARTLABEL=dstack-data + local data_disk + data_disk=$(resolve_block_spec "PARTLABEL=dstack-data" || true) + if [ -n "$data_disk" ]; then + echo "$data_disk" + return 0 + fi + + # 3. Fallback to /dev/vdb for backward compatibility + if [ ! -b /dev/vdb ]; then + log "Error: No dstack-data partition found and /dev/vdb does not exist" + return 1 + fi + + # 3.1. Check if /dev/vdb has LUKS header (0.5.x upgrade path) + if has_luks_header /dev/vdb; then + log "Detected LUKS on /dev/vdb (dstack 0.5.x upgrade), using whole disk" + echo /dev/vdb + return 0 + fi + + # 3.2. Check if /dev/vdb has partition table + if has_partition_table /dev/vdb; then + log "Error: /dev/vdb has partition table but no 'dstack-data' partition found" + log "Please check partition labels or specify dstack.data_device kernel parameter" + return 1 + fi + + # 3.3. /dev/vdb is empty, create partition table + log "Empty disk detected at /dev/vdb, creating dstack-data partition..." + local new_partition + new_partition=$(create_data_partition /dev/vdb) + if [ -z "$new_partition" ]; then + log "Error: Failed to create partition on /dev/vdb" + return 1 + fi + echo "$new_partition" + return 0 +} + +DATA_DEVICE_OVERRIDE=$(get_cmdline_value "dstack.data_device" || true) +if [ -z "$DATA_DEVICE_OVERRIDE" ] && [ -n "$DSTACK_DATA_DEVICE" ]; then + DATA_DEVICE_OVERRIDE="$DSTACK_DATA_DEVICE" +fi +DATA_DEVICE=$(choose_data_device "$DATA_DEVICE_OVERRIDE" || true) +if [ ! -b "$DATA_DEVICE" ]; then + log "Persistent data disk $DATA_DEVICE not found" + exit 1 +fi +log "Using persistent data disk $DATA_DEVICE" + +# Auto-grow partition if disk was expanded +device_name=$(basename "$DATA_DEVICE") +if [ -f "/sys/class/block/${device_name}/partition" ]; then + log "Detected partition ${DATA_DEVICE}, checking if parent disk was expanded..." + + parent_disk=$(lsblk -no PKNAME "$DATA_DEVICE" 2>/dev/null | head -n1 || true) + if [ -n "$parent_disk" ]; then + parent_disk="/dev/${parent_disk}" + fi + + if [ -n "$parent_disk" ] && [ -b "$parent_disk" ]; then + log "Parent disk: ${parent_disk}" + if command -v sgdisk >/dev/null 2>&1; then + log "Refreshing GPT on ${parent_disk}..." + sgdisk -e "$parent_disk" 2>/dev/null || true + fi + if command -v parted >/dev/null 2>&1; then + part_num=$(cat "/sys/class/block/${device_name}/partition" 2>/dev/null || echo "") + if [ -n "$part_num" ]; then + log "Growing partition ${part_num} on ${parent_disk} via parted..." + parted --script "$parent_disk" resizepart "$part_num" 100% 2>/dev/null || log "Partition already at maximum size" + fi + else + log "Warning: parted not available; unable to auto-resize ${DATA_DEVICE}" + fi + # Trigger kernel to re-read partition table + blockdev --rereadpt "$parent_disk" 2>/dev/null || true + fi +fi + +dstack-util setup --work-dir $WORK_DIR --device "$DATA_DEVICE" --mount-point $DATA_MNT -echo "Mounting docker dirs to persistent storage" +log "Mounting docker dirs to persistent storage" # Mount docker dirs to DATA_MNT mkdir -p $DATA_MNT/var/lib/docker mount --rbind $DATA_MNT/var/lib/docker /var/lib/docker @@ -44,9 +251,9 @@ mount --rbind $WORK_DIR /dstack cd /dstack if [ $(jq 'has("init_script")' app-compose.json) == true ]; then - echo "Running init script" - dstack-util notify-host -e "boot.progress" -d "init-script" || true - source <(jq -r '.init_script' app-compose.json) + log "Running init script" + dstack-util notify-host -e "boot.progress" -d "init-script" || true + source <(jq -r '.init_script' app-compose.json) fi RUNNER=$(jq -r '.runner' app-compose.json) From 85d45393944da5e86b18b944a48393232a1a52c1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:07:51 +0800 Subject: [PATCH 018/264] cvm: Skip loading tdx_guest if already exists --- basefiles/dstack-prepare.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index 4442832f3..34b51b0c6 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -99,7 +99,9 @@ log "Syncing system time..." # Let the chronyd correct the system time immediately chronyc makestep -modprobe tdx-guest +if ! [[ -e /dev/tdx_guest ]]; then + modprobe tdx-guest +fi # Setup dstack system log "Preparing dstack system..." From 609c43c6cb47557fb3e38db72f2800e4529986de Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:08:39 +0800 Subject: [PATCH 019/264] cvm: Mount tsm configfs in boot script --- basefiles/dstack-prepare.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index 34b51b0c6..43f61713b 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -103,6 +103,21 @@ if ! [[ -e /dev/tdx_guest ]]; then modprobe tdx-guest fi +# Mount configfs for TSM (required for TDX quote generation) +if [[ ! -d /sys/kernel/config ]]; then + mkdir -p /sys/kernel/config +fi +if ! mountpoint -q /sys/kernel/config; then + log "Mounting configfs for TSM..." + mount -t configfs none /sys/kernel/config +fi + +# Create TSM report directory for TDX attestation +if [[ -e /dev/tdx_guest ]] && [[ ! -d /sys/kernel/config/tsm/report/com.intel.dcap ]]; then + log "Creating TSM report directory..." + mkdir -p /sys/kernel/config/tsm/report/com.intel.dcap +fi + # Setup dstack system log "Preparing dstack system..." From ce639ead5c7f9712905861e325269cf47b9dd249 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:16:42 +0800 Subject: [PATCH 020/264] cvm: Support for TPM key provider --- dstack-util/Cargo.toml | 6 +- dstack-util/src/main.rs | 49 ++++++--- dstack-util/src/system_setup.rs | 177 ++++++++++++++++++++++++++------ 3 files changed, 185 insertions(+), 47 deletions(-) diff --git a/dstack-util/Cargo.toml b/dstack-util/Cargo.toml index 2e7731417..f08c366c1 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -32,9 +32,11 @@ x25519-dalek.workspace = true dstack-kms-rpc.workspace = true ra-rpc = { workspace = true, features = ["client"] } -ra-tls.workspace = true +ra-tls = { workspace = true, features = ["quote"] } dstack-gateway-rpc.workspace = true tdx-attest.workspace = true +tpm-attest.workspace = true +tpm-qvl = { workspace = true, features = ["crl-download"] } host-api = { workspace = true, features = ["client"] } cmd_lib.workspace = true toml.workspace = true @@ -51,6 +53,8 @@ sodiumbox.workspace = true libc.workspace = true luks2.workspace = true scopeguard.workspace = true +tempfile.workspace = true +ez-hash.workspace = true [dev-dependencies] rand.workspace = true diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index ade229e68..fcaec5244 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use dstack_types::KeyProvider; +use dstack_types::{KeyProvider, KeyProviderKind}; use fs_err as fs; use getrandom::fill as getrandom; use host_api::HostApi; @@ -349,26 +349,49 @@ fn cmd_gen_app_keys(args: GenAppKeysArgs) -> Result<()> { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let disk_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let k256_key = SigningKey::random(&mut rand::thread_rng()); - let app_keys = make_app_keys(key, disk_key, k256_key, args.ca_level, None)?; + let key_provider = KeyProvider::None { + key: key.serialize_pem(), + }; + let app_keys = make_app_keys(&key, &disk_key, &k256_key, args.ca_level, key_provider)?; let app_keys = serde_json::to_string(&app_keys).context("Failed to serialize app keys")?; fs::write(&args.output, app_keys).context("Failed to write app keys")?; Ok(()) } -fn gen_app_keys_from_seed(seed: &[u8], mr: Option>) -> Result { +fn gen_app_keys_from_seed( + seed: &[u8], + provider: KeyProviderKind, + mr: Option>, +) -> Result { let key = derive_ecdsa_key_pair_from_bytes(seed, &["app-key".as_bytes()])?; let disk_key = derive_ecdsa_key_pair_from_bytes(seed, &["app-disk-key".as_bytes()])?; let k256_key = derive_ecdsa_key(seed, &["app-k256-key".as_bytes()], 32)?; let k256_key = SigningKey::from_bytes(&k256_key).context("Failed to parse k256 key")?; - make_app_keys(key, disk_key, k256_key, 1, mr) + let key_provider = match provider { + KeyProviderKind::None => KeyProvider::None { + key: key.serialize_pem(), + }, + KeyProviderKind::Local => KeyProvider::Local { + mr: mr.context("Missing MR for local key provider")?, + key: key.serialize_pem(), + }, + KeyProviderKind::Tpm => KeyProvider::Tpm { + key: key.serialize_pem(), + pubkey: key.public_key_der(), + }, + KeyProviderKind::Kms => { + anyhow::bail!("KMS keys must be fetched from the KMS server") + } + }; + make_app_keys(&key, &disk_key, &k256_key, 1, key_provider) } fn make_app_keys( - app_key: KeyPair, - disk_key: KeyPair, - k256_key: SigningKey, + app_key: &KeyPair, + disk_key: &KeyPair, + k256_key: &SigningKey, ca_level: u8, - mr: Option>, + key_provider: KeyProvider, ) -> Result { use ra_tls::cert::CertRequest; let pubkey = app_key.public_key_der(); @@ -394,15 +417,7 @@ fn make_app_keys( k256_signature: vec![], gateway_app_id: "".to_string(), ca_cert: cert.pem(), - key_provider: match mr { - Some(mr) => KeyProvider::Local { - mr, - key: app_key.serialize_pem(), - }, - None => KeyProvider::None { - key: app_key.serialize_pem(), - }, - }, + key_provider, }) } diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 933ea48e6..3c4b7050b 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -54,6 +54,7 @@ use ra_tls::{ }; use serde_human_bytes as hex_bytes; use serde_json::Value; +use tpm_attest::{self as tpm, TpmContext}; mod config_id_verifier; @@ -150,12 +151,6 @@ fn parse_dstack_options(shared: &HostShared) -> Result { Ok(options) } -impl InstanceInfo { - fn is_initialized(&self) -> bool { - !self.instance_id_seed.is_empty() - } -} - #[derive(Clone)] pub struct HostShareDir { base_dir: PathBuf, @@ -730,12 +725,61 @@ impl<'a> Stage0<'a> { .await .context("Failed to get sealing key")?; // write to fs - let app_keys = gen_app_keys_from_seed(&provision.sk, Some(provision.mr.to_vec())) - .context("Failed to generate app keys")?; + let app_keys = gen_app_keys_from_seed( + &provision.sk, + KeyProviderKind::Local, + Some(provision.mr.to_vec()), + ) + .context("Failed to generate app keys")?; Ok(app_keys) } - async fn request_app_keys(&self) -> Result { + fn generate_tpm_app_keys(&self, app_compose_hash: &[u8; 32]) -> Result { + let tpm = TpmContext::detect().context("failed to detect TPM context")?; + + // Get PCR policy for sealing (boot chain + app PCR) + let pcr_policy = tpm::dstack_pcr_policy(); + + // Extend app PCR with app-compose hash BEFORE unsealing + // This ensures the sealed data is bound to this specific app configuration + tpm.pcr_extend_sha256(tpm::APP_PCR, app_compose_hash)?; + + // Dump all PCR values (0-15) AFTER extending for debugging + let all_pcrs = + tpm::PcrSelection::sha256(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + info!("PCR values AFTER extending PCR {}:", tpm::APP_PCR); + tpm.dump_pcr_values(&all_pcrs); + + // Try to read sealed seed (bound to PCR values including app PCR) + if let Some(seed) = tpm + .unseal::<32>(tpm::SEALED_NV_INDEX, tpm::PRIMARY_KEY_HANDLE, &pcr_policy) + .context("failed to unseal from TPM")? + { + info!( + "unsealed root key seed from TPM (PCR policy: {})", + pcr_policy.to_arg() + ); + return gen_app_keys_from_seed(&seed, KeyProviderKind::Tpm, None) + .context("failed to generate TPM app keys"); + } + + // No sealed seed exists, generate new one + info!("no sealed seed found, generating new seed..."); + let seed: [u8; 32] = tpm.get_random().context("TPM RNG unavailable")?; + // Seal the new seed to TPM with PCR policy (including app PCR) + tpm.seal( + &seed, + tpm::SEALED_NV_INDEX, + tpm::PRIMARY_KEY_HANDLE, + &pcr_policy, + ) + .context("failed to seal seed to TPM")?; + + gen_app_keys_from_seed(&seed, KeyProviderKind::Tpm, None) + .context("failed to generate TPM app keys") + } + + async fn request_app_keys(&self, app_compose_hash: &[u8; 32]) -> Result { let key_provider = self.shared.app_compose.key_provider(); match key_provider { KeyProviderKind::Kms => self.request_app_keys_from_kms().await, @@ -743,7 +787,12 @@ impl<'a> Stage0<'a> { KeyProviderKind::None => { info!("No key provider is enabled, generating temporary app keys"); let seed: [u8; 32] = rand::thread_rng().gen(); - gen_app_keys_from_seed(&seed, None).context("Failed to generate app keys") + gen_app_keys_from_seed(&seed, KeyProviderKind::None, None) + .context("Failed to generate app keys") + } + KeyProviderKind::Tpm => { + info!("Generating app keys from TPM"); + self.generate_tpm_app_keys(app_compose_hash) } } } @@ -827,12 +876,66 @@ impl<'a> Stage0<'a> { Ok(()) } - async fn mount_data_disk( - &self, - initialized: bool, - disk_crypt_key: &str, - opts: &DstackOptions, - ) -> Result<()> { + fn is_disk_initialized(&self, opts: &DstackOptions) -> bool { + let device = &self.args.device; + + // For encrypted storage, just check if LUKS header exists + // The filesystem check happens after the LUKS device is opened + let has_luks = if opts.storage_encrypted { + let result = cmd!(cryptsetup isLuks $device).is_ok(); + if result { + info!("LUKS header detected on {}", device.display()); + } + result + } else { + false + }; + + // Check if filesystem exists + let has_fs = match opts.storage_fs { + FsType::Zfs => { + // Check if zpool exists by trying to import it in readonly mode + if cmd!(zpool import -N -o readonly=on dstack).is_ok() { + cmd!(zpool export dstack).ok(); + info!("ZFS pool 'dstack' detected"); + true + } else { + false + } + } + FsType::Ext4 if !opts.storage_encrypted => { + // For unencrypted ext4, check the device directly + if cmd!(blkid -s TYPE -o value $device) + .map(|out| out.trim() == "ext4") + .unwrap_or(false) + { + info!("ext4 filesystem detected on {}", device.display()); + true + } else { + false + } + } + FsType::Ext4 => { + // For encrypted ext4, we can only check after LUKS is opened + // So we rely on LUKS header presence as indicator + has_luks + } + }; + + // For encrypted ZFS, need both LUKS header AND zpool to exist + let initialized = if opts.storage_encrypted && opts.storage_fs == FsType::Zfs { + has_luks && has_fs + } else { + has_luks || has_fs + }; + + if !initialized { + info!("No existing filesystem detected on {}", device.display()); + } + initialized + } + + async fn mount_data_disk(&self, disk_crypt_key: &str, opts: &DstackOptions) -> Result<()> { let name = "dstack_data_disk"; let mount_point = &self.args.mount_point; @@ -845,7 +948,9 @@ impl<'a> Stage0<'a> { cmd!(mkdir -p $mount_point).context("Failed to create mount point")?; - if !initialized { + let disk_initialized = self.is_disk_initialized(opts); + + if !disk_initialized { self.vmm .notify_q("boot.progress", "initializing data disk") .await; @@ -997,6 +1102,20 @@ impl<'a> Stage0<'a> { echo -n $disk_crypt_key | cryptsetup luksOpen --type luks2 --header $in_mem_hdr -d- $root_hd $name; } .or(Err(anyhow!("Failed to open encrypted data disk")))?; + + // Wait for device mapper to create the device + let dm_path = format!("/dev/mapper/{name}"); + for i in 0..10 { + if std::path::Path::new(&dm_path).exists() { + info!("Device mapper {} is ready", dm_path); + break; + } + if i == 9 { + bail!("Timed out waiting for device mapper {}", dm_path); + } + info!("Waiting for device mapper {}...", dm_path); + std::thread::sleep(std::time::Duration::from_millis(500)); + } Ok(()) } @@ -1027,12 +1146,14 @@ impl<'a> Stage0<'a> { sha256(&id_path)[..20].to_vec() }; instance_info.instance_id = instance_id.clone(); - if !kms_enabled && instance_info.app_id != truncated_compose_hash { - bail!("App upgrade is not supported without KMS"); - } + let app_id = if kms_enabled { + instance_info.app_id.clone() + } else { + truncated_compose_hash.to_vec() + }; extend_rtmr3("system-preparing", &[])?; - extend_rtmr3("app-id", &instance_info.app_id)?; + extend_rtmr3("app-id", &app_id)?; extend_rtmr3("compose-hash", &compose_hash)?; extend_rtmr3("instance-id", &instance_id)?; extend_rtmr3("boot-mr-done", &[])?; @@ -1061,6 +1182,9 @@ impl<'a> Stage0<'a> { KeyProvider::Local { mr, .. } => { KeyProviderInfo::new("local-sgx".into(), hex::encode(mr)) } + KeyProvider::Tpm { pubkey, .. } => { + KeyProviderInfo::new("tpm".into(), hex::encode(pubkey)) + } KeyProvider::Kms { pubkey, .. } => { KeyProviderInfo::new("kms".into(), hex::encode(pubkey)) } @@ -1070,7 +1194,6 @@ impl<'a> Stage0<'a> { } async fn setup_fs(self) -> Result> { - let is_initialized = self.shared.instance_info.is_initialized(); let app_info = self .measure_app_info() .context("Failed to measure app info")?; @@ -1080,7 +1203,7 @@ impl<'a> Stage0<'a> { self.vmm .notify_q("boot.progress", "requesting app keys") .await; - let app_keys = self.request_app_keys().await?; + let app_keys = self.request_app_keys(&app_info.compose_hash).await?; if app_keys.disk_crypt_key.is_empty() { bail!("Failed to get valid key phrase from KMS"); } @@ -1100,12 +1223,8 @@ impl<'a> Stage0<'a> { opts.storage_encrypted, opts.storage_fs ); - self.mount_data_disk( - is_initialized, - &hex::encode(&app_keys.disk_crypt_key), - &opts, - ) - .await?; + self.mount_data_disk(&hex::encode(&app_keys.disk_crypt_key), &opts) + .await?; self.setup_swap(self.shared.app_compose.swap_size, &opts) .await?; self.vmm From f47bd9f50be76619d13492dd3785bcd782ffe834 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:32:24 +0800 Subject: [PATCH 021/264] kms: Use attestation v2 --- Cargo.toml | 2 + cert-client/src/lib.rs | 28 +- dstack-types/Cargo.toml | 1 + dstack-types/src/lib.rs | 40 ++- guest-agent/Cargo.toml | 3 +- guest-agent/rpc/proto/agent_rpc.proto | 13 + guest-agent/src/rpc_service.rs | 24 +- kms/Cargo.toml | 1 + kms/rpc/proto/kms_rpc.proto | 3 +- kms/src/main_service.rs | 358 ++++------------------ kms/src/main_service/upgrade_authority.rs | 1 - kms/src/onboard_service.rs | 59 ++-- 12 files changed, 160 insertions(+), 373 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1ae5a7f7d..406bf9347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ lspci = { path = "lspci" } sodiumbox = { path = "sodiumbox" } serde-duration = { path = "serde-duration" } dstack-mr = { path = "dstack-mr" } +dstack-verifier = { path = "verifier", default-features = false } size-parser = { path = "size-parser" } # Core dependencies @@ -227,6 +228,7 @@ yaml-rust2 = "0.10.4" luks2 = "0.5.0" scopeguard = "1.2.0" +flate2 = "1.1" [profile.release] panic = "abort" diff --git a/cert-client/src/lib.rs b/cert-client/src/lib.rs index 3be6bb475..680d51f78 100644 --- a/cert-client/src/lib.rs +++ b/cert-client/src/lib.rs @@ -8,10 +8,9 @@ use dstack_types::{AppKeys, KeyProvider}; use ra_rpc::client::{RaClient, RaClientConfig}; use ra_tls::{ attestation::QuoteContentType, - cert::{generate_ra_cert, CaCert, CertConfig, CertSigningRequest}, + cert::{generate_ra_cert, CaCert, CertConfig, CertSigningRequestV2, Csr}, rcgen::KeyPair, }; -use tdx_attest::{eventlog::read_event_logs, get_quote}; pub enum CertRequestClient { Local { @@ -26,7 +25,7 @@ pub enum CertRequestClient { impl CertRequestClient { pub async fn sign_csr( &self, - csr: &CertSigningRequest, + csr: &CertSigningRequestV2, signature: &[u8], ) -> Result> { match self { @@ -39,7 +38,7 @@ impl CertRequestClient { CertRequestClient::Kms { client, vm_config } => { let response = client .sign_cert(SignCertRequest { - api_version: 1, + api_version: 2, csr: csr.to_vec(), signature: signature.to_vec(), vm_config: vm_config.clone(), @@ -63,7 +62,9 @@ impl CertRequestClient { vm_config: String, ) -> Result { match &keys.key_provider { - KeyProvider::None { key } | KeyProvider::Local { key, .. } => { + KeyProvider::None { key } + | KeyProvider::Local { key, .. } + | KeyProvider::Tpm { key, .. } => { let ca = CaCert::new(keys.ca_cert.clone(), key.clone()) .context("Failed to create CA")?; Ok(CertRequestClient::Local { ca }) @@ -100,22 +101,15 @@ impl CertRequestClient { ) -> Result> { let pubkey = key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let (quote, event_log) = if !no_ra { - let (_, quote) = get_quote(&report_data, None).context("Failed to get quote")?; - let event_log = read_event_logs().context("Failed to decode event log")?; - let event_log = - serde_json::to_vec(&event_log).context("Failed to serialize event log")?; - (quote, event_log) - } else { - (vec![], vec![]) - }; + let attestation = ra_rpc::Attestation::quote(&report_data) + .context("Failed to get quote for cert pubkey")? + .into_versioned(); - let csr = CertSigningRequest { + let csr = CertSigningRequestV2 { confirm: "please sign cert:".to_string(), pubkey, config, - quote, - event_log, + attestation, }; let signature = csr.signed_by(key).context("Failed to sign the CSR")?; self.sign_csr(&csr, &signature) diff --git a/dstack-types/Cargo.toml b/dstack-types/Cargo.toml index d0b0ae647..997b0e43f 100644 --- a/dstack-types/Cargo.toml +++ b/dstack-types/Cargo.toml @@ -10,6 +10,7 @@ edition.workspace = true license.workspace = true [dependencies] +scale = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] } serde-human-bytes.workspace = true sha3.workspace = true diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 4d3d7271c..282172caa 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 +use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; use size_parser::human_size; @@ -106,7 +107,7 @@ impl AppCompose { } pub fn kms_enabled(&self) -> bool { - self.kms_enabled || self.feature_enabled("kms") + self.key_provider().is_kms() } pub fn key_provider(&self) -> KeyProviderKind { @@ -260,3 +261,40 @@ pub fn dstack_agent_address() -> String { } "unix:/var/run/dstack.sock".into() } + +/// Hardware/Cloud Platform +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +#[serde(rename_all = "lowercase")] +pub enum Platform { + /// dstack bare platform + Dstack, + /// Google Cloud Platform + Gcp, +} + +impl Platform { + /// Detect platform from system DMI information + pub fn detect() -> Option { + if let Ok(board_name) = std::fs::read_to_string("/sys/class/dmi/id/product_name") { + match board_name.trim() { + "dstack" | "qemu" => return Some(Self::Dstack), + "Google Compute Engine" => return Some(Self::Gcp), + _ => {} + } + } + None + } + + /// Detect platform from system DMI information, default to Dstack if cannot detect + pub fn detect_or_dstack() -> Self { + Self::detect().unwrap_or(Self::Dstack) + } + + /// Get platform name as string + pub fn as_str(&self) -> &'static str { + match self { + Self::Dstack => "dstack", + Self::Gcp => "gcp", + } + } +} diff --git a/guest-agent/Cargo.toml b/guest-agent/Cargo.toml index b8e0c881c..7d97c92c5 100644 --- a/guest-agent/Cargo.toml +++ b/guest-agent/Cargo.toml @@ -29,8 +29,9 @@ rinja.workspace = true git-version.workspace = true ra-rpc = { workspace = true, features = ["client", "rocket"] } dstack-guest-agent-rpc.workspace = true -ra-tls.workspace = true +ra-tls = { workspace = true, features = ["quote"] } tdx-attest.workspace = true +tpm-attest.workspace = true guest-api = { workspace = true, features = ["client"] } host-api = { workspace = true, features = ["client"] } sysinfo.workspace = true diff --git a/guest-agent/rpc/proto/agent_rpc.proto b/guest-agent/rpc/proto/agent_rpc.proto index 4d728dc34..741e52632 100644 --- a/guest-agent/rpc/proto/agent_rpc.proto +++ b/guest-agent/rpc/proto/agent_rpc.proto @@ -43,6 +43,10 @@ service DstackGuest { // Generates a TDX quote with given report data. rpc GetQuote(RawQuoteArgs) returns (GetQuoteResponse) {} + // Generates a versioned attestation with the given report data. + // Returns a dstack-defined attestation format that supports different attestation modes across platforms. + rpc GetAttestation(RawQuoteArgs) returns (GetAttestationResponse) {} + // Emit an event. This extends the event to RTMR3 on TDX platform. rpc EmitEvent(EmitEventArgs) returns (google.protobuf.Empty) {} @@ -169,6 +173,11 @@ message TdxQuoteResponse { string prefix = 4; } +message GetAttestationResponse { + // The attestation + bytes attestation = 1; +} + message GetQuoteResponse { // TDX quote bytes quote = 1; @@ -211,6 +220,10 @@ message AppInfo { bytes compose_hash = 13; // VM config string vm_config = 14; + // Cloud provider sys_vendor (e.g. "Google") + string cloud_vendor = 15; + // Cloud provider product_name (e.g. "Google Compute Engine") + string cloud_product = 16; } // The response to a Version request diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 7480e5c76..fe9485b26 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -12,9 +12,9 @@ use dstack_guest_agent_rpc::{ tappd_server::{TappdRpc, TappdServer}, worker_server::{WorkerRpc, WorkerServer}, AppInfo, DeriveK256KeyResponse, DeriveKeyArgs, EmitEventArgs, GetAttestationForAppKeyRequest, - GetKeyArgs, GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, GetTlsKeyResponse, RawQuoteArgs, - SignRequest, SignResponse, TdxQuoteArgs, TdxQuoteResponse, VerifyRequest, VerifyResponse, - WorkerVersion, + GetAttestationResponse, GetKeyArgs, GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, + GetTlsKeyResponse, RawQuoteArgs, SignRequest, SignResponse, TdxQuoteArgs, TdxQuoteResponse, + VerifyRequest, VerifyResponse, WorkerVersion, }; use dstack_types::{AppKeys, SysConfig}; use ed25519_dalek::ed25519::signature::hazmat::{PrehashSigner, PrehashVerifier}; @@ -140,23 +140,17 @@ pub struct InternalRpcHandler { pub async fn get_info(state: &AppState, external: bool) -> Result { let hide_tcb_info = external && !state.config().app_compose.public_tcbinfo; - let response = InternalRpcHandler { - state: state.clone(), - } - .get_quote(RawQuoteArgs { - report_data: [0; 64].to_vec(), - }) - .await; - let Ok(response) = response else { - return Ok(AppInfo::default()); - }; - let Ok(attestation) = Attestation::new(response.quote, response.event_log.into()) else { + let Ok(attestation) = Attestation::local() else { return Ok(AppInfo::default()); }; let app_info = attestation .decode_app_info(false) .context("Failed to decode app info")?; - let event_log = &attestation.event_log; + let event_log = attestation + .tdx_quote + .as_ref() + .map(|q| &q.event_log[..]) + .unwrap_or_default(); let tcb_info = if hide_tcb_info { "".to_string() } else { diff --git a/kms/Cargo.toml b/kms/Cargo.toml index 3a08c7485..bc33bc6a7 100644 --- a/kms/Cargo.toml +++ b/kms/Cargo.toml @@ -45,6 +45,7 @@ dstack-types.workspace = true tokio = { workspace = true, features = ["process"] } tempfile.workspace = true serde-duration.workspace = true +dstack-verifier = { workspace = true, default-features = false } dstack-mr.workspace = true [features] diff --git a/kms/rpc/proto/kms_rpc.proto b/kms/rpc/proto/kms_rpc.proto index baac19111..008151218 100644 --- a/kms/rpc/proto/kms_rpc.proto +++ b/kms/rpc/proto/kms_rpc.proto @@ -115,8 +115,7 @@ message BootstrapRequest { message BootstrapResponse { bytes ca_pubkey = 1; bytes k256_pubkey = 2; - bytes quote = 3; - bytes eventlog = 4; + bytes attestation = 3; } message OnboardRequest { diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 66fbf87bc..938c354a1 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -2,11 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::{path::PathBuf, sync::Arc}; use anyhow::{bail, Context, Result}; use dstack_kms_rpc::{ @@ -16,18 +12,17 @@ use dstack_kms_rpc::{ SignCertRequest, SignCertResponse, }; use dstack_types::VmConfig; +use dstack_verifier::{CvmVerifier, VerificationDetails}; use fs_err as fs; use k256::ecdsa::SigningKey; -use ra_rpc::{Attestation, CallContext, RpcCall}; +use ra_rpc::{CallContext, RpcCall}; use ra_tls::{ attestation::VerifiedAttestation, - cert::{CaCert, CertRequest, CertSigningRequest}, + cert::{CaCert, CertRequest, CertSigningRequestV1, CertSigningRequestV2, Csr}, kdf, }; use scale::Decode; -use serde::{Deserialize, Serialize}; use sha2::Digest; -use tokio::{io::AsyncWriteExt, process::Command}; use tracing::{debug, info}; use upgrade_authority::BootInfo; @@ -57,6 +52,7 @@ pub struct KmsStateInner { k256_key: SigningKey, temp_ca_cert: String, temp_ca_key: String, + verifier: CvmVerifier, } impl KmsState { @@ -70,6 +66,11 @@ impl KmsState { fs::read_to_string(config.tmp_ca_key()).context("Faeild to read temp ca key")?; let temp_ca_cert = fs::read_to_string(config.tmp_ca_cert()).context("Faeild to read temp ca cert")?; + let verifier = CvmVerifier::new( + config.image.cache_dir.display().to_string(), + config.image.download_url.clone(), + config.image.download_timeout, + ); Ok(Self { inner: Arc::new(KmsStateInner { config, @@ -77,6 +78,7 @@ impl KmsState { k256_key, temp_ca_cert, temp_ca_key, + verifier, }), }) } @@ -93,49 +95,6 @@ struct BootConfig { os_image_hash: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)] -struct Mrs { - mrtd: String, - rtmr0: String, - rtmr1: String, - rtmr2: String, -} - -impl Mrs { - fn assert_eq(&self, other: &Self) -> Result<()> { - let Self { - mrtd, - rtmr0, - rtmr1, - rtmr2, - } = self; - if mrtd != &other.mrtd { - bail!("MRTD does not match"); - } - if rtmr0 != &other.rtmr0 { - bail!("RTMR0 does not match"); - } - if rtmr1 != &other.rtmr1 { - bail!("RTMR1 does not match"); - } - if rtmr2 != &other.rtmr2 { - bail!("RTMR2 does not match"); - } - Ok(()) - } -} - -impl From<&BootInfo> for Mrs { - fn from(report: &BootInfo) -> Self { - Self { - mrtd: hex::encode(&report.mrtd), - rtmr0: hex::encode(&report.rtmr0), - rtmr1: hex::encode(&report.rtmr1), - rtmr2: hex::encode(&report.rtmr2), - } - } -} - impl RpcHandler { fn ensure_attested(&self) -> Result<&VerifiedAttestation> { let Some(attestation) = &self.attestation else { @@ -161,10 +120,6 @@ impl RpcHandler { self.state.config.image.cache_dir.join("images") } - fn mr_cache_dir(&self) -> PathBuf { - self.state.config.image.cache_dir.join("computed") - } - fn remove_cache(&self, parent_dir: &PathBuf, sub_dir: &str) -> Result<()> { if sub_dir.is_empty() { return Ok(()); @@ -190,236 +145,21 @@ impl RpcHandler { Ok(()) } - fn get_cached_mrs(&self, key: &str) -> Result { - let path = self.mr_cache_dir().join(key); - if !path.exists() { - bail!("Cached MRs not found"); - } - let content = fs::read_to_string(path).context("Failed to read cached MRs")?; - let cached_mrs: Mrs = - serde_json::from_str(&content).context("Failed to parse cached MRs")?; - Ok(cached_mrs) - } - - fn cache_mrs(&self, key: &str, mrs: &Mrs) -> Result<()> { - let path = self.mr_cache_dir().join(key); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).context("Failed to create cache directory")?; - } - safe_write::safe_write( - &path, - serde_json::to_string(mrs).context("Failed to serialize cached MRs")?, - ) - .context("Failed to write cached MRs")?; - Ok(()) - } - - async fn verify_os_image_hash(&self, vm_config: &VmConfig, report: &BootInfo) -> Result<()> { + async fn verify_os_image_hash( + &self, + vm_config: String, + report: &VerifiedAttestation, + ) -> Result<()> { if !self.state.config.image.verify { info!("Image verification is disabled"); return Ok(()); } - let hex_os_image_hash = hex::encode(&vm_config.os_image_hash); - info!("Verifying image {hex_os_image_hash}"); - - let verified_mrs: Mrs = report.into(); - - let cache_key = { - let vm_config = - serde_json::to_vec(vm_config).context("Failed to serialize VM config")?; - hex::encode(sha2::Sha256::new_with_prefix(&vm_config).finalize()) - }; - if let Ok(cached_mrs) = self.get_cached_mrs(&cache_key) { - cached_mrs - .assert_eq(&verified_mrs) - .context("MRs do not match (cached)")?; - return Ok(()); - } - - // Create a directory for the image if it doesn't exist - let image_dir = self.image_cache_dir().join(&hex_os_image_hash); - // Check if metadata.json exists, if not download the image - let metadata_path = image_dir.join("metadata.json"); - if !metadata_path.exists() { - info!("Image {} not found, downloading", hex_os_image_hash); - tokio::time::timeout( - self.state.config.image.download_timeout, - self.download_image(&hex_os_image_hash, &image_dir), - ) + let mut detail = VerificationDetails::default(); + self.state + .verifier + .verify_os_image_hash(vm_config, report, false, &mut detail) .await - .context("Download image timeout")? - .with_context(|| format!("Failed to download image {hex_os_image_hash}"))?; - } - - let image_info = - fs::read_to_string(metadata_path).context("Failed to read image metadata")?; - let image_info: dstack_types::ImageInfo = - serde_json::from_str(&image_info).context("Failed to parse image metadata")?; - - let fw_path = image_dir.join(&image_info.bios); - let kernel_path = image_dir.join(&image_info.kernel); - let initrd_path = image_dir.join(&image_info.initrd); - let kernel_cmdline = image_info.cmdline + " initrd=initrd"; - - let mrs = dstack_mr::Machine::builder() - .cpu_count(vm_config.cpu_count) - .memory_size(vm_config.memory_size) - .firmware(&fw_path.display().to_string()) - .kernel(&kernel_path.display().to_string()) - .initrd(&initrd_path.display().to_string()) - .kernel_cmdline(&kernel_cmdline) - .root_verity(true) - .hotplug_off(vm_config.hotplug_off) - .maybe_two_pass_add_pages(vm_config.qemu_single_pass_add_pages) - .maybe_pic(vm_config.pic) - .maybe_qemu_version(vm_config.qemu_version.clone()) - .maybe_pci_hole64_size(if vm_config.pci_hole64_size > 0 { - Some(vm_config.pci_hole64_size) - } else { - None - }) - .hugepages(vm_config.hugepages) - .num_gpus(vm_config.num_gpus) - .num_nvswitches(vm_config.num_nvswitches) - .build() - .measure() - .context("Failed to compute expected MRs")?; - - let expected_mrs: Mrs = Mrs { - mrtd: hex::encode(&mrs.mrtd), - rtmr0: hex::encode(&mrs.rtmr0), - rtmr1: hex::encode(&mrs.rtmr1), - rtmr2: hex::encode(&mrs.rtmr2), - }; - self.cache_mrs(&cache_key, &expected_mrs) - .context("Failed to cache MRs")?; - expected_mrs - .assert_eq(&verified_mrs) - .context("MRs do not match")?; - Ok(()) - } - - async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { - // Create a hex representation of the os_image_hash for URL and directory naming - let url = self - .state - .config - .image - .download_url - .replace("{OS_IMAGE_HASH}", hex_os_image_hash); - - // Create a temporary directory for extraction within the cache directory - let cache_dir = self.image_cache_dir().join("tmp"); - fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; - let auto_delete_temp_dir = tempfile::Builder::new() - .prefix("tmp-download-") - .tempdir_in(&cache_dir) - .context("Failed to create temporary directory")?; - let tmp_dir = auto_delete_temp_dir.path(); - // Download the image tarball - info!("Downloading image from {}", url); - let client = reqwest::Client::new(); - let response = client - .get(&url) - .send() - .await - .context("Failed to download image")?; - - if !response.status().is_success() { - bail!( - "Failed to download image: HTTP status {}, url: {url}", - response.status(), - ); - } - - // Save the tarball to a temporary file using streaming - let tarball_path = tmp_dir.join("image.tar.gz"); - let mut file = tokio::fs::File::create(&tarball_path) - .await - .context("Failed to create tarball file")?; - let mut response = response; - while let Some(chunk) = response.chunk().await? { - file.write_all(&chunk) - .await - .context("Failed to write chunk to file")?; - } - - let extracted_dir = tmp_dir.join("extracted"); - fs::create_dir_all(&extracted_dir).context("Failed to create extraction directory")?; - - // Extract the tarball - let output = Command::new("tar") - .arg("xzf") - .arg(&tarball_path) - .current_dir(&extracted_dir) - .output() - .await - .context("Failed to extract tarball")?; - - if !output.status.success() { - bail!( - "Failed to extract tarball: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - // Verify checksum - let output = Command::new("sha256sum") - .arg("-c") - .arg("sha256sum.txt") - .current_dir(&extracted_dir) - .output() - .await - .context("Failed to verify checksum")?; - - if !output.status.success() { - bail!( - "Checksum verification failed: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - // Remove the files that are not listed in sha256sum.txt - let sha256sum_path = extracted_dir.join("sha256sum.txt"); - let files_doc = - fs::read_to_string(&sha256sum_path).context("Failed to read sha256sum.txt")?; - let listed_files: Vec<&OsStr> = files_doc - .lines() - .flat_map(|line| line.split_whitespace().nth(1)) - .map(|s| s.as_ref()) - .collect(); - let files = fs::read_dir(&extracted_dir).context("Failed to read directory")?; - for file in files { - let file = file.context("Failed to read directory entry")?; - let filename = file.file_name(); - if !listed_files.contains(&filename.as_os_str()) { - if file.path().is_dir() { - fs::remove_dir_all(file.path()).context("Failed to remove directory")?; - } else { - fs::remove_file(file.path()).context("Failed to remove file")?; - } - } - } - - // os_image_hash should eq to sha256sum of the sha256sum.txt - let os_image_hash = sha2::Sha256::new_with_prefix(files_doc.as_bytes()).finalize(); - if hex::encode(os_image_hash) != hex_os_image_hash { - bail!("os_image_hash does not match sha256sum of the sha256sum.txt"); - } - - // Move the extracted files to the destination directory - let metadata_path = extracted_dir.join("metadata.json"); - if !metadata_path.exists() { - bail!("metadata.json not found in the extracted archive"); - } - - if dst_dir.exists() { - fs::remove_dir_all(dst_dir).context("Failed to remove destination directory")?; - } - let dst_dir_parent = dst_dir.parent().context("Failed to get parent directory")?; - fs::create_dir_all(dst_dir_parent).context("Failed to create parent directory")?; - // Move the extracted files to the destination directory - fs::rename(extracted_dir, dst_dir) - .context("Failed to move extracted files to destination directory")?; + .context("Failed to verify os image hash")?; Ok(()) } @@ -428,17 +168,19 @@ impl RpcHandler { att: &VerifiedAttestation, is_kms: bool, use_boottime_mr: bool, - vm_config: &str, + vm_config_str: &str, ) -> Result { - let report = att - .report + let Some(tdx_report) = &att.report.tdx_report else { + bail!("No TD report in attestation"); + }; + let report = tdx_report .report .as_td10() .context("Failed to decode TD report")?; let app_info = att.decode_app_info(use_boottime_mr)?; - debug!("vm_config: {vm_config}"); + debug!("vm_config: {vm_config_str}"); let vm_config: VmConfig = - serde_json::from_str(vm_config).context("Failed to decode VM config")?; + serde_json::from_str(vm_config_str).context("Failed to decode VM config")?; let os_image_hash = vm_config.os_image_hash.clone(); let boot_info = BootInfo { mrtd: report.mr_td.to_vec(), @@ -454,10 +196,8 @@ impl RpcHandler { instance_id: app_info.instance_id, device_id: app_info.device_id, key_provider_info: app_info.key_provider_info, - event_log: String::from_utf8(att.raw_event_log.clone()) - .context("Failed to serialize event log")?, - tcb_status: att.report.status.clone(), - advisory_ids: att.report.advisory_ids.clone(), + tcb_status: tdx_report.status.clone(), + advisory_ids: tdx_report.advisory_ids.clone(), }; let response = self .state @@ -468,7 +208,7 @@ impl RpcHandler { if !response.is_allowed { bail!("Boot denied: {}", response.reason); } - self.verify_os_image_hash(&vm_config, &boot_info) + self.verify_os_image_hash(vm_config_str.into(), att) .await .context("Failed to verify os image hash")?; Ok(BootConfig { @@ -614,15 +354,27 @@ impl KmsRpc for RpcHandler { } async fn sign_cert(self, request: SignCertRequest) -> Result { - if request.api_version > 1 { - bail!("Unsupported API version: {}", request.api_version); - } - let csr = - CertSigningRequest::decode(&mut &request.csr[..]).context("Failed to parse csr")?; - csr.verify(&request.signature) - .context("Failed to verify csr signature")?; - let attestation = Attestation::new(csr.quote.clone(), csr.event_log.clone()) - .context("Failed to create attestation from quote and event log")? + let csr = match request.api_version { + 1 => { + let csr = CertSigningRequestV1::decode(&mut &request.csr[..]) + .context("Failed to parse csr")?; + csr.verify(&request.signature) + .context("Failed to verify csr signature")?; + csr.try_into().context("Failed to upgrade csr v1 to v2")? + } + 2 => { + let csr = CertSigningRequestV2::decode(&mut &request.csr[..]) + .context("Failed to parse csr")?; + csr.verify(&request.signature) + .context("Failed to verify csr signature")?; + csr + } + _ => bail!("Unsupported API version: {}", request.api_version), + }; + let attestation = csr + .attestation + .clone() + .into_inner() .verify_with_ra_pubkey(&csr.pubkey, self.state.config.pccs_url.as_deref()) .await .context("Quote verification failed")?; @@ -646,8 +398,10 @@ impl KmsRpc for RpcHandler { self.ensure_admin(&request.token)?; self.remove_cache(&self.image_cache_dir(), &request.image_hash) .context("Failed to clear image cache")?; - self.remove_cache(&self.mr_cache_dir(), &request.config_hash) - .context("Failed to clear MR cache")?; + // Clear measurement cache (now handled by verifier's cache in measurements/ dir) + let mr_cache_dir = self.state.config.image.cache_dir.join("measurements"); + self.remove_cache(&mr_cache_dir, &request.config_hash) + .context("Failed to clear measurement cache")?; Ok(()) } } diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index 5db8a47de..6909e6258 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -36,7 +36,6 @@ pub(crate) struct BootInfo { pub device_id: Vec, #[serde(with = "hex_bytes")] pub key_provider_info: Vec, - pub event_log: String, pub tcb_status: String, pub advisory_ids: Vec, } diff --git a/kms/src/onboard_service.rs b/kms/src/onboard_service.rs index ffe38f162..1dcd3e0a0 100644 --- a/kms/src/onboard_service.rs +++ b/kms/src/onboard_service.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use dstack_guest_agent_rpc::{ - dstack_guest_client::DstackGuestClient, GetQuoteResponse, RawQuoteArgs, + dstack_guest_client::DstackGuestClient, GetAttestationResponse, RawQuoteArgs, }; use dstack_kms_rpc::{ kms_client::KmsClient, @@ -16,7 +16,7 @@ use http_client::prpc::PrpcClient; use k256::ecdsa::SigningKey; use ra_rpc::{client::RaClient, CallContext, RpcCall}; use ra_tls::{ - attestation::QuoteContentType, + attestation::{QuoteContentType, VersionedAttestation}, cert::{CaCert, CertRequest}, rcgen::{Certificate, KeyPair, PKCS_ECDSA_P256_SHA256}, }; @@ -58,21 +58,17 @@ impl OnboardRpc for OnboardHandler { let k256_pubkey = keys.k256_key.verifying_key().to_sec1_bytes().to_vec(); let ca_pubkey = keys.ca_key.public_key_der(); - let quote; - let eventlog; - if quote_enabled { - (quote, eventlog) = quote_keys(&ca_pubkey, &k256_pubkey).await?; + let attestation = if quote_enabled { + Some(attest_keys(&ca_pubkey, &k256_pubkey).await?) } else { - quote = vec![]; - eventlog = vec![]; + None }; let cfg = &self.state.config; let response = BootstrapResponse { ca_pubkey, k256_pubkey, - quote, - eventlog, + attestation: attestation.unwrap_or_default(), }; // Store the bootstrap info safe_write(cfg.bootstrap_info(), serde_json::to_vec(&response)?)?; @@ -143,18 +139,17 @@ impl Keys { .key(&ca_key) .build() .self_signed()?; - - let mut quote = None; - let mut event_log = None; - - if quote_enabled { + let attestation = if quote_enabled { let pubkey = rpc_key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let resposne = app_quote(report_data.to_vec()) + let response = app_attest(report_data.to_vec()) .await .context("Failed to get quote")?; - quote = Some(resposne.quote); - event_log = Some(resposne.event_log.into_bytes()); + let attestation = VersionedAttestation::from_scale(&response.attestation) + .context("Invalid attestation")?; + Some(attestation) + } else { + None }; // Sign WWW server cert with KMS cert @@ -162,8 +157,7 @@ impl Keys { .subject(domain) .alt_names(&[domain.to_string()]) .special_usage("kms:rpc") - .maybe_quote(quote.as_deref()) - .maybe_event_log(event_log.as_deref()) + .maybe_attestation(attestation.as_ref()) .key(&rpc_key) .build() .signed_by(&ca_cert, &ca_key)?; @@ -302,21 +296,20 @@ fn dstack_client() -> DstackGuestClient { DstackGuestClient::new(http_client) } -async fn app_quote(report_data: Vec) -> Result { - let quote = dstack_client() - .get_quote(RawQuoteArgs { report_data }) - .await?; - Ok(quote) +async fn app_attest(report_data: Vec) -> Result { + dstack_client() + .get_attestation(RawQuoteArgs { report_data }) + .await } -async fn quote_keys(p256_pubkey: &[u8], k256_pubkey: &[u8]) -> Result<(Vec, Vec)> { +async fn attest_keys(p256_pubkey: &[u8], k256_pubkey: &[u8]) -> Result> { let p256_hex = hex::encode(p256_pubkey); let k256_hex = hex::encode(k256_pubkey); let content_to_quote = format!("dstack-kms-genereted-keys-v1:{p256_hex};{k256_hex};"); let hash = keccak256(content_to_quote.as_bytes()); let report_data = pad64(hash); - let res = app_quote(report_data).await?; - Ok((res.quote, res.event_log.into())) + let res = app_attest(report_data).await?; + Ok(res.attestation) } fn keccak256(msg: &[u8]) -> [u8; 32] { @@ -338,19 +331,17 @@ async fn gen_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result<(String, use ra_tls::rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; let ca = CaCert::new(ca_cert_pem, ca_key_pem)?; - let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let pubkey = key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let quote_res = app_quote(report_data.to_vec()) + let response = app_attest(report_data.to_vec()) .await .context("Failed to get quote")?; - let quote = quote_res.quote; - let event_log: Vec = quote_res.event_log.into(); + let attestation = + VersionedAttestation::from_scale(&response.attestation).context("Invalid attestation")?; let req = CertRequest::builder() .subject("RA-TLS TEMP Cert") - .quote("e) - .event_log(&event_log) + .attestation(&attestation) .key(&key) .build(); let cert = ca.sign(req).context("Failed to sign certificate")?; From d5a5856d9eedc81fec49fb8b3a6d8dc537abfa1c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:33:00 +0800 Subject: [PATCH 022/264] gw: Use attestation v2 --- gateway/rpc/proto/gateway_rpc.proto | 5 ++++ gateway/src/main_service.rs | 38 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/gateway/rpc/proto/gateway_rpc.proto b/gateway/rpc/proto/gateway_rpc.proto index 890e87f7f..2c7aa5528 100644 --- a/gateway/rpc/proto/gateway_rpc.proto +++ b/gateway/rpc/proto/gateway_rpc.proto @@ -87,7 +87,10 @@ message HostInfo { message QuotedPublicKey { bytes public_key = 1; + // The TDX quote of the public_key string quote = 2; + // The dstack attestation of the public key. + string attestation = 3; } // AcmeInfoResponse is the response for AcmeInfo. @@ -104,6 +107,8 @@ message AcmeInfoResponse { string active_cert = 5; // The domain that serves ZT-HTTPS string base_domain = 6; + // The attestation of the ACME account URI. + string account_attestation = 7; } // Get HostInfo for associated instance id. diff --git a/gateway/src/main_service.rs b/gateway/src/main_service.rs index c85d501cc..953df694c 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -202,6 +202,14 @@ impl Proxy { ) .await .unwrap_or_default(); + let account_attestation = get_or_generate_attestation( + &agent, + QuoteContentType::Custom("acme-account"), + account_uri.as_bytes(), + workdir.acme_account_quote_path(), + ) + .await + .unwrap_or_default(); let mut quoted_hist_keys = vec![]; for cert_path in workdir.list_certs().unwrap_or_default() { @@ -215,9 +223,18 @@ impl Proxy { ) .await .unwrap_or_default(); + let attestation = get_or_generate_attestation( + &agent, + QuoteContentType::Custom("zt-cert"), + &pubkey, + cert_path.display().to_string() + ".quote", + ) + .await + .unwrap_or_default(); quoted_hist_keys.push(QuotedPublicKey { public_key: pubkey, quote, + attestation, }); } let active_cert = @@ -227,6 +244,7 @@ impl Proxy { account_uri, hist_keys: keys.into_iter().collect(), account_quote, + account_attestation, quoted_hist_keys, active_cert, base_domain: config.proxy.base_domain.clone(), @@ -825,6 +843,26 @@ async fn get_or_generate_quote( Ok(quote) } +async fn get_or_generate_attestation( + agent: &DstackGuestClient, + content_type: QuoteContentType<'_>, + payload: &[u8], + quote_path: impl AsRef, +) -> Result { + let quote_path = quote_path.as_ref(); + if fs::metadata(quote_path).is_ok() { + return fs::read_to_string(quote_path).context("Failed to read quote"); + } + let report_data = content_type.to_report_data(payload).to_vec(); + let response = agent + .get_attestation(RawQuoteArgs { report_data }) + .await + .context("Failed to get quote")?; + let attestation = serde_json::to_string(&response).context("Failed to serialize quote")?; + safe_write(quote_path, &attestation).context("Failed to write quote")?; + Ok(attestation) +} + impl RpcCall for RpcHandler { type PrpcService = GatewayServer; From d308c08a6d072e1e12e1f80291b91d62fb364e9a Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:35:01 +0800 Subject: [PATCH 023/264] guest-agent: AttestationV2 --- guest-agent/src/rpc_service.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index fe9485b26..a2841c8ab 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -286,15 +286,12 @@ impl DstackGuestRpc for InternalRpcHandler { &self.state.inner.vm_config, ); } - let (_, quote) = - tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?; - let event_log = read_event_logs().context("Failed to decode event log")?; - let event_log = - serde_json::to_string(&event_log).context("Failed to serialize event log")?; - + let attestation = Attestation::quote(&report_data).context("Failed to get quote")?; + let tdx_quote = attestation.get_tdx_quote_bytes(); + let tdx_event_log = attestation.get_tdx_event_log_string(); Ok(GetQuoteResponse { - quote, - event_log, + quote: tdx_quote.unwrap_or_default(), + event_log: tdx_event_log.unwrap_or_default(), report_data: report_data.to_vec(), vm_config: self.state.inner.vm_config.clone(), }) @@ -396,6 +393,11 @@ impl DstackGuestRpc for InternalRpcHandler { }; Ok(VerifyResponse { valid }) } + + async fn get_attestation(self, request: RawQuoteArgs) -> Result { + let todo = "implement it"; + todo!() + } } fn simulate_quote( From 16008462c509b0178997f28f458bd2db709f15e1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:40:57 +0800 Subject: [PATCH 024/264] ratls: Attestation V2 --- ra-tls/Cargo.toml | 12 + ra-tls/src/attestation.rs | 501 +++++++++++++++++++++++++++++--------- ra-tls/src/cert.rs | 391 +++++++++++++++++++++++++---- ra-tls/src/lib.rs | 1 - ra-tls/src/oids.rs | 6 +- 5 files changed, 740 insertions(+), 171 deletions(-) diff --git a/ra-tls/Cargo.toml b/ra-tls/Cargo.toml index c6451fbb3..b953340fa 100644 --- a/ra-tls/Cargo.toml +++ b/ra-tls/Cargo.toml @@ -33,4 +33,16 @@ scale.workspace = true cc-eventlog.workspace = true serde-human-bytes.workspace = true +flate2.workspace = true or-panic.workspace = true +rand.workspace = true +tpm-attest = { workspace = true, optional = true } +tpm-types.workspace = true +dstack-types.workspace = true +tpm-qvl.workspace = true +hex_fmt.workspace = true +ez-hash.workspace = true + +[features] +default = ["quote"] +quote = ["dep:tpm-attest"] diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index 9efe3cfb8..ce134884b 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -4,16 +4,18 @@ //! Attestation functions -use std::borrow::Cow; +use std::{borrow::Cow, str::FromStr}; use anyhow::{anyhow, bail, Context, Result}; -use dcap_qvl::quote::Quote; -use qvl::{ - quote::{EnclaveReport, Report, TDReport10, TDReport15}, - verify::VerifiedReport, +use dcap_qvl::{ + quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15}, + verify::VerifiedReport as TdxVerifiedReport, }; -use serde::Serialize; -use sha2::{Digest as _, Sha384}; +use dstack_types::Platform; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use sha2::Digest as _; +use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; use x509_parser::parse_x509_certificate; use crate::{oids, traits::CertExt}; @@ -21,6 +23,89 @@ use cc_eventlog::TdxEventLogEntry as EventLog; use or_panic::ResultOrPanic; use serde_human_bytes as hex_bytes; +// Re-export TpmQuote from tpm-types +pub use tpm_types::TpmQuote; + +/// Attestation mode +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +pub enum AttestationMode { + /// Intel TDX with DCAP quote only + #[serde(rename = "dstack-tdx")] + #[default] + DstackTdx, + /// GCP TDX with DCAP quote only + #[serde(rename = "gcp-tdx")] + GcpTdx, + /// Dstack attestation SDK in AWS Nitro Enclave + #[serde(rename = "dstack-nitro")] + DstackNitro, +} + +impl FromStr for AttestationMode { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "dstack-tdx" => Ok(Self::DstackTdx), + "gcp-tdx" => Ok(Self::GcpTdx), + "dstack-nitro" => Ok(Self::DstackNitro), + _ => bail!("Invalid attestation mode: {s}"), + } + } +} + +impl AttestationMode { + /// Get string representation + pub fn as_str(&self) -> &str { + match self { + Self::DstackTdx => "dstack-tdx", + Self::GcpTdx => "gcp-tdx", + Self::DstackNitro => "dstack-nitro", + } + } + + /// Detect attestation mode from system + pub fn detect() -> Result { + let has_tdx = std::path::Path::new("/dev/tdx_guest").exists(); + + // First, try to detect platform from DMI product name + let platform = Platform::detect_or_dstack(); + match platform { + Platform::Dstack => { + if has_tdx { + return Ok(Self::DstackTdx); + } + bail!("Unsupported platform: Dstack(-tdx)"); + } + Platform::Gcp => { + // GCP platform: TDX + TPM dual mode + if has_tdx { + return Ok(Self::GcpTdx); + } + bail!("Unsupported platform: GCP(-tdx)"); + } + } + } + + /// Check if TDX quote should be included + pub fn has_tdx(&self) -> bool { + match self { + Self::DstackTdx => true, + Self::GcpTdx => true, + Self::DstackNitro => false, + } + } + + /// Check if TPM quote should be included + pub fn has_tpm(&self) -> bool { + match self { + Self::DstackTdx => false, + Self::GcpTdx => true, + Self::DstackNitro => true, + } + } +} + /// The content type of a quote. A CVM should only generate quotes for these types. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QuoteContentType<'a> { @@ -94,42 +179,169 @@ impl QuoteContentType<'_> { } /// Represents a verified attestation -pub type VerifiedAttestation = Attestation; +#[derive(Clone)] +pub struct DstackVerifiedReport { + /// The verified TDX report + pub tdx_report: Option, + /// The verified TPM report + pub tpm_report: Option, +} -/// Attestation data -#[derive(Debug, Clone)] -pub struct Attestation { - /// Quote +fn get_report_data(report: &TdxVerifiedReport) -> [u8; 64] { + match report.report { + Report::SgxEnclave(enclave_report) => enclave_report.report_data, + Report::TD10(tdreport10) => tdreport10.report_data, + Report::TD15(tdreport15) => tdreport15.base.report_data, + } +} + +impl DstackVerifiedReport { + /// Check if the report is empty + pub fn is_empty(&self) -> bool { + self.tdx_report.is_none() && self.tpm_report.is_none() + } + + /// Ensure report data matches + pub fn ensure_report_data(&self, report_data: Option<[u8; 64]>) -> Result<[u8; 64]> { + let expected = match (&self.tdx_report, report_data) { + (Some(tdx_report), Some(rd)) => { + let td_report_data = get_report_data(tdx_report); + if td_report_data != rd { + bail!("tdx report_data mismatch"); + } + td_report_data + } + (Some(tdx_report), None) => get_report_data(tdx_report), + (None, Some(rd)) => rd, + (None, None) => bail!("no verified report"), + }; + + if let Some(tpm_report) = &self.tpm_report { + let expected_tpm_report_data = sha256(&[&expected]).to_vec(); + if tpm_report.attest.qualified_data != expected_tpm_report_data { + bail!( + "tpm qualified_data mismatch, expected: {}, actual: {}, report_data: {}", + hex_fmt::HexFmt(&expected_tpm_report_data), + hex_fmt::HexFmt(&tpm_report.attest.qualified_data), + hex_fmt::HexFmt(&expected) + ); + } + } + Ok(expected) + } +} + +/// Represents a verified attestation +pub type VerifiedAttestation = Attestation; + +/// Represents a TDX quote +#[derive(Clone, Encode, Decode)] +pub struct TdxQuote { + /// The quote gererated by Intel QE pub quote: Vec, - /// Raw event log - pub raw_event_log: Vec, - /// Event log + /// The event log pub event_log: Vec, +} + +/// Represents a versioned attestation +#[derive(Clone, Encode, Decode)] +pub enum VersionedAttestation { + /// Version 0 + V0 { + /// The attestation report + attestation: Attestation, + }, +} + +impl VersionedAttestation { + /// Decode VerifiedAttestation from scale encoded bytes + pub fn from_scale(scale: &[u8]) -> Result { + Self::decode(&mut &scale[..]).context("Failed to decode VersionedAttestation") + } + + /// Encode to scale encoded bytes + pub fn to_scale(&self) -> Vec { + self.encode() + } + + /// Turn into latest version of attestation + pub fn into_inner(self) -> Attestation { + match self { + Self::V0 { attestation } => attestation, + } + } +} + +/// Attestation data +#[derive(Clone, Encode, Decode)] +pub struct Attestation { + /// Attestation mode + pub mode: AttestationMode, + + /// TDX quote (only for TDX mode) + pub tdx_quote: Option, + + /// TPM quote (only for TPM mode) + pub tpm_quote: Option, + + /// The configuration of the VM + pub config: String, + /// Verified report pub report: R, } +impl Attestation { + /// Get TDX quote bytes + pub fn get_tdx_quote_bytes(&self) -> Option> { + self.tdx_quote.as_ref().map(|q| q.quote.clone()) + } + + /// Get TDX event log bytes + pub fn get_tdx_event_log_bytes(&self) -> Option> { + self.tdx_quote + .as_ref() + .map(|q| serde_json::to_vec(&q.event_log).unwrap_or_default()) + } + + /// Get TDX event log string + pub fn get_tdx_event_log_string(&self) -> Option { + self.tdx_quote + .as_ref() + .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) + } +} + impl Attestation { /// Decode the quote - pub fn decode_quote(&self) -> Result { - Quote::parse(&self.quote) + pub fn decode_tdx_quote(&self) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + Quote::parse(&tdx_quote.quote) } - fn find_event(&self, imr: u32, ad: &str) -> Result { - for event in &self.event_log { + fn find_event(&self, imr: u32, name: &str) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + for event in &tdx_quote.event_log { if event.imr == 3 && event.event == "system-ready" { break; } - if event.imr == imr && event.event == ad { + if event.imr == imr && event.event == name { return Ok(event.clone()); } } - Err(anyhow!("event {ad} not found")) + Err(anyhow!("event {name} not found")) } /// Replay event logs - pub fn replay_event_logs(&self, to_event: Option<&str>) -> Result<[[u8; 48]; 4]> { - replay_event_logs(&self.event_log, to_event) + pub fn replay_rtmr3(&self, to_event: Option<&str>) -> Result<[u8; 48]> { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + cc_eventlog::replay_event_logs(&tdx_quote.event_log, to_event, 3) } fn find_event_payload(&self, event: &str) -> Result> { @@ -159,10 +371,10 @@ impl Attestation { /// Decode the app info from the event log pub fn decode_app_info(&self, boottime_mr: bool) -> Result { - let rtmrs = self - .replay_event_logs(boottime_mr.then_some("boot-mr-done")) + let rtmr3 = self + .replay_rtmr3(boottime_mr.then_some("boot-mr-done")) .context("Failed to replay event logs")?; - let quote = self.decode_quote()?; + let quote = self.decode_tdx_quote()?; let device_id = sha256(&["e.header.user_data]).to_vec(); let td_report = quote.report.as_td10().context("TDX report not found")?; let key_provider_info = if boottime_mr { @@ -177,15 +389,21 @@ impl Attestation { }; let mr_system = sha256(&[ &td_report.mr_td, - &rtmrs[0], - &rtmrs[1], - &rtmrs[2], + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, &mr_key_provider, ]); let mr_aggregated = { use sha2::{Digest as _, Sha256}; let mut hasher = Sha256::new(); - for d in [&td_report.mr_td, &rtmrs[0], &rtmrs[1], &rtmrs[2], &rtmrs[3]] { + for d in [ + &td_report.mr_td, + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + &rtmr3, + ] { hasher.update(d); } // For backward compatibility. Don't include mr_config_id, mr_owner, mr_owner_config if they are all 0. @@ -201,14 +419,14 @@ impl Attestation { }; Ok(AppInfo { app_id: self.find_event_payload("app-id").unwrap_or_default(), - compose_hash: self.find_event_payload("compose-hash")?, + compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), instance_id: self.find_event_payload("instance-id").unwrap_or_default(), device_id, mrtd: td_report.mr_td, - rtmr0: rtmrs[0], - rtmr1: rtmrs[1], - rtmr2: rtmrs[2], - rtmr3: rtmrs[3], + rtmr0: td_report.rt_mr0, + rtmr1: td_report.rt_mr1, + rtmr2: td_report.rt_mr2, + rtmr3, os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), mr_system, mr_aggregated, @@ -219,12 +437,12 @@ impl Attestation { /// Decode the rootfs hash from the event log pub fn decode_rootfs_hash(&self) -> Result { self.find_event(3, "rootfs-hash") - .map(|event| hex::encode(event.digest)) + .map(|event| hex::encode(event.digest())) } /// Decode the report data in the quote - pub fn decode_report_data(&self) -> Result<[u8; 64]> { - match self.decode_quote()?.report { + pub fn decode_tdx_report_data(&self) -> Result<[u8; 64]> { + match self.decode_tdx_quote()?.report { Report::SgxEnclave(report) => Ok(report.report_data), Report::TD10(report) => Ok(report.report_data), Report::TD15(report) => Ok(report.base.report_data), @@ -232,37 +450,53 @@ impl Attestation { } } +#[cfg(feature = "quote")] impl Attestation { - /// Create an attestation for local machine + /// Create an attestation for local machine (auto-detect mode) pub fn local() -> Result { - let (_, quote) = tdx_attest::get_quote(&[0u8; 64], None).context("Failed to get quote")?; - let event_log = - tdx_attest::eventlog::read_event_logs().context("Failed to read event logs")?; - let raw_event_log = - serde_json::to_vec(&event_log).context("Failed to serialize event log")?; - Ok(Self { - quote, - raw_event_log, - event_log, - report: (), - }) + Self::quote(&[0u8; 64]) } - /// Create a new attestation - pub fn new(quote: Vec, raw_event_log: Vec) -> Result { - let event_log: Vec = if !raw_event_log.is_empty() { - serde_json::from_slice(&raw_event_log).context("invalid event log")? + /// Create an attestation from a report data + pub fn quote(report_data: &[u8; 64]) -> Result { + let mode = AttestationMode::detect()?; + let tdx_quote = if mode.has_tdx() { + let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; + let event_log = + tdx_attest::eventlog::read_event_logs().context("Failed to read event logs")?; + Some(TdxQuote { quote, event_log }) } else { - vec![] + None + }; + let tpm_quote = if mode.has_tpm() { + let qualifying_data = ez_hash::sha256(report_data); + let tpm_ctx = tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; + let quote = tpm_ctx + .create_quote(&qualifying_data, &tpm_attest::dstack_pcr_policy()) + .context("Failed to create TPM quote")?; + Some(quote) + } else { + None }; + // TODO: Find a better way handling this hardcode path + let config = + fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default(); Ok(Self { - quote, - raw_event_log, - event_log, + mode, + tdx_quote, + tpm_quote, + config, report: (), }) } + /// Wrap into a versioned attestation for encoding + pub fn into_versioned(self) -> VersionedAttestation { + VersionedAttestation::V0 { attestation: self } + } +} + +impl Attestation { /// Extract attestation data from a certificate pub fn from_cert(cert: &impl CertExt) -> Result> { Self::from_ext_getter(|oid| cert.get_extension_bytes(oid)) @@ -272,12 +506,36 @@ impl Attestation { pub fn from_ext_getter( get_ext: impl Fn(&[u64]) -> Result>>, ) -> Result> { - let quote = match get_ext(oids::PHALA_RATLS_QUOTE)? { - Some(v) => v, - None => return Ok(None), + // Try to detect attestation mode from certificate extension + if let Some(attestation_bytes) = get_ext(oids::PHALA_RATLS_ATTESTATION)? { + let VersionedAttestation::V0 { attestation } = + VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode attestation from cert extension")?; + return Ok(Some(attestation)); + } + // Backward compatibility: if PHALA_RATLS_ATTESTATION + let Some(tdx_quote) = get_ext(oids::PHALA_RATLS_TDX_QUOTE)? else { + return Ok(None); }; - let raw_event_log = get_ext(oids::PHALA_RATLS_EVENT_LOG)?.unwrap_or_default(); - Self::new(quote, raw_event_log).map(Some) + let raw_event_log = + get_ext(oids::PHALA_RATLS_EVENT_LOG)?.context("TDX event log missing")?; + let tdx_event_log = if !raw_event_log.is_empty() { + // Decompress if needed (handles both compressed and uncompressed formats) + serde_json::from_slice(&raw_event_log).context("invalid event log")? + } else { + vec![] + }; + + Ok(Some(Self { + mode: AttestationMode::DstackTdx, + tdx_quote: Some(TdxQuote { + quote: tdx_quote, + event_log: tdx_event_log, + }), + tpm_quote: None, + config: "".into(), + report: (), + })) } /// Extract attestation from x509 certificate @@ -300,23 +558,55 @@ impl Attestation { ra_pubkey_der: &[u8], pccs_url: Option<&str>, ) -> Result { - self.verify( - &QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der), - pccs_url, - ) - .await + let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); + self.verify(Some(expected_report_data), pccs_url).await } /// Verify the quote pub async fn verify( self, - report_data: &[u8; 64], + report_data: Option<[u8; 64]>, pccs_url: Option<&str>, ) -> Result { - let quote = &self.quote; - if &self.decode_report_data()? != report_data { - bail!("report data mismatch"); + let tpm_report = if self.mode.has_tpm() { + let report = self + .verify_tpm() + .await + .context("Failed to verify TPM quote")?; + Some(report) + } else { + None + }; + let tdx_report = if self.mode.has_tdx() { + let report = self.verify_tdx(pccs_url).await?; + Some(report) + } else { + None + }; + let report = DstackVerifiedReport { + tdx_report, + tpm_report, + }; + if report.is_empty() { + bail!("nothing verified"); } + report.ensure_report_data(report_data)?; + Ok(VerifiedAttestation { + mode: self.mode, + tdx_quote: self.tdx_quote, + tpm_quote: self.tpm_quote, + config: self.config, + report, + }) + } + + async fn verify_tpm(&self) -> Result { + let tpm_quote = self.tpm_quote.as_ref().context("TPM quote missing")?; + tpm_qvl::get_collateral_and_verify(tpm_quote).await + } + + async fn verify_tdx(&self, pccs_url: Option<&str>) -> Result { + let quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); if pccs_url.is_empty() { // try to read from PCCS_URL env var @@ -325,32 +615,31 @@ impl Attestation { Err(_) => Cow::Borrowed(""), }; } - let report = qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) - .await - .context("Failed to get collateral")?; - if let Some(report) = report.report.as_td10() { - // Replay the event logs - let rtmrs = self - .replay_event_logs(None) - .context("Failed to replay event logs")?; - if rtmrs != [report.rt_mr0, report.rt_mr1, report.rt_mr2, report.rt_mr3] { - bail!("RTMR mismatch"); - } + let tdx_report = + dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) + .await + .context("Failed to get collateral")?; + let report = tdx_report.report.as_td10().context("TD10 report missing")?; + // Replay the event logs + let rtmr3 = self + .replay_rtmr3(None) + .context("Failed to replay event logs")?; + if rtmr3 != report.rt_mr3 { + bail!( + "RTMR3 mismatch, quoted: {}, replayed: {}", + hex::encode(report.rt_mr3), + hex::encode(rtmr3), + ); } - validate_tcb(&report)?; - Ok(VerifiedAttestation { - quote: self.quote, - raw_event_log: self.raw_event_log, - event_log: self.event_log, - report, - }) + validate_tcb(&tdx_report)?; + Ok(tdx_report) } } -impl Attestation {} +impl Attestation {} /// Validate the TCB attributes -pub fn validate_tcb(report: &VerifiedReport) -> Result<()> { +pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> { fn validate_td10(report: &TDReport10) -> Result<()> { let is_debug = report.td_attributes[0] & 0x01 != 0; if is_debug { @@ -425,34 +714,6 @@ pub struct AppInfo { pub key_provider_info: Vec, } -/// Replay event logs -pub fn replay_event_logs(eventlog: &[EventLog], to_event: Option<&str>) -> Result<[[u8; 48]; 4]> { - let mut rtmrs = [[0u8; 48]; 4]; - for idx in 0..4 { - let mut mr = [0u8; 48]; - - for event in eventlog.iter() { - event - .validate() - .context("Failed to validate event digest")?; - if event.imr == idx { - let mut hasher = Sha384::new(); - hasher.update(mr); - hasher.update(event.digest); - mr = hasher.finalize().into(); - } - if let Some(to_event) = to_event { - if event.event == to_event { - break; - } - } - } - rtmrs[idx as usize] = mr; - } - - Ok(rtmrs) -} - fn sha256(data: &[&[u8]]) -> [u8; 32] { use sha2::{Digest as _, Sha256}; let mut hasher = Sha256::new(); diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 2cfa3e0c0..ae3c224b5 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -14,23 +14,23 @@ use rcgen::{ ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, PublicKeyData, SanType, }; use ring::rand::SystemRandom; -use tdx_attest::eventlog::read_event_logs; -use tdx_attest::get_quote; +use ring::signature::{ + EcdsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_ASN1_SIGNING, +}; +use scale::{Decode, Encode}; use x509_parser::der_parser::Oid; use x509_parser::prelude::{FromDer as _, X509Certificate}; use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; -use crate::attestation::QuoteContentType; -use crate::oids::{PHALA_RATLS_APP_ID, PHALA_RATLS_CERT_USAGE}; -use crate::{ - oids::{PHALA_RATLS_EVENT_LOG, PHALA_RATLS_QUOTE}, - traits::CertExt, +use crate::attestation::{ + Attestation, AttestationMode, QuoteContentType, TdxQuote, VersionedAttestation, }; -use ring::signature::{ - EcdsaKeyPair, UnparsedPublicKey, ECDSA_P256_SHA256_ASN1, ECDSA_P256_SHA256_ASN1_SIGNING, +use crate::oids::{ + PHALA_RATLS_APP_ID, PHALA_RATLS_ATTESTATION, PHALA_RATLS_CERT_USAGE, PHALA_RATLS_EVENT_LOG, + PHALA_RATLS_TDX_QUOTE, }; -use scale::{Decode, Encode}; +use crate::traits::CertExt; /// A CA certificate and private key. pub struct CaCert { @@ -81,13 +81,14 @@ impl CaCert { /// Sign a remote certificate signing request. pub fn sign_csr( &self, - csr: &CertSigningRequest, + csr: &CertSigningRequestV2, app_id: Option<&[u8]>, usage: &str, ) -> Result { let pki = rcgen::SubjectPublicKeyInfo::from_der(&csr.pubkey) .context("Failed to parse signature")?; let cfg = &csr.config; + let attestation = cfg.ext_quote.then_some(&csr.attestation); let req = CertRequest::builder() .key(&pki) .subject(&cfg.subject) @@ -95,8 +96,7 @@ impl CaCert { .alt_names(&cfg.subject_alt_names) .usage_server_auth(cfg.usage_server_auth) .usage_client_auth(cfg.usage_client_auth) - .maybe_quote(cfg.ext_quote.then_some(&csr.quote)) - .maybe_event_log(cfg.ext_quote.then_some(&csr.event_log)) + .maybe_attestation(attestation) .maybe_app_id(app_id) .special_usage(usage) .build(); @@ -122,8 +122,8 @@ pub struct CertConfig { } /// A certificate signing request. -#[derive(Encode, Decode, Clone, PartialEq)] -pub struct CertSigningRequest { +#[derive(Encode, Decode, Clone)] +pub struct CertSigningRequestV1 { /// The confirm word, need to be "please sign cert:" pub confirm: String, /// The public key of the certificate. @@ -136,10 +136,23 @@ pub struct CertSigningRequest { pub event_log: Vec, } -impl CertSigningRequest { - /// Sign the certificate signing request. - pub fn signed_by(&self, key: &KeyPair) -> Result> { - let encoded = self.encode(); +/// A trait for Certificate Signing Request (CSR) operations. +/// +/// This trait provides methods for signing and verifying CSRs using ECDSA P-256 keys. +/// Implementors must provide the data to sign, the public key, and a magic string for validation. +pub trait Csr { + /// Signs the CSR data using the provided key pair. + /// + /// # Arguments + /// * `key` - The ECDSA key pair used to sign the CSR. + /// + /// # Returns + /// The DER-encoded ECDSA signature as a byte vector. + /// + /// # Errors + /// Returns an error if key pair creation or signing fails. + fn signed_by(&self, key: &KeyPair) -> Result> { + let encoded = self.data_to_sign(); let rng = SystemRandom::new(); // Extract the DER-encoded private key and create an ECDSA key pair let key_pair = @@ -155,11 +168,24 @@ impl CertSigningRequest { Ok(signature) } - /// Verify the signature of the certificate signing request. - pub fn verify(&self, signature: &[u8]) -> Result<()> { - let encoded = self.encode(); + /// Verifies the signature of the CSR. + /// + /// # Arguments + /// * `signature` - The signature bytes to verify against the CSR data. + /// + /// # Returns + /// `Ok(())` if the signature is valid and the magic string matches. + /// + /// # Errors + /// Returns an error if: + /// - The public key cannot be parsed + /// - The algorithm is not ECDSA P-256 + /// - The signature is invalid + /// - The magic string does not match "please sign cert:" + fn verify(&self, signature: &[u8]) -> Result<()> { + let encoded = self.data_to_sign(); let (_rem, pki) = - SubjectPublicKeyInfo::from_der(&self.pubkey).context("Failed to parse pubkey")?; + SubjectPublicKeyInfo::from_der(self.pubkey()).context("Failed to parse pubkey")?; let parsed_pki = pki.parsed().context("Failed to parse pki")?; if !matches!(parsed_pki, PublicKey::EC(_)) { bail!("Unsupported algorithm"); @@ -169,16 +195,104 @@ impl CertSigningRequest { key.verify(&encoded, signature) .ok() .context("Invalid signature")?; - if self.confirm != "please sign cert:" { + if self.magic() != "please sign cert:" { bail!("Invalid confirm word"); } Ok(()) } - /// Encode the certificate signing request to a vector. + /// Returns the data that should be signed or verified. + /// + /// Implementors should return the encoded CSR data as a byte vector. + fn data_to_sign(&self) -> Vec; + + /// Returns the public key associated with this CSR. + /// + /// The public key should be in DER-encoded SubjectPublicKeyInfo format. + fn pubkey(&self) -> &[u8]; + + /// Returns the magic string used for validation. + /// + /// This string is checked during verification to ensure the CSR is valid. + /// Expected value: "please sign cert:" + fn magic(&self) -> &str; +} + +impl Csr for CertSigningRequestV1 { + fn data_to_sign(&self) -> Vec { + self.encode() + } + + fn pubkey(&self) -> &[u8] { + &self.pubkey + } + + fn magic(&self) -> &str { + &self.confirm + } +} + +/// A certificate signing request. +#[derive(Encode, Decode, Clone)] +pub struct CertSigningRequestV2 { + /// The confirm word, need to be "please sign cert:" + pub confirm: String, + /// The public key of the certificate. + pub pubkey: Vec, + /// The certificate configuration. + pub config: CertConfig, + /// The attestation. + pub attestation: VersionedAttestation, +} + +impl TryFrom for CertSigningRequestV2 { + type Error = anyhow::Error; + fn try_from(v0: CertSigningRequestV1) -> Result { + Ok(Self { + confirm: v0.confirm, + pubkey: v0.pubkey, + config: v0.config, + attestation: VersionedAttestation::V0 { + attestation: Attestation { + mode: AttestationMode::DstackTdx, + tpm_quote: None, + tdx_quote: Some(TdxQuote { + quote: v0.quote, + event_log: serde_json::from_slice(&v0.event_log) + .context("Failed to parse tdx_event_log")?, + }), + config: "".into(), + report: (), + }, + }, + }) + } +} + +impl Csr for CertSigningRequestV2 { + fn data_to_sign(&self) -> Vec { + self.encode() + } + + fn pubkey(&self) -> &[u8] { + &self.pubkey + } + + fn magic(&self) -> &str { + &self.confirm + } +} + +impl CertSigningRequestV2 { + /// Encodes the certificate signing request into a byte vector. pub fn to_vec(&self) -> Vec { self.encode() } + + /// To attestation + pub fn to_attestation(&self) -> Result { + Ok(self.attestation.clone()) + } } /// Information required to create a certificate. @@ -191,8 +305,7 @@ pub struct CertRequest<'a, Key> { ca_level: Option, app_id: Option<&'a [u8]>, special_usage: Option<&'a str>, - quote: Option<&'a [u8]>, - event_log: Option<&'a [u8]>, + attestation: Option<&'a VersionedAttestation>, not_before: Option, not_after: Option, #[builder(default = false)] @@ -228,20 +341,6 @@ impl CertRequest<'_, Key> { .push(SanType::DnsName(alt_name.clone().try_into()?)); } } - if let Some(quote) = self.quote { - let content = yasna::construct_der(|writer| { - writer.write_bytes(quote); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_QUOTE, content); - params.custom_extensions.push(ext); - } - if let Some(event_log) = self.event_log { - let content = yasna::construct_der(|writer| { - writer.write_bytes(event_log); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_EVENT_LOG, content); - params.custom_extensions.push(ext); - } if let Some(app_id) = self.app_id { let content = yasna::construct_der(|writer| { writer.write_bytes(app_id); @@ -256,6 +355,48 @@ impl CertRequest<'_, Key> { let ext = CustomExtension::from_oid_content(PHALA_RATLS_CERT_USAGE, content); params.custom_extensions.push(ext); } + if let Some(ver_att) = self.attestation { + let mut ver_att = ver_att.clone(); + let VersionedAttestation::V0 { attestation } = &mut ver_att; + if let Some(tdx_quote) = &mut attestation.tdx_quote { + let rtmr3_only: Vec<_> = tdx_quote + .event_log + .iter() + .filter(|e| e.imr == 3) + .cloned() + .collect(); + tdx_quote.event_log = cc_eventlog::strip_event_log_payloads(&rtmr3_only); + } + + match attestation.mode { + AttestationMode::DstackTdx => { + // For backward compatibility, we serialize the quote to the classic oids. + let Some(tdx_quote) = &attestation.tdx_quote else { + bail!("missing tdx quote") + }; + let content = yasna::construct_der(|writer| { + writer.write_bytes(&tdx_quote.quote); + }); + let ext = CustomExtension::from_oid_content(PHALA_RATLS_TDX_QUOTE, content); + params.custom_extensions.push(ext); + let event_log = serde_json::to_vec(&tdx_quote.event_log) + .context("Failed to serialize event log")?; + let content = yasna::construct_der(|writer| { + writer.write_bytes(&event_log); + }); + let ext = CustomExtension::from_oid_content(PHALA_RATLS_EVENT_LOG, content); + params.custom_extensions.push(ext); + } + _ => { + let attestation_bytes = ver_att.to_scale(); + let content = yasna::construct_der(|writer| { + writer.write_bytes(&attestation_bytes); + }); + let ext = CustomExtension::from_oid_content(PHALA_RATLS_ATTESTATION, content); + params.custom_extensions.push(ext); + } + } + } if let Some(ca_level) = self.ca_level { params.is_ca = IsCa::Ca(BasicConstraints::Constrained(ca_level)); params.key_usages.push(KeyUsagePurpose::KeyCertSign); @@ -327,6 +468,50 @@ pub struct CertPair { pub key_pem: String, } +/// Magic prefix for gzip-compressed event log (version 1) +pub const EVENTLOG_GZIP_MAGIC: &[u8] = b"ELGZv1"; + +/// Compress a certificate extension value +pub fn compress_ext_value(data: &[u8]) -> Result> { + use flate2::write::GzEncoder; + use flate2::Compression; + use std::io::Write; + + let mut encoder = GzEncoder::new(Vec::new(), Compression::best()); + encoder + .write_all(data) + .context("failed to write to gzip encoder")?; + let compressed = encoder + .finish() + .context("failed to finish gzip compression")?; + + // Prepend magic prefix + let mut result = Vec::with_capacity(EVENTLOG_GZIP_MAGIC.len() + compressed.len()); + result.extend_from_slice(EVENTLOG_GZIP_MAGIC); + result.extend_from_slice(&compressed); + Ok(result) +} + +/// Decompress a certificate extension value +pub fn decompress_ext_value(data: &[u8]) -> Result> { + use flate2::read::GzDecoder; + use std::io::Read; + + if data.starts_with(EVENTLOG_GZIP_MAGIC) { + // Compressed format + let compressed = &data[EVENTLOG_GZIP_MAGIC.len()..]; + let mut decoder = GzDecoder::new(compressed); + let mut decompressed = Vec::new(); + decoder + .read_to_end(&mut decompressed) + .context("failed to decompress event log")?; + Ok(decompressed) + } else { + // Uncompressed format (backwards compatibility) + Ok(data.to_vec()) + } +} + /// Generate a certificate with RA-TLS quote and event log. pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result { use rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; @@ -335,15 +520,21 @@ pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result Result Date: Fri, 19 Dec 2025 18:43:28 +0800 Subject: [PATCH 025/264] sdk: Add GetAttestation --- sdk/go/dstack/client.go | 35 +++++++++++++++++++ sdk/js/src/get-compose-hash.ts | 4 +-- sdk/js/src/index.ts | 23 ++++++++++++ sdk/python/src/dstack_sdk/__init__.py | 2 ++ sdk/python/src/dstack_sdk/dstack_client.py | 31 ++++++++++++++++ sdk/python/src/dstack_sdk/get_compose_hash.py | 2 +- sdk/rust/src/dstack_client.rs | 12 +++++++ sdk/rust/types/src/dstack.rs | 15 ++++++++ 8 files changed, 121 insertions(+), 3 deletions(-) diff --git a/sdk/go/dstack/client.go b/sdk/go/dstack/client.go index ebff57241..4e8e8efc9 100644 --- a/sdk/go/dstack/client.go +++ b/sdk/go/dstack/client.go @@ -41,6 +41,11 @@ type GetQuoteResponse struct { VmConfig string `json:"vm_config"` } +// Represents the response from an attestation request. +type GetAttestationResponse struct { + Attestation []byte +} + // Represents an event log entry in the TCB info type EventLog struct { IMR int `json:"imr"` @@ -411,6 +416,36 @@ func (c *DstackClient) GetQuote(ctx context.Context, reportData []byte) (*GetQuo }, nil } +// Gets a versioned attestation from the dstack service. +func (c *DstackClient) GetAttestation(ctx context.Context, reportData []byte) (*GetAttestationResponse, error) { + if len(reportData) > 64 { + return nil, fmt.Errorf("report data is too large, it should be at most 64 bytes") + } + + payload := map[string]interface{}{ + "report_data": hex.EncodeToString(reportData), + } + + data, err := c.sendRPCRequest(ctx, "/GetAttestation", payload) + if err != nil { + return nil, err + } + + var response struct { + Attestation string `json:"attestation"` + } + if err := json.Unmarshal(data, &response); err != nil { + return nil, err + } + + attestation, err := hex.DecodeString(response.Attestation) + if err != nil { + return nil, err + } + + return &GetAttestationResponse{Attestation: attestation}, nil +} + // Sends a request to get information about the CVM instance func (c *DstackClient) Info(ctx context.Context) (*InfoResponse, error) { data, err := c.sendRPCRequest(ctx, "/Info", map[string]interface{}{}) diff --git a/sdk/js/src/get-compose-hash.ts b/sdk/js/src/get-compose-hash.ts index 0442b132d..3f3a44ef4 100644 --- a/sdk/js/src/get-compose-hash.ts +++ b/sdk/js/src/get-compose-hash.ts @@ -34,7 +34,7 @@ function sortObject(obj: SortableValue): SortableValue { return obj; } -export type KeyProviderKind = "none" | "kms" | "local"; +export type KeyProviderKind = "none" | "kms" | "local" | "tpm"; export interface DockerConfig extends SortableObject { registry?: string; @@ -105,4 +105,4 @@ export function getComposeHash(app_compose: AppCompose, normalize: boolean = fal } const manifest_str = toDeterministicJson(app_compose); return crypto.createHash("sha256").update(manifest_str, "utf8").digest("hex"); -} \ No newline at end of file +} diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index 6e8e32063..a8c31eeb8 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -97,6 +97,12 @@ export interface GetQuoteResponse { replayRtmrs: () => string[] } +export interface GetAttestationResponse { + __name__: Readonly<'GetAttestationResponse'> + + attestation: Hex +} + export function to_hex(data: string | Buffer | Uint8Array): string { if (typeof data === 'string') { return Buffer.from(data).toString('hex'); @@ -242,6 +248,23 @@ export class DstackClient { return Object.freeze(result) } + async getAttestation(report_data: string | Buffer | Uint8Array): Promise { + let hex = to_hex(report_data) + if (hex.length > 128) { + throw new Error(`Report data is too large, it should be less than 64 bytes.`) + } + const payload = JSON.stringify({ report_data: hex }) + const result = await send_rpc_request<{ attestation: string }>(this.endpoint, '/GetAttestation', payload) + if ('error' in (result as any)) { + const err = (result as any)['error'] as string + throw new Error(err) + } + return Object.freeze({ + __name__: 'GetAttestationResponse', + attestation: result.attestation as Hex, + }) + } + async info(): Promise> { const result = await send_rpc_request, 'tcb_info'> & { tcb_info: string }>(this.endpoint, '/Info', '{}') return Object.freeze({ diff --git a/sdk/python/src/dstack_sdk/__init__.py b/sdk/python/src/dstack_sdk/__init__.py index 2831a17d0..c23457611 100644 --- a/sdk/python/src/dstack_sdk/__init__.py +++ b/sdk/python/src/dstack_sdk/__init__.py @@ -7,6 +7,7 @@ from .dstack_client import DstackClient from .dstack_client import EventLog from .dstack_client import GetKeyResponse +from .dstack_client import GetAttestationResponse from .dstack_client import GetQuoteResponse from .dstack_client import GetTlsKeyResponse from .dstack_client import InfoResponse @@ -32,6 +33,7 @@ # Response types "GetKeyResponse", "GetTlsKeyResponse", + "GetAttestationResponse", "GetQuoteResponse", "InfoResponse", "TcbInfo", diff --git a/sdk/python/src/dstack_sdk/dstack_client.py b/sdk/python/src/dstack_sdk/dstack_client.py index 372c3f556..ea22007bb 100644 --- a/sdk/python/src/dstack_sdk/dstack_client.py +++ b/sdk/python/src/dstack_sdk/dstack_client.py @@ -152,6 +152,13 @@ def replay_rtmrs(self) -> Dict[int, str]: return rtmrs +class GetAttestationResponse(BaseModel): + attestation: str + + def decode_attestation(self) -> bytes: + return bytes.fromhex(self.attestation) + + class SignResponse(BaseModel): signature: str signature_chain: List[str] @@ -378,6 +385,22 @@ async def get_quote( result = await self._send_rpc_request("GetQuote", {"report_data": hex}) return GetQuoteResponse(**result) + async def get_attestation( + self, + report_data: str | bytes, + ) -> GetAttestationResponse: + """Request a versioned attestation for the provided report data.""" + if not report_data or not isinstance(report_data, (bytes, str)): + raise ValueError("report_data can not be empty") + report_bytes: bytes = ( + report_data.encode() if isinstance(report_data, str) else report_data + ) + if len(report_bytes) > 64: + raise ValueError("report_data must be less than 64 bytes") + hex = binascii.hexlify(report_bytes).decode() + result = await self._send_rpc_request("GetAttestation", {"report_data": hex}) + return GetAttestationResponse(**result) + async def info(self) -> InfoResponse[TcbInfo]: """Fetch service information including parsed TCB info.""" result = await self._send_rpc_request("Info", {}) @@ -494,6 +517,14 @@ def get_quote( """Request an attestation quote for the provided report data.""" raise NotImplementedError + @call_async + def get_attestation( + self, + report_data: str | bytes, + ) -> GetAttestationResponse: + """Request a versioned attestation for the provided report data.""" + raise NotImplementedError + @call_async def info(self) -> InfoResponse[TcbInfo]: """Fetch service information including parsed TCB info.""" diff --git a/sdk/python/src/dstack_sdk/get_compose_hash.py b/sdk/python/src/dstack_sdk/get_compose_hash.py index 3839c614f..da5ae4539 100644 --- a/sdk/python/src/dstack_sdk/get_compose_hash.py +++ b/sdk/python/src/dstack_sdk/get_compose_hash.py @@ -17,7 +17,7 @@ from typing import Optional from typing import Union -KeyProviderKind = Literal["none", "kms", "local"] +KeyProviderKind = Literal["none", "kms", "local", "tpm"] class DockerConfig: diff --git a/sdk/rust/src/dstack_client.rs b/sdk/rust/src/dstack_client.rs index 40a5242ae..26d02f275 100644 --- a/sdk/rust/src/dstack_client.rs +++ b/sdk/rust/src/dstack_client.rs @@ -141,6 +141,18 @@ impl DstackClient { Ok(response) } + pub async fn get_attestation(&self, report_data: Vec) -> Result { + if report_data.is_empty() || report_data.len() > 64 { + anyhow::bail!("Invalid report data length") + } + let hex_data = hex_encode(report_data); + let data = json!({ "report_data": hex_data }); + let response = self.send_rpc_request("/GetAttestation", &data).await?; + let response = serde_json::from_value::(response)?; + + Ok(response) + } + pub async fn info(&self) -> Result { let response = self.send_rpc_request("/Info", &json!({})).await?; Ok(InfoResponse::validated_from_value(response)?) diff --git a/sdk/rust/types/src/dstack.rs b/sdk/rust/types/src/dstack.rs index ba151fbc2..d60290d6e 100644 --- a/sdk/rust/types/src/dstack.rs +++ b/sdk/rust/types/src/dstack.rs @@ -113,6 +113,21 @@ pub struct GetQuoteResponse { pub vm_config: String, } +/// Response containing a versioned attestation +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] +pub struct GetAttestationResponse { + /// The attestation in hexadecimal format + pub attestation: String, +} + +impl GetAttestationResponse { + pub fn decode_attestation(&self) -> Result, FromHexError> { + hex::decode(&self.attestation) + } +} + impl GetQuoteResponse { pub fn decode_quote(&self) -> Result, FromHexError> { hex::decode(&self.quote) From 6f837a71892df0ec4690115e447aa0f01ec003bb Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:45:02 +0800 Subject: [PATCH 026/264] verifier: Attestation V2 --- verifier/Cargo.toml | 23 ++- verifier/src/lib.rs | 22 ++ verifier/src/main.rs | 23 +-- verifier/src/types.rs | 13 +- verifier/src/verification.rs | 382 ++++++++++++++++++++++------------- 5 files changed, 299 insertions(+), 164 deletions(-) create mode 100644 verifier/src/lib.rs diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 78706da9c..1256e149c 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -11,18 +11,26 @@ license.workspace = true homepage.workspace = true repository.workspace = true +[lib] +name = "dstack_verifier" +path = "src/lib.rs" + +[[bin]] +name = "dstack-verifier" +path = "src/main.rs" + [dependencies] anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -figment.workspace = true +clap = { workspace = true, features = ["derive"], optional = true } +figment = { workspace = true, optional = true } fs-err.workspace = true hex.workspace = true -rocket = { workspace = true, features = ["json"] } +rocket = { workspace = true, features = ["json"], optional = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true tokio = { workspace = true, features = ["full"] } tracing.workspace = true -tracing-subscriber.workspace = true +tracing-subscriber = { workspace = true, optional = true } reqwest.workspace = true tempfile.workspace = true @@ -35,3 +43,10 @@ dstack-mr.workspace = true dcap-qvl.workspace = true cc-eventlog.workspace = true sha2.workspace = true +tpm-qvl.workspace = true +tpm-types.workspace = true +serde-human-bytes.workspace = true + +[features] +default = ["binary"] +binary = ["dep:clap", "dep:figment", "dep:rocket", "dep:tracing-subscriber"] diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs new file mode 100644 index 000000000..532d17580 --- /dev/null +++ b/verifier/src/lib.rs @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! CVM verification library +//! +//! This library provides functionality to verify Confidential VM (CVM) attestations, +//! including TDX quote verification, event log replay, and OS image hash validation. +//! +//! Can be used both as a library and as a standalone binary/HTTP server. + +mod types; +mod verification; + +// Re-export TdxMeasurements from dstack-mr for convenience +pub use dstack_mr::TdxMeasurements; + +pub use types::{ + AcpiTables, ErrorResponse, RtmrEventEntry, RtmrEventStatus, RtmrMismatch, VerificationDetails, + VerificationRequest, VerificationResponse, +}; +pub use verification::CvmVerifier; diff --git a/verifier/src/main.rs b/verifier/src/main.rs index f145d71d2..79dc9213e 100644 --- a/verifier/src/main.rs +++ b/verifier/src/main.rs @@ -6,6 +6,9 @@ use std::sync::Arc; use anyhow::{Context, Result}; use clap::Parser; +use dstack_verifier::{ + CvmVerifier, VerificationDetails, VerificationRequest, VerificationResponse, +}; use figment::{ providers::{Env, Format, Toml}, Figment, @@ -14,12 +17,6 @@ use rocket::{fairing::AdHoc, get, post, serde::json::Json, State}; use serde::{Deserialize, Serialize}; use tracing::{error, info}; -mod types; -mod verification; - -use types::{VerificationRequest, VerificationResponse}; -use verification::CvmVerifier; - #[derive(Parser)] #[command(name = "dstack-verifier")] #[command(about = "HTTP server providing CVM verification services")] @@ -53,7 +50,7 @@ async fn verify_cvm( error!("Verification failed: {:?}", e); Json(VerificationResponse { is_valid: false, - details: types::VerificationDetails { + details: VerificationDetails { quote_verified: false, event_log_verified: false, os_image_hash_verified: false, @@ -183,14 +180,10 @@ async fn main() -> Result<()> { // Check for oneshot mode if let Some(file_path) = cli.verify { - // Run oneshot verification and exit - let rt = tokio::runtime::Runtime::new().context("Failed to create runtime")?; - rt.block_on(async { - if let Err(e) = run_oneshot(&file_path, &config).await { - error!("Oneshot verification failed: {:#}", e); - std::process::exit(1); - } - }); + if let Err(e) = run_oneshot(&file_path, &config).await { + error!("Oneshot verification failed: {:#}", e); + std::process::exit(1); + } std::process::exit(0); } diff --git a/verifier/src/types.rs b/verifier/src/types.rs index e4e5d2c59..d11024799 100644 --- a/verifier/src/types.rs +++ b/verifier/src/types.rs @@ -5,11 +5,16 @@ use ra_tls::attestation::AppInfo; use serde::{Deserialize, Serialize}; +use serde_human_bytes as serde_bytes; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VerificationRequest { - pub quote: String, - pub event_log: String, - pub vm_config: String, + #[serde(with = "serde_bytes")] + pub quote: Option>, + pub event_log: Option, + pub vm_config: Option, + #[serde(with = "serde_bytes")] + pub attestation: Option>, pub pccs_url: Option, pub debug: Option, } @@ -21,7 +26,7 @@ pub struct VerificationResponse { pub reason: Option, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Default, Serialize)] pub struct VerificationDetails { pub quote_verified: bool, pub event_log_verified: bool, diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 3230865be..35db452c2 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -9,12 +9,14 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; -use cc_eventlog::TdxEventLog as EventLog; +use cc_eventlog::TdxEventLogEntry as EventLog; use dstack_mr::{RtmrLog, TdxMeasurementDetails, TdxMeasurements}; use dstack_types::VmConfig; -use ra_tls::attestation::{Attestation, VerifiedAttestation}; +use ra_tls::attestation::{ + Attestation, AttestationMode, TdxQuote, TpmQuote, VerifiedAttestation, VersionedAttestation, +}; use serde::{Deserialize, Serialize}; -use sha2::{Digest as _, Sha256, Sha384}; +use sha2::{Digest as _, Sha256}; use tokio::{io::AsyncWriteExt, process::Command}; use tracing::{debug, info, warn}; @@ -23,38 +25,6 @@ use crate::types::{ VerificationRequest, VerificationResponse, }; -#[derive(Debug, Clone)] -struct RtmrComputationResult { - event_indices: [Vec; 4], - rtmrs: [[u8; 48]; 4], -} - -fn replay_event_logs(eventlog: &[EventLog]) -> Result { - let mut event_indices: [Vec; 4] = Default::default(); - let mut rtmrs: [[u8; 48]; 4] = [[0u8; 48]; 4]; - - for idx in 0..4 { - for (event_idx, event) in eventlog.iter().enumerate() { - event - .validate() - .context("Failed to validate event digest")?; - - if event.imr == idx { - event_indices[idx as usize].push(event_idx); - let mut hasher = Sha384::new(); - hasher.update(rtmrs[idx as usize]); - hasher.update(event.digest); - rtmrs[idx as usize] = hasher.finalize().into(); - } - } - } - - Ok(RtmrComputationResult { - event_indices, - rtmrs, - }) -} - fn collect_rtmr_mismatch( rtmr_label: &str, expected: &[u8], @@ -76,7 +46,7 @@ fn collect_rtmr_mismatch( } else { event.event.clone() }; - let status = if event.digest == expected_digest.as_slice() { + let status = if event.digest() == expected_digest.as_slice() { RtmrEventStatus::Match } else { RtmrEventStatus::Mismatch @@ -85,7 +55,7 @@ fn collect_rtmr_mismatch( index: idx, event_type: event.event_type, event_name, - actual_digest: hex::encode(event.digest), + actual_digest: hex::encode(event.digest()), expected_digest: Some(hex::encode(expected_digest)), payload_len: event.event_payload.len(), status, @@ -114,7 +84,7 @@ fn collect_rtmr_mismatch( } else { event.event.clone() }, - hex::encode(event.digest), + hex::encode(event.digest()), event.event_payload.len(), ), None => (0, "(missing)".to_string(), String::new(), 0), @@ -156,6 +126,13 @@ struct CachedMeasurement { measurements: TdxMeasurements, } +struct ImagePaths { + fw_path: PathBuf, + kernel_path: PathBuf, + initrd_path: PathBuf, + kernel_cmdline: String, +} + pub struct CvmVerifier { pub image_cache_dir: String, pub download_url: String, @@ -344,17 +321,89 @@ impl CvmVerifier { Ok(measurements) } - pub async fn verify(&self, request: &VerificationRequest) -> Result { - let quote = hex::decode(&request.quote).context("Failed to decode quote hex")?; + /// Helper method to ensure image is downloaded and return image paths + async fn ensure_image_downloaded(&self, vm_config: &VmConfig) -> Result { + let hex_os_image_hash = hex::encode(&vm_config.os_image_hash); - // Event log is always JSON string - let event_log = request.event_log.as_bytes().to_vec(); + // Get image directory + let image_dir = Path::new(&self.image_cache_dir) + .join("images") + .join(&hex_os_image_hash); - let attestation = Attestation::new(quote, event_log) - .context("Failed to create attestation from quote and event log")?; + let metadata_path = image_dir.join("metadata.json"); + if !metadata_path.exists() { + info!("Image {hex_os_image_hash} not found, downloading"); + tokio::time::timeout( + self.download_timeout, + self.download_image(&hex_os_image_hash, &image_dir), + ) + .await + .context("Download image timeout")? + .with_context(|| format!("Failed to download image {hex_os_image_hash}"))?; + } - let debug = request.debug.unwrap_or(false); + let image_info = + fs_err::read_to_string(metadata_path).context("Failed to read image metadata")?; + let image_info: dstack_types::ImageInfo = + serde_json::from_str(&image_info).context("Failed to parse image metadata")?; + + let fw_path = image_dir.join(&image_info.bios); + let kernel_path = image_dir.join(&image_info.kernel); + let initrd_path = image_dir.join(&image_info.initrd); + let kernel_cmdline = image_info.cmdline + " initrd=initrd"; + + Ok(ImagePaths { + fw_path, + kernel_path, + initrd_path, + kernel_cmdline, + }) + } + + /// Compute expected TDX measurements for a given VM configuration. + /// + /// This method downloads the OS image if needed (using the configured cache), + /// then computes the expected MRTD and RTMRs based on the VM configuration. + /// Results are cached automatically. + pub async fn compute_measurements_for_config( + &self, + vm_config: &VmConfig, + ) -> Result { + let image_paths = self.ensure_image_downloaded(vm_config).await?; + + self.load_or_compute_measurements( + vm_config, + &image_paths.fw_path, + &image_paths.kernel_path, + &image_paths.initrd_path, + &image_paths.kernel_cmdline, + ) + } + pub async fn verify(&self, request: &VerificationRequest) -> Result { + let attestation = if let Some(attestation) = &request.attestation { + VersionedAttestation::from_scale(attestation).context("Failed to decode attestaion")? + } else if let Some(tpm_quote) = &request.quote { + let event_log = request + .event_log + .as_ref() + .context("Event log is required")?; + let event_log = + serde_json::from_str(event_log).context("Failed to decode event log")?; + Attestation { + mode: AttestationMode::DstackTdx, + tdx_quote: Some(TdxQuote { + quote: tpm_quote.to_vec(), + event_log, + }), + tpm_quote: None, + config: "".into(), + report: (), + } + .into_versioned() + } else { + bail!("Quote is required"); + }; let mut details = VerificationDetails { quote_verified: false, event_log_verified: false, @@ -367,41 +416,53 @@ impl CvmVerifier { rtmr_debug: None, }; - let vm_config: VmConfig = - serde_json::from_str(&request.vm_config).context("Failed to decode VM config JSON")?; - - // Step 1: Verify the TDX quote using dcap-qvl - let verified_attestation = match self.verify_quote(attestation, &request.pccs_url).await { + let attestation = attestation.into_inner(); + let debug = request.debug.unwrap_or(false); + let verified = attestation.verify(None, request.pccs_url.as_deref()).await; + let verified_attestation = match verified { Ok(att) => { details.quote_verified = true; - details.tcb_status = Some(att.report.status.clone()); - details.advisory_ids = att.report.advisory_ids.clone(); - // Extract and store report_data - if let Ok(report_data) = att.decode_report_data() { - details.report_data = Some(hex::encode(report_data)); - } + details.tcb_status = att.report.tdx_report.as_ref().map(|r| r.status.clone()); + details.advisory_ids = att + .report + .tdx_report + .as_ref() + .map(|r| r.advisory_ids.clone()) + .unwrap_or_default(); + let report_data = att + .report + .ensure_report_data(None) + .context("Failed to decode report data")?; + details.report_data = Some(hex::encode(report_data)); att } Err(e) => { return Ok(VerificationResponse { is_valid: false, details, - reason: Some(format!("Quote verification failed: {}", e)), + reason: Some(format!("Quote verification failed: {e:#}")), }); } }; - // Step 3: Verify os-image-hash matches using dstack-mr - if let Err(e) = self - .verify_os_image_hash(&vm_config, &verified_attestation, debug, &mut details) - .await - { - return Ok(VerificationResponse { - is_valid: false, - details, - reason: Some(format!("OS image hash verification failed: {e:#}")), - }); - } + let verified = self + .verify_os_image_hash( + request.vm_config.clone().unwrap_or_default(), + &verified_attestation, + debug, + &mut details, + ) + .await; + let vm_config = match verified { + Ok(vm_config) => vm_config, + Err(e) => { + return Ok(VerificationResponse { + is_valid: false, + details, + reason: Some(format!("OS image hash verification failed: {e:#}")), + }); + } + }; details.os_image_hash_verified = true; match verified_attestation.decode_app_info(false) { Ok(mut info) => { @@ -425,32 +486,51 @@ impl CvmVerifier { }) } - async fn verify_quote( + pub async fn verify_os_image_hash( &self, - attestation: Attestation, - pccs_url: &Option, - ) -> Result { - // Extract report data from quote - let report_data = attestation.decode_report_data()?; - - attestation - .verify(&report_data, pccs_url.as_deref()) - .await - .context("Quote verification failed") + mut vm_config: String, + attestation: &VerifiedAttestation, + debug: bool, + details: &mut VerificationDetails, + ) -> Result { + if vm_config.is_empty() { + vm_config = attestation.config.clone() + } + let vm_config: VmConfig = + serde_json::from_str(&vm_config).context("Failed to decode VM config JSON")?; + match attestation.mode { + AttestationMode::GcpTdx => { + let Some(tpm_quote) = &attestation.tpm_quote else { + bail!("No TPM quote"); + }; + self.verify_os_image_hash_for_gcp_tdx(&vm_config, tpm_quote) + .await?; + } + AttestationMode::DstackTdx => { + self.verify_os_image_hash_for_dstack_tdx(&vm_config, attestation, debug, details) + .await?; + } + AttestationMode::DstackNitro => bail!("Nitro not supported"), + } + Ok(vm_config) } - async fn verify_os_image_hash( + async fn verify_os_image_hash_for_dstack_tdx( &self, vm_config: &VmConfig, attestation: &VerifiedAttestation, debug: bool, details: &mut VerificationDetails, ) -> Result<()> { - let hex_os_image_hash = hex::encode(&vm_config.os_image_hash); - + let Some(report) = &attestation.report.tdx_report else { + bail!("No TDX report"); + }; + let Some(tdx_quote) = &attestation.tdx_quote else { + bail!("No TDX quote"); + }; + let event_log = &tdx_quote.event_log; // Get boot info from attestation - let report = attestation - .report + let report = report .report .as_td10() .context("Failed to decode TD report")?; @@ -463,35 +543,11 @@ impl CvmVerifier { rtmr2: report.rt_mr2.to_vec(), }; - // Get image directory - let image_dir = Path::new(&self.image_cache_dir) - .join("images") - .join(&hex_os_image_hash); - - let metadata_path = image_dir.join("metadata.json"); - if !metadata_path.exists() { - info!("Image {} not found, downloading", hex_os_image_hash); - tokio::time::timeout( - self.download_timeout, - self.download_image(&hex_os_image_hash, &image_dir), - ) - .await - .context("Download image timeout")? - .with_context(|| format!("Failed to download image {hex_os_image_hash}"))?; - } - - let image_info = - fs_err::read_to_string(metadata_path).context("Failed to read image metadata")?; - let image_info: dstack_types::ImageInfo = - serde_json::from_str(&image_info).context("Failed to parse image metadata")?; - - let fw_path = image_dir.join(&image_info.bios); - let kernel_path = image_dir.join(&image_info.kernel); - let initrd_path = image_dir.join(&image_info.initrd); - let kernel_cmdline = image_info.cmdline + " initrd=initrd"; - - // Use dstack-mr to compute expected MRs + // Compute expected measurements (reusing the public API) let (mrs, expected_logs) = if debug { + // For debug mode, we need detailed logs and ACPI tables + let image_paths = self.ensure_image_downloaded(vm_config).await?; + let TdxMeasurementDetails { measurements, rtmr_logs, @@ -499,10 +555,10 @@ impl CvmVerifier { } = self .compute_measurement_details( vm_config, - &fw_path, - &kernel_path, - &initrd_path, - &kernel_cmdline, + &image_paths.fw_path, + &image_paths.kernel_path, + &image_paths.initrd_path, + &image_paths.kernel_cmdline, ) .context("Failed to compute expected measurements")?; @@ -514,15 +570,11 @@ impl CvmVerifier { (measurements, Some(rtmr_logs)) } else { + // For non-debug mode, reuse the public API with caching ( - self.load_or_compute_measurements( - vm_config, - &fw_path, - &kernel_path, - &initrd_path, - &kernel_cmdline, - ) - .context("Failed to obtain expected measurements")?, + self.compute_measurements_for_config(vm_config) + .await + .context("Failed to compute expected measurements")?, None, ) }; @@ -534,16 +586,6 @@ impl CvmVerifier { rtmr2: mrs.rtmr2.clone(), }; - let event_log: Vec = serde_json::from_slice(&attestation.raw_event_log) - .context("Failed to parse event log for mismatch analysis")?; - - let computation_result = replay_event_logs(&event_log) - .context("Failed to replay event logs for mismatch analysis")?; - - if computation_result.rtmrs[3] != report.rt_mr3 { - bail!("RTMR3 mismatch"); - } - match expected_mrs.assert_eq(&verified_mrs) { Ok(()) => Ok(()), Err(e) => { @@ -562,8 +604,8 @@ impl CvmVerifier { &expected_mrs.rtmr0, &verified_mrs.rtmr0, &expected_logs[0], - &computation_result.event_indices[0], - &event_log, + &[], + event_log, )); } @@ -573,8 +615,8 @@ impl CvmVerifier { &expected_mrs.rtmr1, &verified_mrs.rtmr1, &expected_logs[1], - &computation_result.event_indices[1], - &event_log, + &[], + event_log, )); } @@ -584,8 +626,8 @@ impl CvmVerifier { &expected_mrs.rtmr2, &verified_mrs.rtmr2, &expected_logs[2], - &computation_result.event_indices[2], - &event_log, + &[], + event_log, )); } @@ -598,7 +640,65 @@ impl CvmVerifier { } } - async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { + /// Verify GCP TDX image hash using PCR 2 Event Log + /// + /// For GCP TDX, we verify: + /// 1. PCR 0 matches expected GCP OVMF v2 firmware + /// 2. UKI hash by extracting Event 28 (3rd event in PCR 2) from TPM Event Log + /// + /// IMPORTANT: Extracting the 3rd event is GCP OVMF-specific behavior. + /// On GCP, PCR 2 events are ordered as: + /// [0]=EV_SEPARATOR, [1]=EV_EFI_GPT_EVENT, [2]=UKI (Event 28), [3]=Linux kernel + async fn verify_os_image_hash_for_gcp_tdx( + &self, + vm_config: &VmConfig, + tpm_quote: &TpmQuote, + ) -> Result<()> { + // Verify PCR 0 (GCP OVMF firmware) + const EXPECTED_PCR0: &str = + "0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802"; + + let pcr0 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 0) + .context("PCR 0 not found in TPM quote")?; + + let pcr0_hex = hex::encode(&pcr0.value); + + // Get expected UKI hash from os_image_hash (which should be set to UKI Authenticode hash) + let expected_uki_hash = &vm_config.os_image_hash; + + let pcr2_events: Vec<_> = tpm_quote + .event_log + .iter() + .filter(|e| e.pcr_index == 2) + .collect(); + debug!("PCR 2 Event Log contains {} events", pcr2_events.len()); + // Extract Event 28 (3rd event, 0-indexed as 2) + // NOTE: This is GCP OVMF-specific behavior + let event_28_digest = { + if pcr0_hex != EXPECTED_PCR0 { + bail!("PCR 0 mismatch: expected GCP OVMF v2 ({EXPECTED_PCR0}), got {pcr0_hex}"); + } + &pcr2_events.get(2).context("Event 28 not found")?.digest + }; + + if event_28_digest != expected_uki_hash { + bail!( + "UKI hash mismatch: expected={}, actual={}", + hex::encode(expected_uki_hash), + hex::encode(event_28_digest) + ); + } + debug!( + "✓ UKI hash verified from PCR 2 Event Log (Event 28), digest: {}", + hex::encode(event_28_digest) + ); + Ok(()) + } + + pub async fn download_image(&self, hex_os_image_hash: &str, dst_dir: &Path) -> Result<()> { let url = self .download_url .replace("{OS_IMAGE_HASH}", hex_os_image_hash); From c56c5da9e46c93548a8581daa84505f67e3a3021 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:18:50 +0800 Subject: [PATCH 027/264] dstack-util: Add debug commands --- Cargo.toml | 1 + dstack-util/src/main.rs | 576 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 548 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 406bf9347..cdb3bef36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -186,6 +186,7 @@ xsalsa20poly1305 = "0.9.0" salsa20 = "0.10" rand_core = "0.6.4" alloy = { version = "1.0.32", default-features = false } +ez-hash = "1.0.0" # Certificate/DNS hickory-resolver = "0.24.4" diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index fcaec5244..efa47343b 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -9,6 +9,7 @@ use fs_err as fs; use getrandom::fill as getrandom; use host_api::HostApi; use k256::schnorr::SigningKey; +use ra_rpc::Attestation; use ra_tls::{ attestation::QuoteContentType, cert::generate_ra_cert, @@ -16,8 +17,7 @@ use ra_tls::{ rcgen::KeyPair, }; use scale::Decode; -use serde::Deserialize; -use std::{collections::HashMap, path::Path}; +use std::path::Path; use std::{ io::{self, Read, Write}, path::PathBuf, @@ -43,14 +43,16 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Get TDX report given report data from stdin - Report, /// Generate a TDX quote given report data from stdin Quote, + /// Get TDX event logs + Eventlog, /// Extend RTMRs Extend(ExtendArgs), /// Show the current RTMR state Show, + /// Replay event log and show calculated IMR/RTMR values + ReplayImr, /// Hex encode data Hex(HexCommand), /// Generate a RA-TLS certificate @@ -69,6 +71,13 @@ enum Commands { NotifyHost(HostNotifyArgs), /// Remove orphaned containers RemoveOrphans(RemoveOrphansArgs), + /// Perform vTPM attestation (for GCP TEE instances) + VtpmAttest(VtpmAttestArgs), + /// Generate a TPM quote + TpmQuote(TpmQuoteArgs), + /// Verify a TPM quote + TpmVerify(TpmVerifyArgs), + QuoteReport(QuoteReportArgs), } #[derive(Parser)] @@ -198,35 +207,137 @@ struct RemoveOrphansArgs { docker_root: String, } +#[derive(Parser)] +/// Perform vTPM attestation +struct VtpmAttestArgs { + /// path to Root CA certificate (PEM format) + #[arg(long)] + root_ca: PathBuf, + + /// nonce for replay protection + #[arg(long)] + nonce: String, + + /// expected OS image SHA256 hash (optional) + #[arg(long)] + expected_os_hash: Option, + + /// key algorithm (rsa or ecc, default: rsa) + #[arg(long, default_value = "rsa")] + key_algo: String, + + /// output format (json or text, default: text) + #[arg(long, default_value = "text")] + format: String, +} + +#[derive(Parser)] +/// Generate a TPM quote +struct TpmQuoteArgs { + /// qualifying data (hex encoded, default: 32 zeros) + #[arg(short, long)] + data: Option, + + /// output file (default: stdout) + #[arg(short, long)] + output: Option, + + /// key algorithm (auto, ecc, or rsa; default: auto) + #[arg(short = 'k', long, default_value = "auto")] + key_algo: String, + + /// The hash algorithm to use (default: none) + #[arg(short = 'H', long, default_value = "none")] + hash_algo: String, +} + +#[derive(Parser)] +/// Verify a TPM quote +struct TpmVerifyArgs { + /// path to Root CA certificate (PEM format) + #[arg(long)] + root_ca: PathBuf, + + /// path to TPM quote JSON file + #[arg(short, long)] + quote: PathBuf, +} + +#[derive(Parser)] +struct QuoteReportArgs { + #[arg(long)] + report_data: Option, + + #[arg(long, default_value = "/dstack/.host-shared/.sys-config.json")] + sys_config: PathBuf, + + #[arg(short, long)] + output: Option, + + #[arg(long, default_value_t = false)] + debug: bool, +} + +fn cmd_quote_report(args: QuoteReportArgs) -> Result<()> { + #[derive(serde::Serialize)] + struct VerificationRequestJson { + pub attestation: String, + } + + fn pad64(data: &[u8]) -> Result<[u8; 64]> { + if data.len() > 64 { + anyhow::bail!("report_data must be at most 64 bytes"); + } + let mut out = [0u8; 64]; + out[..data.len()].copy_from_slice(data); + Ok(out) + } + + let report_data = match args.report_data { + Some(hex_data) => { + pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? + } + None => [0u8; 64], + }; + let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + let request = VerificationRequestJson { + attestation: hex::encode(attestation.into_versioned().to_scale()), + }; + + let json = + serde_json::to_string_pretty(&request).context("Failed to serialize request JSON")?; + if let Some(output_path) = args.output { + fs::write(&output_path, json).context("Failed to write quote report")?; + } else { + println!("{json}"); + } + Ok(()) +} + fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; io::stdin() .read_exact(&mut report_data) .context("Failed to read report data")?; - let (_key_id, quote) = att::get_quote(&report_data, None).context("Failed to get quote")?; + let quote = att::get_quote(&report_data).context("Failed to get quote")?; io::stdout() .write_all("e) .context("Failed to write quote")?; Ok(()) } +fn cmd_eventlog() -> Result<()> { + let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; + serde_json::to_writer_pretty(io::stdout(), &event_logs) + .context("Failed to write event logs")?; + Ok(()) +} + fn cmd_extend(extend_args: ExtendArgs) -> Result<()> { let payload = hex::decode(&extend_args.payload).context("Failed to decode payload")?; att::extend_rtmr3(&extend_args.event, &payload).context("Failed to extend RTMR") } -fn cmd_report() -> Result<()> { - let mut report_data = [0; 64]; - io::stdin() - .read_exact(&mut report_data) - .context("Failed to read report data")?; - let report = att::get_report(&report_data).context("Failed to get report")?; - io::stdout() - .write_all(&report.0) - .context("Failed to write report")?; - Ok(()) -} - fn cmd_rand(rand_args: RandArgs) -> Result<()> { let mut data = vec![0u8; rand_args.bytes]; getrandom(&mut data).context("Failed to generate random data")?; @@ -285,6 +396,57 @@ fn cmd_show_mrs() -> Result<()> { Ok(()) } +fn cmd_replay_imr() -> Result<()> { + use sha2::Digest; + + println!("=== Event Log Replay: Calculated IMR/RTMR Values ===\n"); + + // Read and replay event logs + let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; + + println!("Total events: {}", event_logs.len()); + + // Count events per IMR + let mut imr_counts = [0u32; 4]; + for event in &event_logs { + if event.imr < 4 { + imr_counts[event.imr as usize] += 1; + } + } + + println!("Event distribution:"); + for (idx, count) in imr_counts.iter().enumerate() { + println!(" IMR {}: {} events", idx, count); + } + println!(); + + // Replay event logs to calculate IMR/RTMR values + println!("Replaying event log..."); + let mut rtmrs: [[u8; 48]; 4] = [[0u8; 48]; 4]; + + for event in &event_logs { + if event.imr < 4 { + let mut hasher = sha2::Sha384::new(); + hasher.update(rtmrs[event.imr as usize]); + hasher.update(event.digest()); + rtmrs[event.imr as usize] = hasher.finalize().into(); + } + } + + println!("\nCalculated IMR/RTMR values from event log replay:\n"); + println!("IMR 0 (CCEL) → {}", hex::encode(rtmrs[0])); + println!("IMR 1 (CCEL) → {}", hex::encode(rtmrs[1])); + println!("IMR 2 (CCEL) → {}", hex::encode(rtmrs[2])); + println!("IMR 3 (CCEL) → {}", hex::encode(rtmrs[3])); + + println!("\n========================================"); + println!("Note: These are the calculated values from replaying the CCEL event log."); + println!("The mapping between CCEL IMR indices and TDX RTMR indices may vary"); + println!("depending on the platform implementation."); + + Ok(()) +} + fn cmd_hex(hex_args: HexCommand) -> Result<()> { fn hex_encode_io(io: &mut impl Read) -> Result<()> { loop { @@ -323,14 +485,13 @@ fn cmd_gen_ca_cert(args: GenCaCertArgs) -> Result<()> { let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?; let pubkey = key.public_key_der(); let report_data = QuoteContentType::KmsRootCa.to_report_data(&pubkey); - let (_, quote) = att::get_quote(&report_data, None).context("Failed to get quote")?; - let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; - let event_log = serde_json::to_vec(&event_logs).context("Failed to serialize event logs")?; + let attestation = Attestation::quote(&report_data) + .context("Failed to get attestation")? + .into_versioned(); let req = CertRequest::builder() .subject("App Root CA") - .quote("e) - .event_log(&event_log) + .attestation(&attestation) .key(&key) .ca_level(args.ca_level) .build(); @@ -396,14 +557,13 @@ fn make_app_keys( use ra_tls::cert::CertRequest; let pubkey = app_key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let (_, quote) = att::get_quote(&report_data, None).context("Failed to get quote")?; - let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; - let event_log = serde_json::to_vec(&event_logs).context("Failed to serialize event logs")?; + let attestation = Attestation::quote(&report_data) + .context("Failed to get attestation")? + .into_versioned(); let req = CertRequest::builder() .subject("App Root Cert") - .quote("e) - .event_log(&event_log) - .key(&app_key) + .attestation(&attestation) + .key(app_key) .ca_level(ca_level) .build(); let cert = req @@ -434,6 +594,351 @@ fn sha256(data: &[u8]) -> [u8; 32] { sha256.finalize().into() } +fn cmd_vtpm_attest(args: VtpmAttestArgs) -> Result<()> { + use cmd_lib::run_cmd; + use serde::Serialize; + + #[derive(Serialize)] + struct AttestationResult { + success: bool, + ek_cert_verified: bool, + quote_verified: bool, + os_image_verified: Option, + nonce: String, + key_algorithm: String, + error: Option, + } + + // verify root CA file exists + if !args.root_ca.exists() { + anyhow::bail!("root CA file not found: {:?}", args.root_ca); + } + + // verify key algorithm + let (ek_algo, ak_algo, ak_scheme, algo_name) = match args.key_algo.to_lowercase().as_str() { + "rsa" => ("rsa", "rsa", "rsassa", "RSA-2048"), + "ecc" | "ecdsa" => ("ecc", "ecc", "ecdsa", "ECC P-256"), + _ => anyhow::bail!( + "invalid key algorithm: {}. Use 'rsa' or 'ecc'", + args.key_algo + ), + }; + + let mut result = AttestationResult { + success: false, + ek_cert_verified: false, + quote_verified: false, + os_image_verified: None, + nonce: args.nonce.clone(), + key_algorithm: algo_name.to_string(), + error: None, + }; + + let attestation_result = (|| -> Result<()> { + if args.format == "text" { + println!("=== vTPM Attestation ==="); + println!("Root CA: {:?}", args.root_ca); + println!("Nonce: {}", args.nonce); + println!("Key Algorithm: {}", algo_name); + println!(); + } + + // step 1: extract EK certificate + if args.format == "text" { + println!("[1/7] extracting EK certificate..."); + } + run_cmd! { + tpm2_nvread -o /tmp/ek_cert.der 0x1c00002 2>/dev/null; + openssl x509 -inform DER -in /tmp/ek_cert.der -out /tmp/ek_cert.pem 2>/dev/null; + } + .context("failed to extract EK certificate")?; + + // step 2: extract intermediate CA URL + if args.format == "text" { + println!("[2/7] downloading intermediate CA..."); + } + let ica_url_output = std::process::Command::new("openssl") + .args(["x509", "-in", "/tmp/ek_cert.pem", "-noout", "-text"]) + .output() + .context("failed to read EK cert")?; + let ica_text = String::from_utf8_lossy(&ica_url_output.stdout); + let ica_url = ica_text + .lines() + .find(|l| l.contains("CA Issuers") && l.contains("URI:")) + .and_then(|l| l.split("URI:").nth(1)) + .map(|s| s.trim()) + .context("failed to find Intermediate CA URL")?; + + run_cmd! { + curl -s -o /tmp/intermediate_ca.crt $ica_url; + } + .context("failed to download intermediate CA")?; + + // try DER first, then PEM + let convert_result = run_cmd! { + openssl x509 -inform DER -in /tmp/intermediate_ca.crt -outform PEM -out /tmp/intermediate_ca.pem 2>/dev/null; + }; + if convert_result.is_err() { + run_cmd! { + openssl x509 -inform PEM -in /tmp/intermediate_ca.crt -outform PEM -out /tmp/intermediate_ca.pem 2>/dev/null; + } + .context("failed to convert intermediate CA")?; + } + + // step 3: verify intermediate CA + if args.format == "text" { + println!("[3/7] verifying certificate chain..."); + } + let root_ca_path = args.root_ca.to_str().context("invalid root CA path")?; + run_cmd! { + openssl verify -CAfile $root_ca_path /tmp/intermediate_ca.pem >/dev/null 2>&1; + } + .context("intermediate CA verification failed")?; + + // step 4: verify EK certificate + run_cmd! { + cat /tmp/intermediate_ca.pem $root_ca_path > /tmp/ca_chain.pem; + openssl verify -CAfile /tmp/ca_chain.pem /tmp/ek_cert.pem >/dev/null 2>&1; + } + .context("EK certificate verification failed")?; + result.ek_cert_verified = true; + + // step 5: create AK + if args.format == "text" { + println!("[4/7] creating attestation key ({})...", algo_name); + } + run_cmd! { + tpm2_createek -c /tmp/ek.ctx -G $ek_algo -u /tmp/ek.pub >/dev/null 2>&1; + tpm2_createak -C /tmp/ek.ctx -c /tmp/ak.ctx -G $ak_algo -g sha256 -s $ak_scheme -u /tmp/ak.pub -n /tmp/ak.name >/dev/null 2>&1; + } + .context("failed to create attestation key")?; + + // step 6: generate quote + if args.format == "text" { + println!("[5/7] generating TPM quote..."); + } + let nonce = &args.nonce; + run_cmd! { + echo -n $nonce > /tmp/nonce.bin; + tpm2_quote -c /tmp/ak.ctx -l sha256:0,1,2,3,4,5,6,7,8,9,10,14 -q /tmp/nonce.bin -m /tmp/quote.msg -s /tmp/quote.sig -o /tmp/quote.pcr -g sha256 >/dev/null 2>&1; + } + .context("failed to generate quote")?; + + // step 7: verify quote + if args.format == "text" { + println!("[6/7] verifying quote signature..."); + } + run_cmd! { + tpm2_checkquote -u /tmp/ak.pub -m /tmp/quote.msg -s /tmp/quote.sig -f /tmp/quote.pcr -g sha256 -q /tmp/nonce.bin >/dev/null 2>&1; + } + .context("quote verification failed")?; + result.quote_verified = true; + + // step 8: verify OS image (optional) + if let Some(expected_hash) = &args.expected_os_hash { + if args.format == "text" { + println!("[7/7] verifying OS image..."); + } + let tpm_eventlog_path = "/sys/kernel/security/tpm0/binary_bios_measurements"; + if Path::new(tpm_eventlog_path).exists() { + let _ = run_cmd! { + tpm2_eventlog $tpm_eventlog_path > /tmp/eventlog.yaml 2>/dev/null; + }; + + let eventlog = fs::read_to_string("/tmp/eventlog.yaml").unwrap_or_default(); + if eventlog.contains(expected_hash) { + result.os_image_verified = Some(true); + } else { + result.os_image_verified = Some(false); + anyhow::bail!("OS image hash mismatch"); + } + } + } + + result.success = true; + Ok(()) + })(); + + if let Err(e) = attestation_result { + result.error = Some(format!("{:#}", e)); + } + + if args.format == "json" { + println!("{}", serde_json::to_string_pretty(&result)?); + } else { + println!(); + println!("=== Attestation Result ==="); + println!( + " EK Certificate Chain: {}", + if result.ek_cert_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!( + " TPM Quote: {}", + if result.quote_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + if let Some(os_verified) = result.os_image_verified { + println!( + " OS Image: {}", + if os_verified { + "✓ VERIFIED" + } else { + "✗ MISMATCH" + } + ); + } + println!(); + if result.success { + println!("🎉 ATTESTATION PASSED"); + } else { + println!("❌ ATTESTATION FAILED"); + if let Some(error) = &result.error { + println!("Error: {}", error); + } + anyhow::bail!("attestation failed"); + } + } + + Ok(()) +} + +fn cmd_tpm_quote(args: TpmQuoteArgs) -> Result<()> { + let data = if let Some(hex_data) = args.data { + let decoded = hex::decode(&hex_data).context("Failed to decode hex data")?; + if decoded.len() > 64 { + anyhow::bail!("Qualifying data must be at most 64 bytes"); + } + decoded + } else { + vec![0u8; 32] // TPM 2.0 max qualifying data is 32 bytes + }; + + // Parse key algorithm + let key_algo = args + .key_algo + .parse::() + .context("Failed to parse key algorithm")?; + + let qualifying_data: [u8; 32] = match args.hash_algo.as_str() { + "none" => data + .try_into() + .ok() + .context("qualifying data must be 32 bytes")?, + "sha256" => ez_hash::sha256(&data), + _ => { + anyhow::bail!("Unsupported hash algorithm"); + } + }; + + let tpm = tpm_attest::TpmContext::open(None).context("Failed to open TPM context")?; + let pcr_selection = tpm_attest::dstack_pcr_policy(); + let tpm_quote = tpm + .create_quote_with_algo(&qualifying_data, &pcr_selection, key_algo) + .context("Failed to create TPM quote")?; + + let quote_json = + serde_json::to_string_pretty(&tpm_quote).context("Failed to serialize TPM quote")?; + + if let Some(output_path) = args.output { + fs::write(&output_path, quote_json).context("Failed to write quote to file")?; + eprintln!("TPM quote written to: {:?}", output_path); + } else { + println!("{}", quote_json); + } + + Ok(()) +} + +async fn cmd_tpm_verify(args: TpmVerifyArgs) -> Result<()> { + let root_ca_pem = fs::read_to_string(&args.root_ca).context("Failed to read root CA")?; + let quote_json = fs::read_to_string(&args.quote).context("Failed to read quote file")?; + let tpm_quote: tpm_attest::TpmQuote = + serde_json::from_str("e_json).context("Failed to parse quote JSON")?; + + println!("=== TPM Quote Verification (dcap-qvl architecture) ==="); + println!("Root CA: {:?}", args.root_ca); + println!("Quote file: {:?}", args.quote); + println!(); + + // Step 1: Get collateral (certificates + CRLs) + println!("[Step 1] Fetching quote collateral (certificates + CRLs)..."); + let collateral = tpm_qvl::get_collateral(&tpm_quote, &root_ca_pem) + .await + .context("Failed to get collateral")?; + let crl_count = collateral.crls.len() + + if collateral.root_ca_crl.is_some() { + 1 + } else { + 0 + }; + println!(" ✓ Collateral fetched: {} CRLs downloaded", crl_count); + println!(); + + // Step 2: Verify quote with conditional CRL checking + println!("[Step 2] Verifying quote (CRL verification if CRL DP present)..."); + + match tpm_qvl::verify::verify_quote_with_ca(&tpm_quote, &collateral, &root_ca_pem) { + Ok(_) => { + // Success - print simple success message + println!(); + let crl_count = collateral.crls.len() + + if collateral.root_ca_crl.is_some() { + 1 + } else { + 0 + }; + if crl_count == 0 { + println!("🎉 VERIFICATION PASSED (no CRLs available)"); + } else { + println!( + "🎉 VERIFICATION PASSED (with {} CRL(s) verified)", + crl_count + ); + } + Ok(()) + } + Err(verification_result) => { + // Failure - print detailed status + println!(); + println!("=== Verification Result ==="); + println!( + " AK Certificate Chain (webpki + CRL): {}", + if verification_result.status.ak_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!( + " Quote Signature: {}", + if verification_result.status.signature_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!( + " PCR Values: {}", + if verification_result.status.pcr_verified { + "✓ VERIFIED" + } else { + "✗ FAILED" + } + ); + println!(" Error: {}", verification_result.error); + println!(); + anyhow::bail!("Verification failed") + } + } +} + #[tokio::main] async fn main() -> Result<()> { { @@ -445,9 +950,10 @@ async fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Commands::Report => cmd_report()?, Commands::Quote => cmd_quote()?, + Commands::Eventlog => cmd_eventlog()?, Commands::Show => cmd_show_mrs()?, + Commands::ReplayImr => cmd_replay_imr()?, Commands::Extend(extend_args) => { cmd_extend(extend_args)?; } @@ -486,6 +992,18 @@ async fn main() -> Result<()> { docker_compose::remove_orphans(args.compose, args.dry_run).await?; } } + Commands::VtpmAttest(args) => { + cmd_vtpm_attest(args)?; + } + Commands::TpmQuote(args) => { + cmd_tpm_quote(args)?; + } + Commands::TpmVerify(args) => { + cmd_tpm_verify(args).await?; + } + Commands::QuoteReport(args) => { + cmd_quote_report(args)?; + } } Ok(()) From 01f7c5f63060e4489d26a36125db2a5143e078f2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:45:48 +0800 Subject: [PATCH 028/264] mod tdx_guest: Compile on kernel 6.17 --- mod-tdx-guest/mod.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod-tdx-guest/mod.c b/mod-tdx-guest/mod.c index 98b865d1c..45e04d316 100644 --- a/mod-tdx-guest/mod.c +++ b/mod-tdx-guest/mod.c @@ -138,7 +138,7 @@ static long tdx_guest_ioctl(struct file *file, unsigned int cmd, static const struct file_operations tdx_guest_fops = { .owner = THIS_MODULE, .unlocked_ioctl = tdx_guest_ioctl, - .llseek = no_llseek, + .llseek = noop_llseek, }; static struct miscdevice tdx_misc_dev = { From 8a186cda74802d455beaae58d1a586f195924938 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 18:57:44 +0800 Subject: [PATCH 029/264] Update Cargo.lock --- Cargo.lock | 2285 +++++++++++++++++++++++++++++----------------------- 1 file changed, 1277 insertions(+), 1008 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 348a9493f..275893489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -64,23 +55,11 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -93,9 +72,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f63701831729cb154cf0b6945256af46c426074646c98b9d123148ba1d8bde" +checksum = "f07655fedc35188f3c50ff8fc6ee45703ae14ef1bc7ae7d80e23a747012184e3" dependencies = [ "alloy-core", "alloy-signer", @@ -104,9 +83,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a3bd0305a44fb457cae77de1e82856eadd42ea3cdf0dae29df32eb3b592979" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives", @@ -115,23 +94,25 @@ dependencies = [ "alloy-trie", "alloy-tx-macros", "auto_impl", + "borsh", "c-kzg", - "derive_more 2.0.1", + "derive_more 2.1.0", "either", "k256", "once_cell", "rand 0.8.5", "secp256k1", "serde", + "serde_json", "serde_with", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-consensus-any" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a842b4023f571835e62ac39fb8d523d19fcdbacfa70bf796ff96e7e19586f50" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -143,9 +124,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe6c56d58fbfa9f0f6299376e8ce33091fc6494239466814c3f54b55743cb09" +checksum = "5ca96214615ec8cf3fa2a54b32f486eb49100ca7fe7eb0b8c1137cd316e7250a" dependencies = [ "alloy-primitives", ] @@ -160,37 +141,39 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-eip2930" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", ] [[package]] name = "alloy-eip7702" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" dependencies = [ "alloy-primitives", "alloy-rlp", + "borsh", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-eips" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd749c57f38f8cbf433e651179fc5a676255e6b95044f467d49255d2b81725a" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -199,20 +182,21 @@ dependencies = [ "alloy-rlp", "alloy-serde", "auto_impl", + "borsh", "c-kzg", - "derive_more 2.0.1", + "derive_more 2.1.0", "either", "serde", "serde_with", "sha2 0.10.9", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-json-abi" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125a1c373261b252e53e04d6e92c37d881833afc1315fceab53fd46045695640" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -222,24 +206,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f614019a029c8fec14ae661aa7d4302e6e66bdbfb869dab40e78dcfba935fc97" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", "tracing", ] [[package]] name = "alloy-network" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8b6d58e98803017bbfea01dde96c4d270a29e7aed3beb65c8d28b5ab464e0e" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -254,18 +238,18 @@ dependencies = [ "alloy-sol-types", "async-trait", "auto_impl", - "derive_more 2.0.1", + "derive_more 2.1.0", "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-network-primitives" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db489617bffe14847bf89f175b1c183e5dd7563ef84713936e2c34255cfbd845" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" dependencies = [ "alloy-consensus", "alloy-eips", @@ -276,18 +260,18 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" +checksum = "355bf68a433e0fd7f7d33d5a9fc2583fde70bf5c530f63b80845f8da5505cf28" dependencies = [ "alloy-rlp", "bytes", "cfg-if", "const-hex", - "derive_more 2.0.1", - "foldhash", - "hashbrown 0.15.5", - "indexmap 2.10.0", + "derive_more 2.1.0", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "indexmap 2.12.1", "itoa", "k256", "keccak-asm", @@ -295,7 +279,7 @@ dependencies = [ "proptest", "rand 0.9.2", "ruint", - "rustc-hash 2.1.1", + "rustc-hash", "serde", "sha3", "tiny-keccak", @@ -320,14 +304,14 @@ checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "alloy-rpc-types-any" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f27c0c41a16cd0af4f5dbf791f7be2a60502ca8b0e840e0ad29803fac2d587" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -336,9 +320,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f5812f81c3131abc2cd8953dc03c41999e180cff7252abbccaba68676e15027" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -352,14 +336,14 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-serde" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dfe41a47805a34b848c83448946ca96f3d36842e8c074bcf8fa0870e337d12" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives", "serde", @@ -368,9 +352,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79237b4c1b0934d5869deea4a54e6f0a7425a8cd943a739d6293afdf893d847" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" dependencies = [ "alloy-primitives", "async-trait", @@ -378,14 +362,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-signer-local" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e90a3858da59d1941f496c17db8d505f643260f7e97cdcdd33823ddca48fc1" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" dependencies = [ "alloy-consensus", "alloy-network", @@ -394,46 +378,46 @@ dependencies = [ "async-trait", "k256", "rand 0.8.5", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "alloy-sol-macro" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20d867dcf42019d4779519a1ceb55eba8d7f3d0e4f0a89bcba82b8f9eb01e48" +checksum = "f3ce480400051b5217f19d6e9a82d9010cdde20f1ae9c00d53591e4a1afbb312" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74e91b0b553c115d14bd0ed41898309356dc85d0e3d4b9014c4e7715e48c8ad" +checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" dependencies = [ "alloy-sol-macro-input", "const-hex", "heck 0.5.0", - "indexmap 2.10.0", + "indexmap 2.12.1", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84194d31220803f5f62d0a00f583fd3a062b36382e2bea446f1af96727754565" +checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" dependencies = [ "const-hex", "dunce", @@ -441,15 +425,15 @@ dependencies = [ "macro-string", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8c27b3cf6b2bb8361904732f955bc7c05e00be5f469cec7e2280b6167f3ff0" +checksum = "954d1b2533b9b2c7959652df3076954ecb1122a28cc740aa84e7b0a49f6ac0a9" dependencies = [ "serde", "winnow", @@ -457,9 +441,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5383d34ea00079e6dd89c652bcbdb764db160cef84e6250926961a0b2295d04" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -476,7 +460,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arrayvec 0.7.6", - "derive_more 2.0.1", + "derive_more 2.1.0", "nybbles", "serde", "smallvec", @@ -485,23 +469,16 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.32" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e434e0917dce890f755ea774f59d6f12557bc8c7dd9fa06456af80cfe0f0181e" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" dependencies = [ - "alloy-primitives", - "darling 0.21.2", + "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -513,9 +490,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -528,9 +505,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -543,29 +520,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "ark-ff" @@ -605,6 +582,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.3.0" @@ -625,6 +622,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-ff-macros" version = "0.3.0" @@ -650,6 +657,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -671,6 +691,18 @@ dependencies = [ "num-bigint", ] +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "num-bigint", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -691,6 +723,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -742,7 +784,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -754,7 +796,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -782,7 +824,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -793,7 +835,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -819,7 +861,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -830,9 +872,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -841,11 +883,10 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" dependencies = [ - "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -854,9 +895,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", @@ -870,8 +911,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "sync_wrapper", "tower", "tower-layer", @@ -880,9 +920,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -891,27 +931,11 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base16ct" version = "0.2.0" @@ -924,6 +948,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -932,9 +962,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "basic-toml" @@ -960,49 +990,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.2", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.106", - "which 4.4.2", -] - -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.9.2", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.106", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1020,20 +1007,26 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", ] +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + [[package]] name = "bitflags" version = "1.3.2" @@ -1042,9 +1035,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitvec" @@ -1075,7 +1068,20 @@ checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", "arrayvec 0.5.2", - "constant_time_eq", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.3.1", ] [[package]] @@ -1098,9 +1104,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd49896f12ac9b6dcd7a5998466b9b58263a695a3dd1ecc1aaca2e12a90b080" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" dependencies = [ "cc", "glob", @@ -1133,7 +1139,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tokio-util", "tower-service", @@ -1154,9 +1160,9 @@ dependencies = [ [[package]] name = "bon" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a0c21249ad725ebcadcb1b1885f8e3d56e8e6b8924f560268aab000982d637" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" dependencies = [ "bon-macros", "rustversion", @@ -1164,24 +1170,24 @@ dependencies = [ [[package]] name = "bon-macros" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a660ebdea4d4d3ec7788cfc9c035b66efb66028b9b97bf6cde7023ccc8e77e28" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" dependencies = [ - "darling 0.21.2", + "darling", "ident_case", "prettyplease", "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ "borsh-derive", "cfg_aliases", @@ -1189,25 +1195,25 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", - "regex-automata 0.4.9", + "regex-automata", "serde", ] @@ -1225,9 +1231,9 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -1237,18 +1243,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] [[package]] name = "c-kzg" -version = "2.1.1" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7318cfa722931cb5fe0838b98d3ce5621e75f6a6408abc21721d80de9223f2e4" +checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" dependencies = [ "blst", "cc", @@ -1261,10 +1267,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.33" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1334,24 +1341,15 @@ dependencies = [ "rustls", "serde", "tokio", - "toml_edit", + "toml_edit 0.22.27", "tracing-subscriber", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1361,17 +1359,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1394,22 +1391,11 @@ dependencies = [ "zeroize", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" -version = "4.5.45" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -1417,9 +1403,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.44" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -1429,21 +1415,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.45" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -1477,7 +1463,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1500,15 +1486,14 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.14.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e22e0ed40b96a48d3db274f72fd365bd78f67af39b6bbd47e8a15e1c6207ff" +checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" dependencies = [ "cfg-if", "cpufeatures", - "hex", "proptest", - "serde", + "serde_core", ] [[package]] @@ -1519,9 +1504,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -1543,6 +1528,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.6.0" @@ -1561,6 +1552,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -1609,9 +1609,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -1706,9 +1706,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1788,48 +1788,24 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08440b3dd222c3d0433e63e097463969485f112baff337dfdaca043a0d760570" -dependencies = [ - "darling_core 0.21.2", - "darling_macro 0.21.2", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", + "darling_core", + "darling_macro", ] [[package]] name = "darling_core" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25b7912bc28a04ab1b7715a68ea03aaa15662b43a1a4b2c480531fd19f8bf7e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", @@ -1837,29 +1813,18 @@ dependencies = [ "quote", "serde", "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "darling_macro" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce154b9bea7fb0c8e8326e62d00354000c36e79770ff21b8c84e3aa267d9d531" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.21.2", + "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1873,7 +1838,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -1884,13 +1849,14 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dcap-qvl" -version = "0.3.0" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ef42eade99e79b9fb9d31532041a0a380a7e849d3486950a40b1afd5bf417c" +checksum = "435989ce7ba46ba3f837f9df3c8139469e72ae810e707893b19f8b6b370d14ef" dependencies = [ "anyhow", "asn1_der", "base64 0.22.1", + "borsh", "byteorder", "chrono", "const-oid", @@ -1977,17 +1943,17 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -2012,11 +1978,11 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ - "derive_more-impl 2.0.1", + "derive_more-impl 2.1.0", ] [[package]] @@ -2028,19 +1994,21 @@ dependencies = [ "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "unicode-xid", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ + "convert_case 0.10.0", "proc-macro2", "quote", - "syn 2.0.106", + "rustc_version 0.4.1", + "syn 2.0.111", "unicode-xid", ] @@ -2070,11 +2038,11 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2116,7 +2084,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2127,7 +2095,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2149,7 +2117,7 @@ checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" dependencies = [ "documented-macros", "phf", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -2164,7 +2132,7 @@ dependencies = [ "proc-macro2", "quote", "strum", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2192,7 +2160,7 @@ dependencies = [ "ipnet", "jemallocator", "load_config", - "nix", + "nix 0.29.0", "or-panic", "parcelona", "pin-project", @@ -2272,6 +2240,7 @@ dependencies = [ "tdx-attest", "tempfile", "tokio", + "tpm-attest", "tracing", "tracing-subscriber", ] @@ -2300,6 +2269,7 @@ dependencies = [ "dstack-kms-rpc", "dstack-mr", "dstack-types", + "dstack-verifier", "fs-err", "git-version", "hex", @@ -2354,7 +2324,7 @@ dependencies = [ "flate2", "fs-err", "hex", - "hex-literal 1.0.0", + "hex-literal 1.1.0", "log", "object", "parity-scale-codec", @@ -2364,7 +2334,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "tar", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -2422,6 +2392,7 @@ dependencies = [ name = "dstack-types" version = "0.5.5" dependencies = [ + "parity-scale-codec", "serde", "serde-human-bytes", "sha3", @@ -2443,8 +2414,9 @@ dependencies = [ "dstack-gateway-rpc", "dstack-kms-rpc", "dstack-types", + "ez-hash", "fs-err", - "getrandom 0.3.3", + "getrandom 0.3.4", "hex", "hex_fmt", "host-api", @@ -2461,17 +2433,20 @@ dependencies = [ "serde", "serde-human-bytes", "serde_json", - "serde_yaml2", "sha2 0.10.9", "sha3", "sodiumbox", "tdx-attest", + "tempfile", "tokio", "toml", + "tpm-attest", + "tpm-qvl", "tracing", "tracing-subscriber", "x25519-dalek", "x509-parser", + "yaml-rust2", ] [[package]] @@ -2491,10 +2466,13 @@ dependencies = [ "reqwest", "rocket", "serde", + "serde-human-bytes", "serde_json", "sha2 0.10.9", "tempfile", "tokio", + "tpm-qvl", + "tpm-types", "tracing", "tracing-subscriber", ] @@ -2511,7 +2489,9 @@ dependencies = [ "dstack-kms-rpc", "dstack-types", "dstack-vmm-rpc", + "fatfs", "fs-err", + "fscommon", "git-version", "guest-api", "hex", @@ -2611,6 +2591,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "either" version = "1.15.0" @@ -2665,7 +2657,27 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -2677,7 +2689,27 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -2707,12 +2739,27 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "ez-hash" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55891018a9c34579a8ec3c9d7d503699a68176f961eac8f06b27129f8e4520c" +dependencies = [ + "blake2", + "blake3", + "digest 0.10.7", + "md-5", + "sha1", + "sha2 0.10.9", + "sha3", ] [[package]] @@ -2754,6 +2801,18 @@ dependencies = [ "bytes", ] +[[package]] +name = "fatfs" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "chrono", + "log", +] + [[package]] name = "ff" version = "0.13.1" @@ -2797,6 +2856,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -2829,9 +2894,9 @@ checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -2849,20 +2914,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] [[package]] name = "fs-err" -version = "3.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" +checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" dependencies = [ "autocfg", ] @@ -2873,6 +2944,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fscommon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "315ce685aca5ddcc5a3e7e436ef47d4a5d0064462849b6f0f628c28140103531" +dependencies = [ + "log", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2944,7 +3024,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2996,20 +3076,6 @@ dependencies = [ "windows 0.48.0", ] -[[package]] -name = "generator" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.3", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -3047,15 +3113,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -3079,12 +3145,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "git-version" version = "0.3.9" @@ -3102,7 +3162,7 @@ checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3147,7 +3207,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -3185,30 +3245,36 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.2.0", "serde", + "serde_core", ] [[package]] name = "hashlink" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.5", ] [[package]] @@ -3236,16 +3302,13 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-conservative" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" dependencies = [ "arrayvec 0.7.6", ] @@ -3258,9 +3321,9 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hex-literal" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hex_fmt" @@ -3310,7 +3373,7 @@ dependencies = [ "once_cell", "rand 0.9.2", "ring", - "thiserror 2.0.15", + "thiserror 2.0.17", "tinyvec", "tokio", "tracing", @@ -3329,7 +3392,7 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "resolv-conf", "smallvec", @@ -3350,11 +3413,11 @@ dependencies = [ "ipconfig", "moka", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.9.2", "resolv-conf", "smallvec", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -3389,11 +3452,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3409,14 +3472,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "hostname-validator" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" + [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -3501,19 +3569,20 @@ dependencies = [ [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -3521,6 +3590,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -3561,9 +3631,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -3577,7 +3647,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -3600,9 +3670,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3610,7 +3680,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -3624,9 +3694,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -3637,9 +3707,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -3650,11 +3720,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -3665,42 +3734,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -3716,9 +3781,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3752,7 +3817,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3768,13 +3833,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -3789,7 +3855,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -3814,9 +3880,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.43.1" +version = "1.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" dependencies = [ "console", "once_cell", @@ -3863,17 +3929,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.2", - "cfg-if", - "libc", -] - [[package]] name = "iohash" version = "0.5.5" @@ -3910,9 +3965,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -3920,20 +3975,20 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -3944,15 +3999,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3999,19 +4045,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -4086,28 +4132,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +dependencies = [ + "spin", +] [[package]] name = "libc" -version = "0.2.175" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" - -[[package]] -name = "libloading" -version = "0.8.8" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.3", -] +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libm" @@ -4117,13 +4150,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -4140,15 +4173,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "load_config" @@ -4162,19 +4195,18 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -4183,7 +4215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if", - "generator 0.7.5", + "generator", "scoped-tls", "serde", "serde_json", @@ -4191,19 +4223,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator 0.8.5", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru-cache" version = "0.1.2" @@ -4257,16 +4276,16 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4275,6 +4294,26 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "mbox" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de" +dependencies = [ + "libc", + "stable_deref_trait", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memalloc" version = "0.1.0" @@ -4283,9 +4322,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" @@ -4337,6 +4376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -4354,14 +4394,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4375,20 +4415,19 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom 0.7.2", - "parking_lot 0.12.4", + "equivalent", + "parking_lot 0.12.5", "portable-atomic", "rustc_version 0.4.1", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] @@ -4481,7 +4520,19 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -4511,13 +4562,13 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "fsevent-sys", "inotify", "kqueue", "libc", "log", - "mio 1.0.4", + "mio 1.1.1", "notify-types", "walkdir", "windows-sys 0.60.2", @@ -4549,12 +4600,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4567,12 +4617,39 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4582,6 +4659,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -4612,33 +4700,11 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "nybbles" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0418987d1aaed324d95b4beffc93635e19be965ed5d63ec07a35980fe3b71a4" +checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" dependencies = [ "alloy-rlp", "cfg-if", @@ -4650,18 +4716,18 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", @@ -4678,6 +4744,15 @@ dependencies = [ "ruzstd", ] +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + [[package]] name = "oid-registry" version = "0.7.1" @@ -4699,9 +4774,9 @@ dependencies = [ [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -4723,7 +4798,7 @@ checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4740,20 +4815,14 @@ checksum = "596a79faf55e869e7bc0c2162cf2f18a54d4d1112876bceae587ad954fcbd574" [[package]] name = "os_pipe" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -4785,7 +4854,7 @@ checksum = "89ec0a2252bc3809594c903cc8c1b83cbccaba85b11d4728a43a681263f6c132" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4813,7 +4882,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4829,12 +4898,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -4853,15 +4922,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -4917,17 +4986,17 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -4941,18 +5010,17 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" dependencies = [ "memchr", - "thiserror 2.0.15", "ucd-trie", ] @@ -4963,7 +5031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset 0.4.2", - "indexmap 2.10.0", + "indexmap 2.12.1", ] [[package]] @@ -4973,7 +5041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset 0.5.7", - "indexmap 2.10.0", + "indexmap 2.12.1", ] [[package]] @@ -5006,7 +5074,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5018,6 +5086,41 @@ dependencies = [ "siphasher", ] +[[package]] +name = "picky-asn1" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212" +dependencies = [ + "oid", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-der" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208" +dependencies = [ + "base64 0.21.7", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -5035,7 +5138,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5050,6 +5153,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5060,6 +5174,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "poly1305" version = "0.8.0" @@ -5091,9 +5211,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -5115,12 +5235,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5145,11 +5265,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.9", ] [[package]] @@ -5195,14 +5315,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -5215,26 +5335,25 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "version_check", "yansi", ] [[package]] name = "proptest" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.2", - "lazy_static", + "bitflags 2.10.0", "num-traits", "rand 0.9.2", "rand_chacha 0.9.0", "rand_xorshift", - "regex-syntax 0.8.5", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -5296,7 +5415,7 @@ dependencies = [ "prost 0.13.5", "prost-types 0.13.5", "regex", - "syn 2.0.106", + "syn 2.0.111", "tempfile", ] @@ -5323,7 +5442,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5382,7 +5501,7 @@ dependencies = [ "prost-build 0.9.0", "prost-types 0.13.5", "quote", - "syn 2.0.106", + "syn 2.0.111", "template-quote", ] @@ -5393,7 +5512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac0855066edbf6bdcb42beb02cd9063d12d8d6d44b9a0c2f15a30e6ddd11f5" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5404,19 +5523,19 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", - "socket2 0.5.10", - "thiserror 2.0.15", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -5424,20 +5543,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.15", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -5445,23 +5564,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -5499,13 +5618,18 @@ dependencies = [ "bon", "cc-eventlog", "dcap-qvl", + "dstack-types", "elliptic-curve", + "ez-hash", + "flate2", "fs-err", "hex", + "hex_fmt", "hkdf", "or-panic", "p256", "parity-scale-codec", + "rand 0.8.5", "rcgen", "ring", "rustls-pki-types", @@ -5515,6 +5639,9 @@ dependencies = [ "sha2 0.10.9", "sha3", "tdx-attest", + "tpm-attest", + "tpm-qvl", + "tpm-types", "tracing", "x509-parser", "yasna", @@ -5616,7 +5743,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "serde", ] @@ -5663,11 +5790,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", ] [[package]] @@ -5678,27 +5805,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5709,53 +5836,38 @@ checksum = "09c30c54dffee5b40af088d5d50aa3455c91a0127164b51f0215efc4cb28fb3c" [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" dependencies = [ "base64 0.22.1", "bytes", @@ -5797,9 +5909,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "result" @@ -5856,9 +5968,9 @@ dependencies = [ "proc-macro2", "quote", "rinja_parser", - "rustc-hash 2.1.1", + "rustc-hash", "serde", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5885,7 +5997,7 @@ dependencies = [ [[package]] name = "rocket" version = "0.6.0-dev" -source = "git+https://github.com/rwf2/Rocket?branch=master#f9de1bf4671100b2f9c9bea6ce206fc4748ca999" +source = "git+https://github.com/rwf2/Rocket?branch=master#504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" dependencies = [ "async-stream", "async-trait", @@ -5898,12 +6010,12 @@ dependencies = [ "http", "hyper", "hyper-util", - "indexmap 2.10.0", + "indexmap 2.12.1", "libc", "memchr", "multer", "num_cpus", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "rand 0.8.5", "ref-cast", @@ -5945,11 +6057,11 @@ name = "rocket-vsock-listener" version = "0.5.5" dependencies = [ "anyhow", - "derive_more 2.0.1", + "derive_more 2.1.0", "pin-project", "rocket", "serde", - "thiserror 2.0.15", + "thiserror 2.0.17", "tokio", "tokio-vsock", ] @@ -5957,15 +6069,15 @@ dependencies = [ [[package]] name = "rocket_codegen" version = "0.6.0-dev" -source = "git+https://github.com/rwf2/Rocket?branch=master#f9de1bf4671100b2f9c9bea6ce206fc4748ca999" +source = "git+https://github.com/rwf2/Rocket?branch=master#504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" dependencies = [ "devise", "glob", - "indexmap 2.10.0", + "indexmap 2.12.1", "proc-macro2", "quote", "rocket_http", - "syn 2.0.106", + "syn 2.0.111", "unicode-xid", "version_check", ] @@ -5973,11 +6085,11 @@ dependencies = [ [[package]] name = "rocket_http" version = "0.6.0-dev" -source = "git+https://github.com/rwf2/Rocket?branch=master#f9de1bf4671100b2f9c9bea6ce206fc4748ca999" +source = "git+https://github.com/rwf2/Rocket?branch=master#504efef179622df82ba1dbd37f2e0d9ed2b7c9e4" dependencies = [ "cookie", "either", - "indexmap 2.10.0", + "indexmap 2.12.1", "memchr", "pear", "percent-encoding", @@ -5990,15 +6102,36 @@ dependencies = [ "uncased", ] +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "ruint" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", "ark-ff 0.4.2", + "ark-ff 0.5.0", "bytes", "fastrlp 0.3.1", "fastrlp 0.4.0", @@ -6012,7 +6145,7 @@ dependencies = [ "rand 0.9.2", "rlp", "ruint-macro", - "serde", + "serde_core", "valuable", "zeroize", ] @@ -6031,22 +6164,10 @@ checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ "base64 0.13.1", "blake2b_simd", - "constant_time_eq", + "constant_time_eq 0.1.5", "crossbeam-utils", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -6074,7 +6195,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.26", + "semver 1.0.27", ] [[package]] @@ -6092,7 +6213,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6101,38 +6222,38 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -6151,9 +6272,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -6172,9 +6293,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring", @@ -6190,9 +6311,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error", @@ -6217,9 +6338,9 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "s2n-codec" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a2a209163c3c3e68c100651706645843188015db6859236a19d63c7aa00f18" +checksum = "2ce62ab2534380ee20a32ce169c733df8462ef9821489ae8df8603c1d9e89574" dependencies = [ "byteorder", "bytes", @@ -6228,9 +6349,9 @@ dependencies = [ [[package]] name = "s2n-quic" -version = "1.63.0" +version = "1.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e462d4e16d99370a0ae1544705a1dfb1f572f3b13068af7ffdfe1f196482b06a" +checksum = "62cfd37e7de6e7d73a8c55996a1389d27d6bbe377e565440ef4980252062bf09" dependencies = [ "bytes", "cfg-if", @@ -6252,9 +6373,9 @@ dependencies = [ [[package]] name = "s2n-quic-core" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dea73b3696c383fd86dc423ba744a4ff4f6608c844832f4933366b9d9d68d57" +checksum = "d36dc08b73cea81834036fa54ba9cf4ad91ede5a31543d0ad64a63f7e99d15db" dependencies = [ "atomic-waker", "byteorder", @@ -6274,9 +6395,9 @@ dependencies = [ [[package]] name = "s2n-quic-crypto" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07254da875a37720bc944eaad8026c225d6599715ce30b0af198c039cd8046dd" +checksum = "da8e47e0db3b85012cdcb7a95490639893fa60ea0d680730424839d2c268e23c" dependencies = [ "aws-lc-rs", "cfg-if", @@ -6300,28 +6421,28 @@ dependencies = [ [[package]] name = "s2n-quic-platform" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487103480577416f5cb614d270cd65149f1cfa2adf8841b6ae64260da048d4ec" +checksum = "218ddcd7677c8dee8fc48e6f4daaeecb63c7c5b28b21d2429a53fcbf326b55ce" dependencies = [ "cfg-if", "futures", "lazy_static", "libc", "s2n-quic-core", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", ] [[package]] name = "s2n-quic-rustls" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656d175084631103e3db1b888d0e1c23b7fd2f855d87c41a3ad634c0823ec5ba" +checksum = "cc59fae30cffc275a6e798c00996f3e56dc74371bd685e112e66a231ced86c4a" dependencies = [ "bytes", "rustls", - "rustls-pemfile", + "rustls-pki-types", "s2n-codec", "s2n-quic-core", "s2n-quic-crypto", @@ -6329,14 +6450,14 @@ dependencies = [ [[package]] name = "s2n-quic-transport" -version = "0.63.0" +version = "0.69.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33468e6ab9dd8aa389f4df02464f737fcd05c33b8fc50d0274b57613e9596a06" +checksum = "dfbd2465236f7ce448f082a33bdc2e85ef62a7ce68c7eef79e4d90dc1b3eec0c" dependencies = [ "bytes", "futures-channel", "futures-core", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "intrusive-collections", "once_cell", "s2n-codec", @@ -6394,16 +6515,16 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6420,9 +6541,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -6517,11 +6638,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -6530,9 +6651,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -6549,9 +6670,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "semver-parser" @@ -6564,9 +6685,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -6590,41 +6711,43 @@ dependencies = [ [[package]] name = "serde-human-bytes" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ef65cb41f3f9cef63c431193229067e8b98b53c4d4c4ed38a8ca87c4d07676" +checksum = "6a091af6294712930d01e375cce513e4ac416f823e033e8991ec4e5d6e6ef4c0" dependencies = [ + "base64 0.13.1", "hex", "serde", ] [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde_core" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.225" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6640,15 +6763,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -6670,7 +6794,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6696,19 +6820,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.1.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -6716,35 +6839,35 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ - "darling 0.20.11", + "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] -name = "serde_yaml2" -version = "0.1.3" +name = "serdect" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63a7808e70b0b09116f01a4730d8976bcab457ea4a5e2e638e9b12ed3e01f27c" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" dependencies = [ + "base16ct", "serde", - "thiserror 1.0.69", - "yaml-rust2", ] [[package]] -name = "serdect" -version = "0.2.0" +name = "sha1" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "base16ct", - "serde", + "cfg-if", + "cpufeatures", + "digest 0.10.7", ] [[package]] @@ -6851,9 +6974,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -6868,6 +6991,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "similar" version = "2.7.0" @@ -6887,7 +7016,7 @@ dependencies = [ "anyhow", "serde", "serde_json", - "thiserror 2.0.15", + "thiserror 2.0.17", ] [[package]] @@ -6917,12 +7046,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6964,9 +7093,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state" @@ -6974,7 +7103,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" dependencies = [ - "loom 0.5.6", + "loom", ] [[package]] @@ -7016,7 +7145,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7037,7 +7166,7 @@ dependencies = [ "git-version", "libc", "load_config", - "nix", + "nix 0.29.0", "notify", "or-panic", "rocket", @@ -7083,9 +7212,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -7094,14 +7223,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b198d366dbec045acfcd97295eb653a7a2b40e4dc764ef1e79aafcad439d3c" +checksum = "ff790eb176cc81bb8936aed0f7b9f14fc4670069a2d371b3e3b0ecce908b2cb3" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7121,7 +7250,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7192,6 +7321,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tdx-attest" version = "0.5.5" @@ -7201,35 +7336,26 @@ dependencies = [ "fs-err", "hex", "insta", - "num_enum", + "log", "parity-scale-codec", "serde", "serde-human-bytes", "serde_json", "sha2 0.10.9", - "tdx-attest-sys", - "thiserror 2.0.15", -] - -[[package]] -name = "tdx-attest-sys" -version = "0.5.5" -dependencies = [ - "bindgen 0.71.1", - "cc", + "thiserror 2.0.17", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -7274,11 +7400,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.15" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.15", + "thiserror-impl 2.0.17", ] [[package]] @@ -7289,18 +7415,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "thiserror-impl" -version = "2.0.15" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7323,9 +7449,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -7338,15 +7464,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -7363,9 +7489,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -7373,9 +7499,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -7388,40 +7514,37 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", - "mio 1.0.4", - "parking_lot 0.12.4", + "mio 1.1.1", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -7440,9 +7563,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -7472,8 +7595,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -7485,20 +7608,50 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -7522,11 +7675,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.2", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -7550,11 +7703,64 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tpm-attest" +version = "0.5.5" +dependencies = [ + "anyhow", + "dstack-types", + "fs-err", + "hex", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.10.9", + "tempfile", + "tpm-types", + "tracing", + "tss-esapi", +] + +[[package]] +name = "tpm-qvl" +version = "0.5.5" +dependencies = [ + "anyhow", + "base64 0.22.1", + "dcap-qvl-webpki", + "dstack-types", + "hex", + "nom", + "p256", + "pem", + "reqwest", + "rsa", + "rustls-pki-types", + "rustls-webpki 0.103.8", + "serde", + "serde_json", + "sha2 0.10.9", + "tpm-types", + "tracing", + "x509-parser", +] + +[[package]] +name = "tpm-types" +version = "0.5.5" +dependencies = [ + "cc-eventlog", + "dstack-types", + "parity-scale-codec", + "serde", + "serde-human-bytes", +] + [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -7563,20 +7769,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -7595,15 +7801,15 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "parking_lot 0.12.4", - "regex", + "parking_lot 0.12.5", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -7618,6 +7824,39 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tss-esapi" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ea9ccde878b029392ac97b5be1f470173d06ea41d18ad0bb3c92794c16a0f2" +dependencies = [ + "bitfield", + "enumflags2", + "getrandom 0.2.16", + "hostname-validator", + "log", + "mbox", + "num-derive", + "num-traits", + "oid", + "picky-asn1", + "picky-asn1-x509", + "regex", + "serde", + "tss-esapi-sys", + "zeroize", +] + +[[package]] +name = "tss-esapi-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535cd192581c2ec4d5f82e670b1d3fbba6a23ccce8c85de387642051d7cad5b5" +dependencies = [ + "pkg-config", + "target-lexicon", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -7630,9 +7869,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ubyte" @@ -7685,9 +7924,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -7725,13 +7964,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -7754,11 +7994,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -7783,12 +8023,12 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vsock" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8b4d00e672f147fc86a09738fadb1445bd1c0a40542378dfb82909deeee688" +checksum = "e2da6e4ac76cd19635dce0f98985378bb62f8044ee2ff80abd2a7334b920ed63" dependencies = [ "libc", - "nix", + "nix 0.30.1", ] [[package]] @@ -7841,45 +8081,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -7890,9 +8117,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7900,31 +8127,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -7942,9 +8169,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -7969,15 +8196,15 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix 1.0.8", + "rustix 1.1.2", "winsafe", ] [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -7997,11 +8224,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8026,9 +8253,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -8038,7 +8265,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -8049,9 +8276,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -8060,31 +8300,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8093,14 +8333,20 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", ] [[package]] @@ -8109,7 +8355,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -8118,7 +8373,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -8154,7 +8418,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -8190,19 +8463,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -8211,7 +8484,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -8228,9 +8501,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -8246,9 +8519,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -8264,9 +8537,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -8276,9 +8549,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -8294,9 +8567,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -8312,9 +8585,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -8330,9 +8603,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -8348,15 +8621,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -8378,19 +8651,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.2", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -8449,7 +8719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.0.8", + "rustix 1.1.2", ] [[package]] @@ -8477,9 +8747,9 @@ dependencies = [ [[package]] name = "yaml-rust2" -version = "0.8.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ "arraydeque", "encoding_rs", @@ -8506,11 +8776,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -8518,34 +8787,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -8565,15 +8834,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -8586,14 +8855,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -8602,9 +8871,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -8613,11 +8882,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] From 2bb190ebff368c80de1fda34e91fa49d9c63093f Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 02:48:26 +0000 Subject: [PATCH 030/264] dstack-util: add attest subcommand --- dstack-util/src/main.rs | 63 +++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index efa47343b..f46f2ee2d 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -78,6 +78,8 @@ enum Commands { /// Verify a TPM quote TpmVerify(TpmVerifyArgs), QuoteReport(QuoteReportArgs), + /// Generate a versioned attestation for simulator use + Attest(AttestArgs), } #[derive(Parser)] @@ -278,21 +280,36 @@ struct QuoteReportArgs { debug: bool, } +#[derive(Parser)] +struct AttestArgs { + /// report data in hex (max 64 bytes) + #[arg(long)] + report_data: Option, + + /// output file (default: attestation.bin) + #[arg(short, long)] + output: Option, + + /// hex encode output + #[arg(long, default_value_t = false)] + hex: bool, +} + +fn pad64(data: &[u8]) -> Result<[u8; 64]> { + if data.len() > 64 { + anyhow::bail!("report_data must be at most 64 bytes"); + } + let mut out = [0u8; 64]; + out[..data.len()].copy_from_slice(data); + Ok(out) +} + fn cmd_quote_report(args: QuoteReportArgs) -> Result<()> { #[derive(serde::Serialize)] struct VerificationRequestJson { pub attestation: String, } - fn pad64(data: &[u8]) -> Result<[u8; 64]> { - if data.len() > 64 { - anyhow::bail!("report_data must be at most 64 bytes"); - } - let mut out = [0u8; 64]; - out[..data.len()].copy_from_slice(data); - Ok(out) - } - let report_data = match args.report_data { Some(hex_data) => { pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? @@ -314,6 +331,31 @@ fn cmd_quote_report(args: QuoteReportArgs) -> Result<()> { Ok(()) } +fn cmd_attest(args: AttestArgs) -> Result<()> { + let report_data = match args.report_data { + Some(hex_data) => { + pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? + } + None => [0u8; 64], + }; + let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + let attestation = attestation.into_versioned().to_scale(); + + if args.hex { + let encoded = hex::encode(attestation); + if let Some(output) = args.output { + fs::write(&output, encoded).context("Failed to write attestation hex")?; + } else { + println!("{encoded}"); + } + return Ok(()); + } + + let output = args.output.unwrap_or_else(|| PathBuf::from("attestation.bin")); + fs::write(&output, &attestation).context("Failed to write attestation sample")?; + Ok(()) +} + fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; io::stdin() @@ -1004,6 +1046,9 @@ async fn main() -> Result<()> { Commands::QuoteReport(args) => { cmd_quote_report(args)?; } + Commands::Attest(args) => { + cmd_attest(args)?; + } } Ok(()) From 6d35ba37ec4eb55a6e5d0eb927c0c55b5d3e1e9f Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 19 Dec 2025 15:21:49 +0000 Subject: [PATCH 031/264] guest-agent: Implement fn attest --- dstack-util/src/main.rs | 4 +- gateway/src/main_service.rs | 2 +- guest-agent/dstack.toml | 1 + guest-agent/rpc/proto/agent_rpc.proto | 4 +- guest-agent/src/config.rs | 1 + guest-agent/src/rpc_service.rs | 47 ++++++++++++++++------ kms/src/onboard_service.rs | 8 ++-- sdk/curl/api.md | 31 ++++++++++++++ sdk/go/dstack/client.go | 8 ++-- sdk/js/src/index.ts | 10 ++--- sdk/python/src/dstack_sdk/__init__.py | 4 +- sdk/python/src/dstack_sdk/dstack_client.py | 14 +++---- sdk/rust/src/dstack_client.rs | 6 +-- sdk/rust/types/src/dstack.rs | 4 +- 14 files changed, 99 insertions(+), 45 deletions(-) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index f46f2ee2d..cd73e011c 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -351,7 +351,9 @@ fn cmd_attest(args: AttestArgs) -> Result<()> { return Ok(()); } - let output = args.output.unwrap_or_else(|| PathBuf::from("attestation.bin")); + let output = args + .output + .unwrap_or_else(|| PathBuf::from("attestation.bin")); fs::write(&output, &attestation).context("Failed to write attestation sample")?; Ok(()) } diff --git a/gateway/src/main_service.rs b/gateway/src/main_service.rs index 953df694c..25befd26c 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -855,7 +855,7 @@ async fn get_or_generate_attestation( } let report_data = content_type.to_report_data(payload).to_vec(); let response = agent - .get_attestation(RawQuoteArgs { report_data }) + .attest(RawQuoteArgs { report_data }) .await .context("Failed to get quote")?; let attestation = serde_json::to_string(&response).context("Failed to serialize quote")?; diff --git a/guest-agent/dstack.toml b/guest-agent/dstack.toml index 28722bf08..7b6e52846 100644 --- a/guest-agent/dstack.toml +++ b/guest-agent/dstack.toml @@ -19,6 +19,7 @@ data_disks = ["/"] [default.core.simulator] enabled = false quote_file = "quote.hex" +attestation_file = "attestation.bin" event_log_file = "eventlog.json" sys_config_file = "sys-config.json" diff --git a/guest-agent/rpc/proto/agent_rpc.proto b/guest-agent/rpc/proto/agent_rpc.proto index 741e52632..f12b161de 100644 --- a/guest-agent/rpc/proto/agent_rpc.proto +++ b/guest-agent/rpc/proto/agent_rpc.proto @@ -45,7 +45,7 @@ service DstackGuest { // Generates a versioned attestation with the given report data. // Returns a dstack-defined attestation format that supports different attestation modes across platforms. - rpc GetAttestation(RawQuoteArgs) returns (GetAttestationResponse) {} + rpc Attest(RawQuoteArgs) returns (AttestResponse) {} // Emit an event. This extends the event to RTMR3 on TDX platform. rpc EmitEvent(EmitEventArgs) returns (google.protobuf.Empty) {} @@ -173,7 +173,7 @@ message TdxQuoteResponse { string prefix = 4; } -message GetAttestationResponse { +message AttestResponse { // The attestation bytes attestation = 1; } diff --git a/guest-agent/src/config.rs b/guest-agent/src/config.rs index c8b75ed09..c13438f65 100644 --- a/guest-agent/src/config.rs +++ b/guest-agent/src/config.rs @@ -73,4 +73,5 @@ pub struct Simulator { pub enabled: bool, pub quote_file: String, pub event_log_file: String, + pub attestation_file: String, } diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index a2841c8ab..fb5f33e87 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -11,8 +11,8 @@ use dstack_guest_agent_rpc::{ dstack_guest_server::{DstackGuestRpc, DstackGuestServer}, tappd_server::{TappdRpc, TappdServer}, worker_server::{WorkerRpc, WorkerServer}, - AppInfo, DeriveK256KeyResponse, DeriveKeyArgs, EmitEventArgs, GetAttestationForAppKeyRequest, - GetAttestationResponse, GetKeyArgs, GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, + AppInfo, AttestResponse, DeriveK256KeyResponse, DeriveKeyArgs, EmitEventArgs, + GetAttestationForAppKeyRequest, GetKeyArgs, GetKeyResponse, GetQuoteResponse, GetTlsKeyArgs, GetTlsKeyResponse, RawQuoteArgs, SignRequest, SignResponse, TdxQuoteArgs, TdxQuoteResponse, VerifyRequest, VerifyResponse, WorkerVersion, }; @@ -270,14 +270,6 @@ impl DstackGuestRpc for InternalRpcHandler { } async fn get_quote(self, request: RawQuoteArgs) -> Result { - fn pad64(data: &[u8]) -> Option<[u8; 64]> { - if data.len() > 64 { - return None; - } - let mut padded = [0u8; 64]; - padded[..data.len()].copy_from_slice(data); - Some(padded) - } let report_data = pad64(&request.report_data).context("Report data is too long")?; if self.state.config().simulator.enabled { return simulate_quote( @@ -394,10 +386,39 @@ impl DstackGuestRpc for InternalRpcHandler { Ok(VerifyResponse { valid }) } - async fn get_attestation(self, request: RawQuoteArgs) -> Result { - let todo = "implement it"; - todo!() + async fn attest(self, request: RawQuoteArgs) -> Result { + let report_data = pad64(&request.report_data).context("Report data is too long")?; + if self.state.config().simulator.enabled { + return simulate_attestation( + self.state.config(), + report_data, + &self.state.inner.vm_config, + ); + } + let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + Ok(AttestResponse { + attestation: attestation.into_versioned().to_scale(), + }) + } +} + +fn pad64(data: &[u8]) -> Option<[u8; 64]> { + if data.len() > 64 { + return None; } + let mut padded = [0u8; 64]; + padded[..data.len()].copy_from_slice(data); + Some(padded) +} + +fn simulate_attestation( + config: &Config, + report_data: [u8; 64], + vm_config: &str, +) -> Result { + let attestation = + fs::read(&config.simulator.attestation_file).context("Failed to read attestation file")?; + Ok(AttestResponse { attestation }) } fn simulate_quote( diff --git a/kms/src/onboard_service.rs b/kms/src/onboard_service.rs index 1dcd3e0a0..4a4107fd5 100644 --- a/kms/src/onboard_service.rs +++ b/kms/src/onboard_service.rs @@ -4,7 +4,7 @@ use anyhow::{Context, Result}; use dstack_guest_agent_rpc::{ - dstack_guest_client::DstackGuestClient, GetAttestationResponse, RawQuoteArgs, + dstack_guest_client::DstackGuestClient, AttestResponse, RawQuoteArgs, }; use dstack_kms_rpc::{ kms_client::KmsClient, @@ -296,10 +296,8 @@ fn dstack_client() -> DstackGuestClient { DstackGuestClient::new(http_client) } -async fn app_attest(report_data: Vec) -> Result { - dstack_client() - .get_attestation(RawQuoteArgs { report_data }) - .await +async fn app_attest(report_data: Vec) -> Result { + dstack_client().attest(RawQuoteArgs { report_data }).await } async fn attest_keys(p256_pubkey: &[u8], k256_pubkey: &[u8]) -> Result> { diff --git a/sdk/curl/api.md b/sdk/curl/api.md index 2a18c393b..99f6c232a 100644 --- a/sdk/curl/api.md +++ b/sdk/curl/api.md @@ -265,6 +265,37 @@ curl --unix-socket /var/run/dstack.sock -X POST \ } ``` +### 8. Attest + +Generates a versioned attestation with the given report data. Returns a dstack-defined attestation format that supports different attestation modes across platforms. + +**Endpoint:** `/Attest` + +**Request Parameters:** + +| Field | Type | Description | Example | +|-------|------|-------------|----------| +| `report_data` | string | Report data of max length 64 bytes. Padding with 0s if less than 64 bytes. | `"1234deadbeaf"` | + +**Example:** +```bash +curl --unix-socket /var/run/dstack.sock -X POST \ + http://dstack/Attest \ + -H 'Content-Type: application/json' \ + -d '{ + "report_data": "1234deadbeaf" + }' +``` +Or +```bash +curl --unix-socket /var/run/dstack.sock http://dstack/Attest?report_data=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +``` + +**Response:** +```json +{ + "attestation": "" +} ``` ## Error Responses diff --git a/sdk/go/dstack/client.go b/sdk/go/dstack/client.go index 4e8e8efc9..c700e1f21 100644 --- a/sdk/go/dstack/client.go +++ b/sdk/go/dstack/client.go @@ -42,7 +42,7 @@ type GetQuoteResponse struct { } // Represents the response from an attestation request. -type GetAttestationResponse struct { +type AttestResponse struct { Attestation []byte } @@ -417,7 +417,7 @@ func (c *DstackClient) GetQuote(ctx context.Context, reportData []byte) (*GetQuo } // Gets a versioned attestation from the dstack service. -func (c *DstackClient) GetAttestation(ctx context.Context, reportData []byte) (*GetAttestationResponse, error) { +func (c *DstackClient) Attest(ctx context.Context, reportData []byte) (*AttestResponse, error) { if len(reportData) > 64 { return nil, fmt.Errorf("report data is too large, it should be at most 64 bytes") } @@ -426,7 +426,7 @@ func (c *DstackClient) GetAttestation(ctx context.Context, reportData []byte) (* "report_data": hex.EncodeToString(reportData), } - data, err := c.sendRPCRequest(ctx, "/GetAttestation", payload) + data, err := c.sendRPCRequest(ctx, "/Attest", payload) if err != nil { return nil, err } @@ -443,7 +443,7 @@ func (c *DstackClient) GetAttestation(ctx context.Context, reportData []byte) (* return nil, err } - return &GetAttestationResponse{Attestation: attestation}, nil + return &AttestResponse{Attestation: attestation}, nil } // Sends a request to get information about the CVM instance diff --git a/sdk/js/src/index.ts b/sdk/js/src/index.ts index a8c31eeb8..01d909314 100644 --- a/sdk/js/src/index.ts +++ b/sdk/js/src/index.ts @@ -97,8 +97,8 @@ export interface GetQuoteResponse { replayRtmrs: () => string[] } -export interface GetAttestationResponse { - __name__: Readonly<'GetAttestationResponse'> +export interface AttestResponse { + __name__: Readonly<'AttestResponse'> attestation: Hex } @@ -248,19 +248,19 @@ export class DstackClient { return Object.freeze(result) } - async getAttestation(report_data: string | Buffer | Uint8Array): Promise { + async attest(report_data: string | Buffer | Uint8Array): Promise { let hex = to_hex(report_data) if (hex.length > 128) { throw new Error(`Report data is too large, it should be less than 64 bytes.`) } const payload = JSON.stringify({ report_data: hex }) - const result = await send_rpc_request<{ attestation: string }>(this.endpoint, '/GetAttestation', payload) + const result = await send_rpc_request<{ attestation: string }>(this.endpoint, '/Attest', payload) if ('error' in (result as any)) { const err = (result as any)['error'] as string throw new Error(err) } return Object.freeze({ - __name__: 'GetAttestationResponse', + __name__: 'AttestResponse', attestation: result.attestation as Hex, }) } diff --git a/sdk/python/src/dstack_sdk/__init__.py b/sdk/python/src/dstack_sdk/__init__.py index c23457611..4e1f6e907 100644 --- a/sdk/python/src/dstack_sdk/__init__.py +++ b/sdk/python/src/dstack_sdk/__init__.py @@ -7,7 +7,7 @@ from .dstack_client import DstackClient from .dstack_client import EventLog from .dstack_client import GetKeyResponse -from .dstack_client import GetAttestationResponse +from .dstack_client import AttestResponse from .dstack_client import GetQuoteResponse from .dstack_client import GetTlsKeyResponse from .dstack_client import InfoResponse @@ -33,7 +33,7 @@ # Response types "GetKeyResponse", "GetTlsKeyResponse", - "GetAttestationResponse", + "AttestResponse", "GetQuoteResponse", "InfoResponse", "TcbInfo", diff --git a/sdk/python/src/dstack_sdk/dstack_client.py b/sdk/python/src/dstack_sdk/dstack_client.py index ea22007bb..c62e8b019 100644 --- a/sdk/python/src/dstack_sdk/dstack_client.py +++ b/sdk/python/src/dstack_sdk/dstack_client.py @@ -152,7 +152,7 @@ def replay_rtmrs(self) -> Dict[int, str]: return rtmrs -class GetAttestationResponse(BaseModel): +class AttestResponse(BaseModel): attestation: str def decode_attestation(self) -> bytes: @@ -385,10 +385,10 @@ async def get_quote( result = await self._send_rpc_request("GetQuote", {"report_data": hex}) return GetQuoteResponse(**result) - async def get_attestation( + async def attest( self, report_data: str | bytes, - ) -> GetAttestationResponse: + ) -> AttestResponse: """Request a versioned attestation for the provided report data.""" if not report_data or not isinstance(report_data, (bytes, str)): raise ValueError("report_data can not be empty") @@ -398,8 +398,8 @@ async def get_attestation( if len(report_bytes) > 64: raise ValueError("report_data must be less than 64 bytes") hex = binascii.hexlify(report_bytes).decode() - result = await self._send_rpc_request("GetAttestation", {"report_data": hex}) - return GetAttestationResponse(**result) + result = await self._send_rpc_request("Attest", {"report_data": hex}) + return AttestResponse(**result) async def info(self) -> InfoResponse[TcbInfo]: """Fetch service information including parsed TCB info.""" @@ -518,10 +518,10 @@ def get_quote( raise NotImplementedError @call_async - def get_attestation( + def attest( self, report_data: str | bytes, - ) -> GetAttestationResponse: + ) -> AttestResponse: """Request a versioned attestation for the provided report data.""" raise NotImplementedError diff --git a/sdk/rust/src/dstack_client.rs b/sdk/rust/src/dstack_client.rs index 26d02f275..281b325cd 100644 --- a/sdk/rust/src/dstack_client.rs +++ b/sdk/rust/src/dstack_client.rs @@ -141,14 +141,14 @@ impl DstackClient { Ok(response) } - pub async fn get_attestation(&self, report_data: Vec) -> Result { + pub async fn attest(&self, report_data: Vec) -> Result { if report_data.is_empty() || report_data.len() > 64 { anyhow::bail!("Invalid report data length") } let hex_data = hex_encode(report_data); let data = json!({ "report_data": hex_data }); - let response = self.send_rpc_request("/GetAttestation", &data).await?; - let response = serde_json::from_value::(response)?; + let response = self.send_rpc_request("/Attest", &data).await?; + let response = serde_json::from_value::(response)?; Ok(response) } diff --git a/sdk/rust/types/src/dstack.rs b/sdk/rust/types/src/dstack.rs index d60290d6e..332ddd510 100644 --- a/sdk/rust/types/src/dstack.rs +++ b/sdk/rust/types/src/dstack.rs @@ -117,12 +117,12 @@ pub struct GetQuoteResponse { #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "borsh_schema", derive(BorshSchema))] -pub struct GetAttestationResponse { +pub struct AttestResponse { /// The attestation in hexadecimal format pub attestation: String, } -impl GetAttestationResponse { +impl AttestResponse { pub fn decode_attestation(&self) -> Result, FromHexError> { hex::decode(&self.attestation) } From f5d4aad302bf5d71469ec07ef739475287168f10 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 03:09:08 +0000 Subject: [PATCH 032/264] simulate Attestation --- cert-client/src/lib.rs | 13 ++++--- dstack-util/src/system_setup.rs | 2 +- guest-agent/src/rpc_service.rs | 59 ++++++++++++++++++++------------ sdk/simulator/attestation.bin | Bin 0 -> 38158 bytes sdk/simulator/dstack.toml | 2 +- 5 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 sdk/simulator/attestation.bin diff --git a/cert-client/src/lib.rs b/cert-client/src/lib.rs index 680d51f78..30a366995 100644 --- a/cert-client/src/lib.rs +++ b/cert-client/src/lib.rs @@ -7,7 +7,7 @@ use dstack_kms_rpc::{kms_client::KmsClient, SignCertRequest}; use dstack_types::{AppKeys, KeyProvider}; use ra_rpc::client::{RaClient, RaClientConfig}; use ra_tls::{ - attestation::QuoteContentType, + attestation::{QuoteContentType, VersionedAttestation}, cert::{generate_ra_cert, CaCert, CertConfig, CertSigningRequestV2, Csr}, rcgen::KeyPair, }; @@ -97,13 +97,16 @@ impl CertRequestClient { &self, key: &KeyPair, config: CertConfig, - no_ra: bool, + attestation_override: Option, ) -> Result> { let pubkey = key.public_key_der(); let report_data = QuoteContentType::RaTlsCert.to_report_data(&pubkey); - let attestation = ra_rpc::Attestation::quote(&report_data) - .context("Failed to get quote for cert pubkey")? - .into_versioned(); + let attestation = match attestation_override { + Some(attestation) => attestation, + None => ra_rpc::Attestation::quote(&report_data) + .context("Failed to get quote for cert pubkey")? + .into_versioned(), + }; let csr = CertSigningRequestV2 { confirm: "please sign cert:".to_string(), diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 3c4b7050b..fdf96aadf 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -407,7 +407,7 @@ impl<'a> GatewayContext<'a> { let client_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).context("Failed to generate key")?; let client_certs = cert_client - .request_cert(&client_key, config, false) + .request_cert(&client_key, config, None) .await .context("Failed to request cert")?; let client_cert = client_certs.join("\n"); diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index fb5f33e87..fc15c4395 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -26,7 +26,7 @@ use k256::ecdsa::SigningKey; use or_panic::ResultOrPanic; use ra_rpc::{Attestation, CallContext, RpcCall}; use ra_tls::{ - attestation::{QuoteContentType, DEFAULT_HASH_ALGORITHM}, + attestation::{QuoteContentType, VersionedAttestation, DEFAULT_HASH_ALGORITHM}, cert::CertConfig, kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, }; @@ -59,8 +59,20 @@ struct AppStateInner { } impl AppStateInner { + fn simulator_attestation(&self) -> Result> { + if !self.config.simulator.enabled { + return Ok(None); + } + let attestation_bytes = fs::read(&self.config.simulator.attestation_file) + .context("Failed to read simulator attestation file")?; + let attestation = VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode simulator attestation")?; + Ok(Some(attestation)) + } + async fn request_demo_cert(&self) -> Result { let key = KeyPair::generate().context("Failed to generate demo key")?; + let attestation_override = self.simulator_attestation()?; let demo_cert = self .cert_client .request_cert( @@ -73,7 +85,7 @@ impl AppStateInner { usage_client_auth: true, ext_quote: true, }, - self.config.simulator.enabled, + attestation_override, ) .await .context("Failed to get app cert")? @@ -140,8 +152,13 @@ pub struct InternalRpcHandler { pub async fn get_info(state: &AppState, external: bool) -> Result { let hide_tcb_info = external && !state.config().app_compose.public_tcbinfo; - let Ok(attestation) = Attestation::local() else { - return Ok(AppInfo::default()); + let attestation = if let Some(attestation) = state.inner.simulator_attestation()? { + attestation.into_inner() + } else { + let Ok(attestation) = Attestation::local() else { + return Ok(AppInfo::default()); + }; + attestation }; let app_info = attestation .decode_app_info(false) @@ -214,11 +231,16 @@ impl DstackGuestRpc for InternalRpcHandler { usage_client_auth: request.usage_client_auth, ext_quote: request.usage_ra_tls, }; + let attestation_override = self + .state + .inner + .simulator_attestation() + .context("Failed to load simulator attestation")?; let certificate_chain = self .state .inner .cert_client - .request_cert(&derived_key, config, self.state.config().simulator.enabled) + .request_cert(&derived_key, config, attestation_override) .await .context("Failed to sign the CSR")?; Ok(GetTlsKeyResponse { @@ -388,12 +410,10 @@ impl DstackGuestRpc for InternalRpcHandler { async fn attest(self, request: RawQuoteArgs) -> Result { let report_data = pad64(&request.report_data).context("Report data is too long")?; - if self.state.config().simulator.enabled { - return simulate_attestation( - self.state.config(), - report_data, - &self.state.inner.vm_config, - ); + if let Some(attestation) = self.state.inner.simulator_attestation()? { + return Ok(AttestResponse { + attestation: attestation.to_scale(), + }); } let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; Ok(AttestResponse { @@ -411,16 +431,6 @@ fn pad64(data: &[u8]) -> Option<[u8; 64]> { Some(padded) } -fn simulate_attestation( - config: &Config, - report_data: [u8; 64], - vm_config: &str, -) -> Result { - let attestation = - fs::read(&config.simulator.attestation_file).context("Failed to read attestation file")?; - Ok(AttestResponse { attestation }) -} - fn simulate_quote( config: &Config, report_data: [u8; 64], @@ -478,11 +488,16 @@ impl TappdRpc for InternalRpcHandlerV0 { usage_client_auth: request.usage_client_auth, ext_quote: request.usage_ra_tls, }; + let attestation_override = self + .state + .inner + .simulator_attestation() + .context("Failed to load simulator attestation")?; let certificate_chain = self .state .inner .cert_client - .request_cert(&derived_key, config, self.state.config().simulator.enabled) + .request_cert(&derived_key, config, attestation_override) .await .context("Failed to sign the CSR")?; Ok(GetTlsKeyResponse { diff --git a/sdk/simulator/attestation.bin b/sdk/simulator/attestation.bin new file mode 100644 index 0000000000000000000000000000000000000000..18da636435dfbb371629ad76d43925defdb946c6 GIT binary patch literal 38158 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 literal 0 HcmV?d00001 diff --git a/sdk/simulator/dstack.toml b/sdk/simulator/dstack.toml index ab5bcebdb..a8c5b4bd8 100644 --- a/sdk/simulator/dstack.toml +++ b/sdk/simulator/dstack.toml @@ -19,6 +19,7 @@ sys_config_file = "sys-config.json" enabled = true quote_file = "quote.hex" event_log_file = "eventlog.json" +attestation_file = "attestation.bin" [internal-v0] address = "unix:./tappd.sock" @@ -35,4 +36,3 @@ reuse = true [guest-api] address = "unix:./guest.sock" reuse = true - From 83da3f20e173f006cb42d8c1004a179bef7fc2c8 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 03:12:55 +0000 Subject: [PATCH 033/264] Add tests for fn attest to sdk --- sdk/go/dstack/client_test.go | 20 ++++++++++++++++++++ sdk/js/src/__tests__/index.test.ts | 12 ++++++++++++ sdk/python/src/dstack_sdk/__init__.py | 2 +- sdk/python/tests/test_client.py | 16 ++++++++++++++++ sdk/rust/tests/test_client.rs | 11 +++++++++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/sdk/go/dstack/client_test.go b/sdk/go/dstack/client_test.go index ee8df0ff3..a6b3ae375 100644 --- a/sdk/go/dstack/client_test.go +++ b/sdk/go/dstack/client_test.go @@ -96,6 +96,26 @@ func TestGetQuote(t *testing.T) { } } +func TestAttest(t *testing.T) { + client := dstack.NewDstackClient() + resp, err := client.Attest(context.Background(), []byte("test")) + if err != nil { + t.Fatal(err) + } + + if len(resp.Attestation) == 0 { + t.Error("expected attestation to not be empty") + } + + _, err = client.Attest(context.Background(), bytes.Repeat([]byte("a"), 65)) + if err == nil { + t.Fatal("expected error for report data larger than 64 bytes") + } + if !strings.Contains(err.Error(), "report data is too large") { + t.Fatalf("expected error to mention report data size, got: %v", err) + } +} + func TestGetTlsKey(t *testing.T) { client := dstack.NewDstackClient() altNames := []string{"localhost"} diff --git a/sdk/js/src/__tests__/index.test.ts b/sdk/js/src/__tests__/index.test.ts index dea1a1eb7..a3a732ef7 100644 --- a/sdk/js/src/__tests__/index.test.ts +++ b/sdk/js/src/__tests__/index.test.ts @@ -49,6 +49,13 @@ describe('DstackClient', () => { expect(result.replayRtmrs().length).toBe(4) }) + it('should be able to attest', async () => { + const client = new DstackClient() + const result = await client.attest('test') + expect(result).toHaveProperty('attestation') + expect(result.attestation).not.toBe('') + }) + it('should able to get derive key result as uint8array', async () => { const client = new DstackClient() const result = await client.getKey('/', 'test') @@ -87,6 +94,11 @@ describe('DstackClient', () => { await expect(() => client.getQuote(input)).rejects.toThrow() }) + it('should throw error on attest report_data larger than 64 bytes', async () => { + const client = new DstackClient() + await expect(() => client.attest(Buffer.alloc(65))).rejects.toThrow() + }) + it('should be able to get info', async () => { const client = new DstackClient() const result = await client.info() diff --git a/sdk/python/src/dstack_sdk/__init__.py b/sdk/python/src/dstack_sdk/__init__.py index 4e1f6e907..ecf744142 100644 --- a/sdk/python/src/dstack_sdk/__init__.py +++ b/sdk/python/src/dstack_sdk/__init__.py @@ -4,10 +4,10 @@ from .dstack_client import AsyncDstackClient from .dstack_client import AsyncTappdClient +from .dstack_client import AttestResponse from .dstack_client import DstackClient from .dstack_client import EventLog from .dstack_client import GetKeyResponse -from .dstack_client import AttestResponse from .dstack_client import GetQuoteResponse from .dstack_client import GetTlsKeyResponse from .dstack_client import InfoResponse diff --git a/sdk/python/tests/test_client.py b/sdk/python/tests/test_client.py index 437acf953..6b0d9b1bf 100644 --- a/sdk/python/tests/test_client.py +++ b/sdk/python/tests/test_client.py @@ -10,6 +10,7 @@ from dstack_sdk import AsyncDstackClient from dstack_sdk import AsyncTappdClient +from dstack_sdk import AttestResponse from dstack_sdk import DstackClient from dstack_sdk import GetKeyResponse from dstack_sdk import GetQuoteResponse @@ -43,6 +44,13 @@ def test_sync_client_get_quote(): assert isinstance(result, GetQuoteResponse) +def test_sync_client_attest(): + client = DstackClient() + result = client.attest("test") + assert isinstance(result, AttestResponse) + assert len(result.attestation) > 0 + + def test_sync_client_get_tls_key(): client = DstackClient() result = client.get_tls_key() @@ -98,6 +106,14 @@ async def test_async_client_get_quote(): assert isinstance(result, GetQuoteResponse) +@pytest.mark.asyncio +async def test_async_client_attest(): + client = AsyncDstackClient() + result = await client.attest("test") + assert isinstance(result, AttestResponse) + assert len(result.attestation) > 0 + + @pytest.mark.asyncio async def test_async_client_get_tls_key(): client = AsyncDstackClient() diff --git a/sdk/rust/tests/test_client.rs b/sdk/rust/tests/test_client.rs index e7be67e08..3010a1104 100644 --- a/sdk/rust/tests/test_client.rs +++ b/sdk/rust/tests/test_client.rs @@ -24,6 +24,17 @@ async fn test_async_client_get_quote() { assert!(!result.quote.is_empty()); } +#[tokio::test] +async fn test_async_client_attest() { + let client = AsyncDstackClient::new(None); + let result = client.attest(b"test".to_vec()).await.unwrap(); + let attestation = result.decode_attestation().unwrap(); + assert!(!attestation.is_empty()); + + let too_large = client.attest(vec![0_u8; 65]).await; + assert!(too_large.is_err()); +} + #[tokio::test] async fn test_async_client_get_tls_key() { let client = AsyncDstackClient::new(None); From 9dc7ce4deb9343e5bee7cfe3fec387c5f6964284 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 03:12:13 +0000 Subject: [PATCH 034/264] Remove unused code --- dstack-util/src/main.rs | 36 ------------------------------------ ra-tls/src/cert.rs | 3 --- tpm-attest/src/gcp_ak.rs | 1 - verifier/src/verification.rs | 24 ------------------------ 4 files changed, 64 deletions(-) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index cd73e011c..14c8f3bf9 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -16,7 +16,6 @@ use ra_tls::{ kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, rcgen::KeyPair, }; -use scale::Decode; use std::path::Path; use std::{ io::{self, Read, Write}, @@ -394,41 +393,6 @@ fn cmd_rand(rand_args: RandArgs) -> Result<()> { Ok(()) } -#[derive(Decode)] -struct ParsedReport { - attributes: [u8; 8], - xfam: [u8; 8], - mrtd: [u8; 48], - mrconfigid: [u8; 48], - mrowner: [u8; 48], - mrownerconfig: [u8; 48], - rtmr0: [u8; 48], - rtmr1: [u8; 48], - rtmr2: [u8; 48], - rtmr3: [u8; 48], - servtd_hash: [u8; 48], -} - -impl core::fmt::Debug for ParsedReport { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - use hex_fmt::HexFmt as HF; - - f.debug_struct("ParsedReport") - .field("attributes", &HF(&self.attributes)) - .field("xfam", &HF(&self.xfam)) - .field("mrtd", &HF(&self.mrtd)) - .field("mrconfigid", &HF(&self.mrconfigid)) - .field("mrowner", &HF(&self.mrowner)) - .field("mrownerconfig", &HF(&self.mrownerconfig)) - .field("rtmr0", &HF(&self.rtmr0)) - .field("rtmr1", &HF(&self.rtmr1)) - .field("rtmr2", &HF(&self.rtmr2)) - .field("rtmr3", &HF(&self.rtmr3)) - .field("servtd_hash", &HF(&self.servtd_hash)) - .finish() - } -} - fn cmd_show_mrs() -> Result<()> { let attestation = ra_tls::attestation::Attestation::local().context("Failed to get attestation")?; diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index ae3c224b5..275e5eb93 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -521,9 +521,6 @@ pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result, - pub rtmr0: Vec, - pub rtmr1: Vec, - pub rtmr2: Vec, - pub rtmr3: Vec, - pub mr_aggregated: Vec, - pub os_image_hash: Vec, - pub mr_system: Vec, - pub app_id: Vec, - pub compose_hash: Vec, - pub instance_id: Vec, - pub device_id: Vec, - pub key_provider_info: Vec, - pub event_log: String, - pub tcb_status: String, - pub advisory_ids: Vec, - } -} From c22155111607bfee350e6b224efeea4f62d6befd Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 03:48:38 +0000 Subject: [PATCH 035/264] ra-tls: Refactor add_ext and strip code --- ra-tls/src/attestation.rs | 15 +++++++++++ ra-tls/src/cert.rs | 57 +++++++++++++-------------------------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index ce134884b..683096d76 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -270,6 +270,21 @@ impl VersionedAttestation { Self::V0 { attestation } => attestation, } } + + /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). + pub fn into_stripped(mut self) -> Self { + let VersionedAttestation::V0 { attestation } = &mut self; + if let Some(tdx_quote) = &mut attestation.tdx_quote { + let rtmr3_only: Vec<_> = tdx_quote + .event_log + .iter() + .filter(|e| e.imr == 3) + .cloned() + .collect(); + tdx_quote.event_log = cc_eventlog::strip_event_log_payloads(&rtmr3_only); + } + self + } } /// Attestation data diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 275e5eb93..6545e3f8f 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -342,58 +342,28 @@ impl CertRequest<'_, Key> { } } if let Some(app_id) = self.app_id { - let content = yasna::construct_der(|writer| { - writer.write_bytes(app_id); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_APP_ID, content); - params.custom_extensions.push(ext); + add_ext(&mut params, PHALA_RATLS_APP_ID, app_id); } - if let Some(special_usage) = self.special_usage { - let content = yasna::construct_der(|writer| { - writer.write_bytes(special_usage.as_bytes()); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_CERT_USAGE, content); - params.custom_extensions.push(ext); + if let Some(usage) = self.special_usage { + add_ext(&mut params, PHALA_RATLS_CERT_USAGE, usage); } if let Some(ver_att) = self.attestation { - let mut ver_att = ver_att.clone(); - let VersionedAttestation::V0 { attestation } = &mut ver_att; - if let Some(tdx_quote) = &mut attestation.tdx_quote { - let rtmr3_only: Vec<_> = tdx_quote - .event_log - .iter() - .filter(|e| e.imr == 3) - .cloned() - .collect(); - tdx_quote.event_log = cc_eventlog::strip_event_log_payloads(&rtmr3_only); - } - + let VersionedAttestation::V0 { attestation } = &ver_att; match attestation.mode { AttestationMode::DstackTdx => { // For backward compatibility, we serialize the quote to the classic oids. let Some(tdx_quote) = &attestation.tdx_quote else { bail!("missing tdx quote") }; - let content = yasna::construct_der(|writer| { - writer.write_bytes(&tdx_quote.quote); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_TDX_QUOTE, content); - params.custom_extensions.push(ext); let event_log = serde_json::to_vec(&tdx_quote.event_log) .context("Failed to serialize event log")?; - let content = yasna::construct_der(|writer| { - writer.write_bytes(&event_log); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_EVENT_LOG, content); - params.custom_extensions.push(ext); + add_ext(&mut params, PHALA_RATLS_TDX_QUOTE, &tdx_quote.quote); + add_ext(&mut params, PHALA_RATLS_EVENT_LOG, &event_log); } _ => { - let attestation_bytes = ver_att.to_scale(); - let content = yasna::construct_der(|writer| { - writer.write_bytes(&attestation_bytes); - }); - let ext = CustomExtension::from_oid_content(PHALA_RATLS_ATTESTATION, content); - params.custom_extensions.push(ext); + // The event logs are too large on GCP TDX to put in the certificate, so we strip them + let attestation_bytes = ver_att.clone().into_stripped().to_scale(); + add_ext(&mut params, PHALA_RATLS_ATTESTATION, &attestation_bytes); } } } @@ -417,6 +387,15 @@ impl CertRequest<'_, Key> { } } +fn add_ext(params: &mut CertificateParams, oid: &[u64], content: impl AsRef<[u8]>) { + let content = yasna::construct_der(|writer| { + writer.write_bytes(content.as_ref()); + }); + params + .custom_extensions + .push(CustomExtension::from_oid_content(oid, content)); +} + impl CertRequest<'_, KeyPair> { /// Create a self-signed certificate. pub fn self_signed(self) -> Result { From f4e4f483e48e8e7aceb29e5b6f30604c49591804 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 03:16:16 +0000 Subject: [PATCH 036/264] dstack-util: add attest info subcommand --- dstack-util/src/main.rs | 58 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index 14c8f3bf9..b9f8c5d78 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -11,11 +11,12 @@ use host_api::HostApi; use k256::schnorr::SigningKey; use ra_rpc::Attestation; use ra_tls::{ - attestation::QuoteContentType, + attestation::{QuoteContentType, VersionedAttestation}, cert::generate_ra_cert, kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, rcgen::KeyPair, }; +use scale::Encode; use std::path::Path; use std::{ io::{self, Read, Write}, @@ -79,6 +80,8 @@ enum Commands { QuoteReport(QuoteReportArgs), /// Generate a versioned attestation for simulator use Attest(AttestArgs), + /// Show size breakdown for a versioned attestation file + AttestInfo(AttestInfoArgs), } #[derive(Parser)] @@ -294,6 +297,13 @@ struct AttestArgs { hex: bool, } +#[derive(Parser)] +struct AttestInfoArgs { + /// input file (default: attestation.bin) + #[arg(short, long)] + input: Option, +} + fn pad64(data: &[u8]) -> Result<[u8; 64]> { if data.len() > 64 { anyhow::bail!("report_data must be at most 64 bytes"); @@ -357,6 +367,49 @@ fn cmd_attest(args: AttestArgs) -> Result<()> { Ok(()) } +fn cmd_attest_info(args: AttestInfoArgs) -> Result<()> { + let input = args + .input + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + let data = fs::read(&input).context("Failed to read attestation file")?; + let attestation = + VersionedAttestation::from_scale(&data).context("Failed to decode attestation")?; + + println!("file: {}", input.display()); + println!("total_bytes: {}", data.len()); + + match attestation { + VersionedAttestation::V0 { attestation } => { + println!("version: V0"); + println!("mode: {:?}", attestation.mode); + println!("config_bytes: {}", attestation.config.as_bytes().len()); + match attestation.tdx_quote { + Some(tdx) => { + let event_log_json = serde_json::to_vec(&tdx.event_log) + .context("Failed to serialize event log")?; + println!("tdx_quote_bytes: {}", tdx.quote.len()); + println!("event_log_entries: {}", tdx.event_log.len()); + println!("event_log_json_bytes: {}", event_log_json.len()); + } + None => { + println!("tdx_quote_bytes: 0"); + println!("event_log_entries: 0"); + println!("event_log_json_bytes: 0"); + } + } + match attestation.tpm_quote { + Some(tpm) => { + let tpm_bytes = tpm.encode(); + println!("tpm_quote_bytes: {}", tpm_bytes.len()); + } + None => println!("tpm_quote_bytes: 0"), + } + } + } + + Ok(()) +} + fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; io::stdin() @@ -1015,6 +1068,9 @@ async fn main() -> Result<()> { Commands::Attest(args) => { cmd_attest(args)?; } + Commands::AttestInfo(args) => { + cmd_attest_info(args)?; + } } Ok(()) From bde50028c75029f1d8a7fd71e5cbc47903fca39d Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 03:48:07 +0000 Subject: [PATCH 037/264] dstack-util: Add attest-json and attest-strip subcommands --- dstack-util/src/main.rs | 90 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index b9f8c5d78..df15d0097 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -82,6 +82,10 @@ enum Commands { Attest(AttestArgs), /// Show size breakdown for a versioned attestation file AttestInfo(AttestInfoArgs), + /// Dump a versioned attestation as JSON + AttestJson(AttestJsonArgs), + /// Strip attestation for certificate embedding + AttestStrip(AttestStripArgs), } #[derive(Parser)] @@ -304,6 +308,28 @@ struct AttestInfoArgs { input: Option, } +#[derive(Parser)] +struct AttestJsonArgs { + /// input file (default: attestation.bin) + #[arg(short, long)] + input: Option, + + /// output file (default: stdout) + #[arg(short, long)] + output: Option, +} + +#[derive(Parser)] +struct AttestStripArgs { + /// input file (default: attestation.bin) + #[arg(short, long)] + input: Option, + + /// output file (default: attestation.strip.bin) + #[arg(short, long)] + output: Option, +} + fn pad64(data: &[u8]) -> Result<[u8; 64]> { if data.len() > 64 { anyhow::bail!("report_data must be at most 64 bytes"); @@ -410,6 +436,64 @@ fn cmd_attest_info(args: AttestInfoArgs) -> Result<()> { Ok(()) } +fn cmd_attest_json(args: AttestJsonArgs) -> Result<()> { + let input = args + .input + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + let data = fs::read(&input).context("Failed to read attestation file")?; + let attestation = + VersionedAttestation::from_scale(&data).context("Failed to decode attestation")?; + + let json = match attestation { + VersionedAttestation::V0 { attestation } => { + let mode = serde_json::to_value(attestation.mode) + .context("Failed to serialize attestation mode")?; + let tdx_quote = match attestation.tdx_quote { + Some(tdx) => serde_json::json!({ + "quote": hex::encode(tdx.quote), + "event_log": tdx.event_log, + }), + None => serde_json::Value::Null, + }; + let tpm_quote = match attestation.tpm_quote { + Some(tpm) => serde_json::to_value(tpm).context("Failed to serialize TPM quote")?, + None => serde_json::Value::Null, + }; + + serde_json::json!({ + "version": "V0", + "mode": mode, + "config": attestation.config, + "tdx_quote": tdx_quote, + "tpm_quote": tpm_quote, + }) + } + }; + + let output = serde_json::to_string_pretty(&json).context("Failed to serialize JSON")?; + if let Some(path) = args.output { + fs::write(&path, output).context("Failed to write JSON output")?; + } else { + println!("{output}"); + } + Ok(()) +} + +fn cmd_attest_strip(args: AttestStripArgs) -> Result<()> { + let input = args + .input + .unwrap_or_else(|| PathBuf::from("attestation.bin")); + let data = fs::read(&input).context("Failed to read attestation file")?; + let attestation = + VersionedAttestation::from_scale(&data).context("Failed to decode attestation")?; + let stripped = attestation.into_stripped(); + let output = args + .output + .unwrap_or_else(|| PathBuf::from("attestation.strip.bin")); + fs::write(&output, stripped.to_scale()).context("Failed to write stripped attestation")?; + Ok(()) +} + fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; io::stdin() @@ -1071,6 +1155,12 @@ async fn main() -> Result<()> { Commands::AttestInfo(args) => { cmd_attest_info(args)?; } + Commands::AttestJson(args) => { + cmd_attest_json(args)?; + } + Commands::AttestStrip(args) => { + cmd_attest_strip(args)?; + } } Ok(()) From f5d4a23d1aee2454c57cada811e36860d14ae9e6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 12:44:04 +0000 Subject: [PATCH 038/264] Refactor cc eventlog --- Cargo.lock | 37 +- Cargo.toml | 4 +- cc-eventlog/Cargo.toml | 2 + cc-eventlog/src/lib.rs | 327 +----------- cc-eventlog/src/runtime_events.rs | 112 ++++ cc-eventlog/src/tcg.rs | 170 +++++- cc-eventlog/src/tdx.rs | 101 ++++ dstack-attest/Cargo.toml | 34 ++ dstack-attest/src/attestation.rs | 774 +++++++++++++++++++++++++++ dstack-attest/src/lib.rs | 32 ++ dstack-util/Cargo.toml | 2 + dstack-util/src/main.rs | 9 +- dstack-util/src/system_setup.rs | 40 +- guest-agent/Cargo.toml | 2 + guest-agent/src/rpc_service.rs | 11 +- ra-rpc/src/client.rs | 7 +- ra-rpc/src/rocket_helper.rs | 4 +- ra-tls/Cargo.toml | 5 +- ra-tls/src/attestation.rs | 839 ++---------------------------- ra-tls/src/cert.rs | 20 +- ra-tls/src/lib.rs | 1 + tdx-attest/src/lib.rs | 15 - tdx-attest/src/linux.rs | 28 +- tpm-attest/src/lib.rs | 2 +- verifier/src/main.rs | 4 +- verifier/src/verification.rs | 26 +- 26 files changed, 1374 insertions(+), 1234 deletions(-) create mode 100644 cc-eventlog/src/runtime_events.rs create mode 100644 cc-eventlog/src/tdx.rs create mode 100644 dstack-attest/Cargo.toml create mode 100644 dstack-attest/src/attestation.rs create mode 100644 dstack-attest/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 275893489..fb3e752ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1282,6 +1282,8 @@ name = "cc-eventlog" version = "0.5.5" dependencies = [ "anyhow", + "digest 0.10.7", + "ez-hash", "fs-err", "hex", "insta", @@ -2135,6 +2137,31 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dstack-attest" +version = "0.5.5" +dependencies = [ + "anyhow", + "cc-eventlog", + "dcap-qvl", + "dstack-types", + "ez-hash", + "fs-err", + "hex", + "hex_fmt", + "or-panic", + "parity-scale-codec", + "serde", + "serde-human-bytes", + "serde_json", + "sha2 0.10.9", + "sha3", + "tdx-attest", + "tpm-attest", + "tpm-qvl", + "tpm-types", +] + [[package]] name = "dstack-gateway" version = "0.5.5" @@ -2204,11 +2231,13 @@ dependencies = [ "anyhow", "base64 0.22.1", "bollard", + "cc-eventlog", "cert-client", "chrono", "clap", "cmd_lib", "default-net", + "dstack-attest", "dstack-guest-agent-rpc", "dstack-types", "ed25519-dalek", @@ -2406,11 +2435,13 @@ dependencies = [ "aes-gcm", "anyhow", "bollard", + "cc-eventlog", "cert-client", "clap", "cmd_lib", "curve25519-dalek", "dcap-qvl", + "dstack-attest", "dstack-gateway-rpc", "dstack-kms-rpc", "dstack-types", @@ -2749,9 +2780,9 @@ dependencies = [ [[package]] name = "ez-hash" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d55891018a9c34579a8ec3c9d7d503699a68176f961eac8f06b27129f8e4520c" +checksum = "42b3b3adc5fbbc9e21416d5b721b1bccb501a87d7b32ac89f2c7cea229d40772" dependencies = [ "blake2", "blake3", @@ -5618,6 +5649,7 @@ dependencies = [ "bon", "cc-eventlog", "dcap-qvl", + "dstack-attest", "dstack-types", "elliptic-curve", "ez-hash", @@ -5639,7 +5671,6 @@ dependencies = [ "sha2 0.10.9", "sha3", "tdx-attest", - "tpm-attest", "tpm-qvl", "tpm-types", "tracing", diff --git a/Cargo.toml b/Cargo.toml index cdb3bef36..0b2e19391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "tpm-attest", "tpm-types", "tpm-qvl", + "dstack-attest", "dstack-util", "iohash", "guest-agent", @@ -72,6 +73,7 @@ supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } tpm-attest = { path = "tpm-attest" } tpm-types = { path = "tpm-types" } +dstack-attest = { path = "dstack-attest" } tpm-qvl = { path = "tpm-qvl" } certbot = { path = "certbot" } rocket-vsock-listener = { path = "rocket-vsock-listener" } @@ -186,7 +188,7 @@ xsalsa20poly1305 = "0.9.0" salsa20 = "0.10" rand_core = "0.6.4" alloy = { version = "1.0.32", default-features = false } -ez-hash = "1.0.0" +ez-hash = "1.1.0" # Certificate/DNS hickory-resolver = "0.24.4" diff --git a/cc-eventlog/Cargo.toml b/cc-eventlog/Cargo.toml index 9ff79736e..2863760f7 100644 --- a/cc-eventlog/Cargo.toml +++ b/cc-eventlog/Cargo.toml @@ -12,6 +12,8 @@ license.workspace = true [dependencies] anyhow.workspace = true +digest = "0.10.7" +ez-hash.workspace = true fs-err.workspace = true hex.workspace = true scale.workspace = true diff --git a/cc-eventlog/src/lib.rs b/cc-eventlog/src/lib.rs index 6c18c75cc..93bbc77ff 100644 --- a/cc-eventlog/src/lib.rs +++ b/cc-eventlog/src/lib.rs @@ -2,330 +2,15 @@ // // SPDX-License-Identifier: Apache-2.0 -use crate::codecs::VecOf; -use anyhow::{Context, Result}; -use scale::{Decode, Encode}; -use serde::{Deserialize, Serialize}; -use sha2::Digest; -use tcg::{TcgDigest, TcgEfiSpecIdEvent}; +pub use runtime_events::{replay_events, RuntimeEvent}; +pub use tdx::TdxEvent; mod codecs; +mod runtime_events; mod tcg; +pub mod tdx; pub mod tpm; -/// The path to the userspace TDX event log file. -pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/tdx_mr3/tdx_events.log"; -/// The path to boottime ccel file. -const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL"; -/// The event type for dstack runtime events. -/// This code is not defined in the TCG specification. -/// See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf -pub const DSTACK_RUNTIME_EVENT_TYPE: u32 = 0x08000001; - -/// This is the common struct for tcg event logs to be delivered in different formats. -/// Currently TCG supports several event log formats defined in TCG_PCClient Spec, -/// Canonical Eventlog Spec, etc. -/// This struct provides the functionality to convey event logs in different format -/// according to request. -#[derive(Clone, scale::Decode)] -pub struct TcgEventLog { - /// IMR index, starts from 1 - pub imr_index: u32, - /// Event type - pub event_type: u32, - /// List of digests - pub digests: VecOf, - /// Raw event data - pub event: VecOf, -} - -/// This is the TDX event log format that is used to store the event log in the TDX guest. -/// It is a simplified version of the TCG event log format, containing only a single digest -/// and the raw event data. The IMR index is zero-based, unlike the TCG event log format -/// which is one-based. -/// -/// As for RTMR3, the digest extended is calculated as `sha384(event_type.to_ne_bytes() || b":" || event || b":" || event_payload)`. -#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] -pub struct TdxEventLogEntry { - /// IMR index, starts from 0 - pub imr: u32, - /// Event type - pub event_type: u32, - /// Digest - #[serde(with = "serde_human_bytes", default)] - digest: Vec, - /// Event name - pub event: String, - /// Event payload - #[serde(with = "serde_human_bytes")] - pub event_payload: Vec, -} - -fn event_digest(ty: u32, event: &str, payload: &[u8]) -> [u8; 48] { - use sha2::Digest; - let mut hasher = sha2::Sha384::new(); - hasher.update(ty.to_ne_bytes()); - hasher.update(b":"); - hasher.update(event.as_bytes()); - hasher.update(b":"); - hasher.update(payload); - hasher.finalize().into() -} - -impl TdxEventLogEntry { - pub fn new(imr: u32, event_type: u32, event: String, event_payload: Vec) -> Self { - Self { - imr, - event_type, - digest: vec![], - event, - event_payload, - } - } - - /// Create a version of this event with payload stripped (for size reduction). - /// Only call this on events where can_strip_payload() returns true. - pub fn stripped(&self) -> Self { - if self.is_dstack_runtime_event() { - Self { - imr: self.imr, - event_type: self.event_type, - digest: Vec::new(), - event: self.event.clone(), - event_payload: self.event_payload.clone(), - } - } else { - Self { - imr: self.imr, - event_type: self.event_type, - digest: self.digest.clone(), - event: self.event.clone(), - event_payload: Vec::new(), - } - } - } - - pub fn digest(&self) -> Vec { - if self.is_dstack_runtime_event() { - return event_digest(self.event_type, &self.event, &self.event_payload).to_vec(); - } - self.digest.clone() - } - - pub fn is_dstack_runtime_event(&self) -> bool { - self.event_type == DSTACK_RUNTIME_EVENT_TYPE - } -} - -/// Strip large payloads from event logs to reduce size. -/// CCEL boot-time events only need their digest for RTMR replay, not the payload. -/// Runtime events need their payload for verification, so those are kept. -pub fn strip_event_log_payloads(events: &[TdxEventLogEntry]) -> Vec { - events.iter().map(|e| e.stripped()).collect() -} - -impl TryFrom for TdxEventLogEntry { - type Error = anyhow::Error; - - fn try_from(value: TcgEventLog) -> Result { - if value.digests.len() != 1 { - return Err(anyhow::anyhow!( - "expected 1 digest, got {}", - value.digests.len() - )); - } - let digest = value - .digests - .into_inner() - .into_iter() - .next() - .context("digest not found")? - .hash; - Ok(TdxEventLogEntry { - imr: value - .imr_index - .checked_sub(1) - .context("invalid IMR index: must be >= 1")?, - event_type: value.event_type, - digest, - event: Default::default(), - event_payload: value.event.into(), - }) - } -} - -impl core::fmt::Debug for TcgEventLog { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("TcgEventLog") - .field("imr_index", &self.imr_index) - .field("event_type", &self.event_type) - .field( - "digests", - &self - .digests - .iter() - .map(|d| hex::encode(&d.hash)) - .collect::>(), - ) - .field("event", &hex::encode(&self.event)) - .finish() - } -} - -const fn alg_id_to_digest_size(alg_id: u16) -> Option { - use tcg::*; - match alg_id { - TPM_ALG_SHA1 => Some(20), - TPM_ALG_SHA256 => Some(32), - TPM_ALG_SHA384 => Some(48), - TPM_ALG_SHA512 => Some(64), - _ => None, - } -} - -#[derive(Clone, Debug)] -pub struct EventLogs { - pub spec_id_header_event: TcgEfiSpecIdEvent, - pub event_logs: Vec, -} - -impl scale::Decode for TcgDigest { - fn decode(input: &mut I) -> Result { - let algo_id = u16::decode(input)?; - let digest_size = - alg_id_to_digest_size(algo_id).ok_or(scale::Error::from("Unsupported algorithm ID"))?; - let mut digest_data = vec![0; digest_size as usize]; - input - .read(&mut digest_data) - .map_err(|_| scale::Error::from("failed to read digest_data"))?; - Ok(TcgDigest { - algo_id, - hash: digest_data, - }) - } -} - -impl EventLogs { - pub fn decode(input: &mut &[u8]) -> Result { - let (_spec_id_header, spec_id_header_event) = - parse_spec_id_event_log(input).context("Failed to parse spec id event")?; - let mut event_logs = vec![]; - loop { - // A tmp head_buffer is used to peek the imr and event type - let head_buffer = &mut &input[..]; - let imr = u32::decode(head_buffer).context("failed to decode imr")?; - if imr == 0xFFFFFFFF { - break; - } - let event_log = TcgEventLog::decode(input).context("Failed to parse event log")?; - event_logs.push(event_log); - } - Ok(EventLogs { - spec_id_header_event, - event_logs, - }) - } - - pub fn decode_from_ccel_file() -> Result { - let data = fs_err::read(CCEL_FILE).context("Failed to read CCEL")?; - Self::decode(&mut data.as_slice()) - } - - pub fn to_tdx_event_logs(&self) -> Result> { - self.event_logs - .iter() - .filter(|log| log.imr_index > 0) // GCP fills some IMRs starting from 0 - .cloned() - .map(TdxEventLogEntry::try_from) - .collect() - } -} - -fn parse_spec_id_event_log( - input: &mut I, -) -> Result<(TcgEventLog, TcgEfiSpecIdEvent)> { - #[derive(Decode)] - struct Header { - imr_index: u32, - header_event_type: u32, - digest_hash: [u8; 20], - header_event: VecOf, - } - - let decoded_header = Header::decode(input).context("failed to decode log_item")?; - // Parse EFI Spec Id Event structure - let input = &mut decoded_header.header_event.as_slice(); - let spec_id_event = - TcgEfiSpecIdEvent::decode(input).context("failed to decode TcgEfiSpecIdEvent")?; - - let digests = vec![TcgDigest { - algo_id: tcg::TPM_ALG_ERROR, - hash: decoded_header.digest_hash.to_vec(), - }]; - let spec_id_header = TcgEventLog { - imr_index: decoded_header.imr_index, - event_type: decoded_header.header_event_type, - digests: (digests.len() as u32, digests).into(), - event: decoded_header.header_event, - }; - Ok((spec_id_header, spec_id_event)) -} - -/// Read runtime event logs from the runtime event log file -pub fn read_runtime_event_logs() -> Result> { - let data = match fs_err::read_to_string(RUNTIME_EVENT_LOG_FILE) { - Ok(data) => data, - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - return Ok(vec![]); - } - return Err(e).context("Failed to read user event log"); - } - }; - let mut event_logs = vec![]; - for line in data.lines() { - if line.trim().is_empty() { - continue; - } - let event_log = serde_json::from_str::(line) - .context("Failed to decode user event log")?; - event_logs.push(event_log); - } - Ok(event_logs) -} - -/// Read both boottime and runtime event logs. -pub fn read_event_logs() -> Result> { - let mut event_logs = EventLogs::decode_from_ccel_file()?.to_tdx_event_logs()?; - event_logs.extend(read_runtime_event_logs()?); - Ok(event_logs) -} - -/// Replay event logs -pub fn replay_event_logs( - eventlog: &[TdxEventLogEntry], - to_event: Option<&str>, - imr: u32, -) -> Result<[u8; 48]> { - let mut mr = [0u8; 48]; - - for event in eventlog.iter() { - if event.imr == imr { - let mut hasher = sha2::Sha384::new(); - hasher.update(mr); - hasher.update(event.digest()); - mr = hasher.finalize().into(); - } - if let Some(to_event) = to_event { - if event.event == to_event { - break; - } - } - } - - Ok(mr) -} - #[cfg(test)] mod tests { use super::*; @@ -333,9 +18,9 @@ mod tests { #[test] fn parse_ccel() { let boot_time_data = include_bytes!("../samples/ccel.bin"); - let event_logs = EventLogs::decode(&mut boot_time_data.as_slice()).unwrap(); + let event_logs = tcg::TcgEventLog::decode(&mut boot_time_data.as_slice()).unwrap(); insta::assert_debug_snapshot!(&event_logs.event_logs); - let tdx_event_logs = event_logs.to_tdx_event_logs().unwrap(); + let tdx_event_logs = event_logs.to_cc_event_log().unwrap(); let json = serde_json::to_string_pretty(&tdx_event_logs).unwrap(); insta::assert_snapshot!(json); } diff --git a/cc-eventlog/src/runtime_events.rs b/cc-eventlog/src/runtime_events.rs new file mode 100644 index 000000000..fa566bb3b --- /dev/null +++ b/cc-eventlog/src/runtime_events.rs @@ -0,0 +1,112 @@ +use anyhow::{Context, Result}; +use fs_err as fs; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use std::io::Write; + +use ez_hash::{Hasher, Sha256, Sha384}; + +/// The event type for dstack runtime events. +/// This code is not defined in the TCG specification. +/// See https://trustedcomputinggroup.org/wp-content/uploads/PC-ClientSpecific_Platform_Profile_for_TPM_2p0_Systems_v51.pdf +pub const DSTACK_RUNTIME_EVENT_TYPE: u32 = 0x08000001; +/// The path to the userspace TDX event log file. +pub const RUNTIME_EVENT_LOG_FILE: &str = "/run/log/dstack/runtime_events.log"; + +/// Abstraction of cross-platform runtime events. +#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] +pub struct RuntimeEvent { + /// Event name + pub event: String, + /// Event payload + pub payload: Vec, +} + +impl RuntimeEvent { + pub fn new(event: String, payload: Vec) -> Self { + Self { event, payload } + } + + pub fn read_all() -> Result> { + let data = match fs_err::read_to_string(RUNTIME_EVENT_LOG_FILE) { + Ok(data) => data, + Err(e) => { + if e.kind() == std::io::ErrorKind::NotFound { + return Ok(vec![]); + } + return Err(e).context("Failed to read user event log"); + } + }; + let mut event_logs = vec![]; + for line in data.lines() { + if line.trim().is_empty() { + continue; + } + let event_log = serde_json::from_str::(line) + .context("Failed to decode user event log")?; + event_logs.push(event_log); + } + Ok(event_logs) + } + + pub fn emit(&self) -> Result<()> { + let logline = serde_json::to_string(self).context("failed to serialize event log")?; + + let logfile_path = std::path::Path::new(RUNTIME_EVENT_LOG_FILE); + let logfile_dir = logfile_path + .parent() + .context("failed to get event log directory")?; + fs::create_dir_all(logfile_dir).context("failed to create event log directory")?; + + let mut logfile = fs::OpenOptions::new() + .append(true) + .create(true) + .open(logfile_path) + .context("failed to open event log file")?; + + logfile + .write_all(logline.as_bytes()) + .context("failed to write to event log file")?; + logfile + .write_all(b"\n") + .context("failed to write to event log file")?; + Ok(()) + } + + pub fn sha384_digest(&self) -> [u8; 48] { + self.digest::() + } + + pub fn sha256_digest(&self) -> [u8; 32] { + self.digest::() + } + + /// Compute the digest of the event. + pub fn digest(&self) -> H::Output { + H::hash([ + &DSTACK_RUNTIME_EVENT_TYPE.to_ne_bytes()[..], + b":", + self.event.as_bytes(), + b":", + &self.payload, + ]) + } + + pub fn cc_event_type(&self) -> u32 { + DSTACK_RUNTIME_EVENT_TYPE + } +} + +/// Replay event logs +pub fn replay_events(eventlog: &[RuntimeEvent], to_event: Option<&str>) -> H::Output { + let mut mr = H::zeros(); + for event in eventlog.iter() { + mr = H::hash((mr, event.digest::())); + if let Some(to_event) = to_event { + if event.event == to_event { + break; + } + } + } + mr +} diff --git a/cc-eventlog/src/tcg.rs b/cc-eventlog/src/tcg.rs index 602ecd214..ff0aadc32 100644 --- a/cc-eventlog/src/tcg.rs +++ b/cc-eventlog/src/tcg.rs @@ -4,7 +4,12 @@ // // SPDX-License-Identifier: Apache-2.0 -use crate::codecs::VecOf; +use crate::{codecs::VecOf, tdx::TdxEvent}; +use anyhow::{Context, Result}; +use scale::Decode; + +/// The path to boottime ccel file. +const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL"; pub const TPM_ALG_ERROR: u16 = 0x0; pub const TPM_ALG_RSA: u16 = 0x1; @@ -205,3 +210,166 @@ pub struct TcgEfiSpecIdEventAlgorithmSize { pub algo_id: u16, pub digest_size: u16, } + +/// This is the common struct for tcg event logs to be delivered in different formats. +/// Currently TCG supports several event log formats defined in TCG_PCClient Spec, +/// Canonical Eventlog Spec, etc. +/// This struct provides the functionality to convey event logs in different format +/// according to request. +#[derive(Clone, scale::Decode)] +pub struct TcgEvent { + /// IMR index, starts from 1 + pub imr_index: u32, + /// Event type + pub event_type: u32, + /// List of digests + pub digests: VecOf, + /// Raw event data + pub event: VecOf, +} + +impl core::fmt::Debug for TcgEvent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TcgEventLog") + .field("imr_index", &self.imr_index) + .field("event_type", &self.event_type) + .field( + "digests", + &self + .digests + .iter() + .map(|d| hex::encode(&d.hash)) + .collect::>(), + ) + .field("event", &hex::encode(&self.event)) + .finish() + } +} + +const fn alg_id_to_digest_size(alg_id: u16) -> Option { + match alg_id { + TPM_ALG_SHA1 => Some(20), + TPM_ALG_SHA256 => Some(32), + TPM_ALG_SHA384 => Some(48), + TPM_ALG_SHA512 => Some(64), + _ => None, + } +} + +#[derive(Clone, Debug)] +pub struct TcgEventLog { + pub spec_id_header_event: TcgEfiSpecIdEvent, + pub event_logs: Vec, +} + +impl scale::Decode for TcgDigest { + fn decode(input: &mut I) -> Result { + let algo_id = u16::decode(input)?; + let digest_size = + alg_id_to_digest_size(algo_id).ok_or(scale::Error::from("Unsupported algorithm ID"))?; + let mut digest_data = vec![0; digest_size as usize]; + input + .read(&mut digest_data) + .map_err(|_| scale::Error::from("failed to read digest_data"))?; + Ok(TcgDigest { + algo_id, + hash: digest_data, + }) + } +} + +impl TcgEventLog { + pub fn decode(input: &mut &[u8]) -> Result { + let (_spec_id_header, spec_id_header_event) = + parse_spec_id_event_log(input).context("Failed to parse spec id event")?; + let mut event_logs = vec![]; + loop { + // A tmp head_buffer is used to peek the imr and event type + let head_buffer = &mut &input[..]; + let imr = u32::decode(head_buffer).context("failed to decode imr")?; + if imr == 0xFFFFFFFF { + break; + } + let event_log = TcgEvent::decode(input).context("Failed to parse event log")?; + event_logs.push(event_log); + } + Ok(TcgEventLog { + spec_id_header_event, + event_logs, + }) + } + + pub fn decode_from_ccel_file() -> Result { + let data = fs_err::read(CCEL_FILE).context("Failed to read CCEL")?; + Self::decode(&mut data.as_slice()) + } + + pub fn to_cc_event_log(&self) -> Result> { + self.event_logs + .iter() + .filter(|log| log.imr_index > 0) // GCP fills some IMRs starting from 0 + .cloned() + .map(TdxEvent::try_from) + .collect() + } +} + +fn parse_spec_id_event_log( + input: &mut I, +) -> Result<(TcgEvent, TcgEfiSpecIdEvent)> { + #[derive(Decode)] + struct Header { + imr_index: u32, + header_event_type: u32, + digest_hash: [u8; 20], + header_event: VecOf, + } + + let decoded_header = Header::decode(input).context("failed to decode log_item")?; + // Parse EFI Spec Id Event structure + let input = &mut decoded_header.header_event.as_slice(); + let spec_id_event = + TcgEfiSpecIdEvent::decode(input).context("failed to decode TcgEfiSpecIdEvent")?; + + let digests = vec![TcgDigest { + algo_id: TPM_ALG_ERROR, + hash: decoded_header.digest_hash.to_vec(), + }]; + let spec_id_header = TcgEvent { + imr_index: decoded_header.imr_index, + event_type: decoded_header.header_event_type, + digests: (digests.len() as u32, digests).into(), + event: decoded_header.header_event, + }; + Ok((spec_id_header, spec_id_event)) +} + +impl TryFrom for TdxEvent { + type Error = anyhow::Error; + + fn try_from(value: TcgEvent) -> Result { + if value.digests.len() != 1 { + return Err(anyhow::anyhow!( + "expected 1 digest, got {}", + value.digests.len() + )); + } + let digest = value + .digests + .into_inner() + .into_iter() + .next() + .context("digest not found")? + .hash; + Ok(TdxEvent { + imr: value + .imr_index + .checked_sub(1) + .context("invalid IMR index: must be >= 1")?, + event_type: value.event_type, + digest, + event: Default::default(), + event_payload: value.event.into(), + }) + } +} diff --git a/cc-eventlog/src/tdx.rs b/cc-eventlog/src/tdx.rs new file mode 100644 index 000000000..da046afa6 --- /dev/null +++ b/cc-eventlog/src/tdx.rs @@ -0,0 +1,101 @@ +use anyhow::Result; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; + +use crate::{ + runtime_events::{RuntimeEvent, DSTACK_RUNTIME_EVENT_TYPE}, + tcg::TcgEventLog, +}; + +/// This is the TDX event log format that is used to store the event log in the TDX guest. +/// It is a simplified version of the TCG event log format, containing only a single digest +/// and the raw event data. The IMR index is zero-based, unlike the TCG event log format +/// which is one-based. +/// +/// As for RTMR3, the digest extended is calculated as `sha384(event_type.to_ne_bytes() || b":" || event || b":" || event_payload)`. +#[derive(Clone, Debug, Serialize, Deserialize, Encode, Decode)] +pub struct TdxEvent { + /// IMR index, starts from 0 + pub imr: u32, + /// Event type + pub event_type: u32, + /// Digest + #[serde(with = "serde_human_bytes", default)] + pub digest: Vec, + /// Event name + pub event: String, + /// Event payload + #[serde(with = "serde_human_bytes")] + pub event_payload: Vec, +} + +impl TdxEvent { + pub fn new(imr: u32, event_type: u32, event: String, event_payload: Vec) -> Self { + Self { + imr, + event_type, + digest: vec![], + event, + event_payload, + } + } + + /// Create a version of this event with payload stripped (for size reduction). + /// Only call this on events where can_strip_payload() returns true. + pub fn stripped(&self) -> Self { + if self.is_runtime_event() { + Self { + imr: self.imr, + event_type: self.event_type, + digest: Vec::new(), + event: self.event.clone(), + event_payload: self.event_payload.clone(), + } + } else { + Self { + imr: self.imr, + event_type: self.event_type, + digest: self.digest.clone(), + event: self.event.clone(), + event_payload: Vec::new(), + } + } + } + + pub fn digest(&self) -> Vec { + if let Some(runtime_event) = self.to_runtime_event() { + return runtime_event.sha384_digest().to_vec(); + } + self.digest.clone() + } + + pub fn is_runtime_event(&self) -> bool { + self.event_type == DSTACK_RUNTIME_EVENT_TYPE + } + + pub fn to_runtime_event(&self) -> Option { + self.is_runtime_event().then_some(RuntimeEvent { + event: self.event.clone(), + payload: self.event_payload.clone(), + }) + } +} + +impl From for TdxEvent { + fn from(value: RuntimeEvent) -> Self { + TdxEvent { + imr: 3, + event_type: DSTACK_RUNTIME_EVENT_TYPE, + digest: value.sha384_digest().to_vec(), + event: value.event, + event_payload: value.payload, + } + } +} + +/// Read both boottime and runtime event logs. +pub fn read_event_log() -> Result> { + let mut event_logs = TcgEventLog::decode_from_ccel_file()?.to_cc_event_log()?; + event_logs.extend(RuntimeEvent::read_all()?.into_iter().map(Into::into)); + Ok(event_logs) +} diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml new file mode 100644 index 000000000..7ccfca47d --- /dev/null +++ b/dstack-attest/Cargo.toml @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "dstack-attest" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +cc-eventlog.workspace = true +dcap-qvl.workspace = true +dstack-types.workspace = true +ez-hash.workspace = true +fs-err.workspace = true +hex.workspace = true +hex_fmt.workspace = true +or-panic.workspace = true +scale = { workspace = true, features = ["derive"] } +serde.workspace = true +serde-human-bytes.workspace = true +serde_json.workspace = true +sha2.workspace = true +sha3.workspace = true +tdx-attest.workspace = true +tpm-attest.workspace = true +tpm-qvl.workspace = true +tpm-types.workspace = true + +[features] +quote = [] \ No newline at end of file diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs new file mode 100644 index 000000000..dc45a820e --- /dev/null +++ b/dstack-attest/src/attestation.rs @@ -0,0 +1,774 @@ +// SPDX-FileCopyrightText: © 2024-2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Attestation functions + +use std::{borrow::Cow, str::FromStr}; + +use anyhow::{anyhow, bail, Context, Result}; +use cc_eventlog::{RuntimeEvent, TdxEvent}; +use dcap_qvl::{ + quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15}, + verify::VerifiedReport as TdxVerifiedReport, +}; +use dstack_types::Platform; +use ez_hash::{Hasher, Sha256, Sha384}; +use or_panic::ResultOrPanic; +use scale::{Decode, Encode}; +use serde::{Deserialize, Serialize}; +use serde_human_bytes as hex_bytes; +use sha2::Digest as _; +use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; + +// Re-export TpmQuote from tpm-types +pub use tpm_types::TpmQuote; + +/// Attestation mode +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +pub enum AttestationMode { + /// Intel TDX with DCAP quote only + #[serde(rename = "dstack-tdx")] + #[default] + DstackTdx, + /// GCP TDX with DCAP quote only + #[serde(rename = "gcp-tdx")] + GcpTdx, + /// Dstack attestation SDK in AWS Nitro Enclave + #[serde(rename = "dstack-nitro")] + DstackNitro, +} + +impl FromStr for AttestationMode { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "dstack-tdx" => Ok(Self::DstackTdx), + "gcp-tdx" => Ok(Self::GcpTdx), + "dstack-nitro" => Ok(Self::DstackNitro), + _ => bail!("Invalid attestation mode: {s}"), + } + } +} + +impl AttestationMode { + /// Get string representation + pub fn as_str(&self) -> &str { + match self { + Self::DstackTdx => "dstack-tdx", + Self::GcpTdx => "gcp-tdx", + Self::DstackNitro => "dstack-nitro", + } + } + + /// Detect attestation mode from system + pub fn detect() -> Result { + let has_tdx = std::path::Path::new("/dev/tdx_guest").exists(); + + // First, try to detect platform from DMI product name + let platform = Platform::detect_or_dstack(); + match platform { + Platform::Dstack => { + if has_tdx { + return Ok(Self::DstackTdx); + } + bail!("Unsupported platform: Dstack(-tdx)"); + } + Platform::Gcp => { + // GCP platform: TDX + TPM dual mode + if has_tdx { + return Ok(Self::GcpTdx); + } + bail!("Unsupported platform: GCP(-tdx)"); + } + } + } + + /// Check if TDX quote should be included + pub fn has_tdx(&self) -> bool { + match self { + Self::DstackTdx => true, + Self::GcpTdx => true, + Self::DstackNitro => false, + } + } + + /// Check if TPM quote should be included + pub fn has_tpm(&self) -> bool { + match self { + Self::DstackTdx => false, + Self::GcpTdx => true, + Self::DstackNitro => true, + } + } + + /// Get TPM runtime event PCR index + pub fn tpm_runtime_pcr(&self) -> Option { + match self { + Self::GcpTdx => Some(14), + Self::DstackTdx => None, + Self::DstackNitro => None, + } + } +} + +/// The content type of a quote. A CVM should only generate quotes for these types. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QuoteContentType<'a> { + /// The public key of KMS root CA + KmsRootCa, + /// The public key of the RA-TLS certificate + RaTlsCert, + /// App defined data + AppData, + /// The custom content type + Custom(&'a str), +} + +/// The default hash algorithm used to hash the report data. +pub const DEFAULT_HASH_ALGORITHM: &str = "sha512"; + +impl QuoteContentType<'_> { + /// The tag of the content type used in the report data. + pub fn tag(&self) -> &str { + match self { + Self::KmsRootCa => "kms-root-ca", + Self::RaTlsCert => "ratls-cert", + Self::AppData => "app-data", + Self::Custom(tag) => tag, + } + } + + /// Convert the content to the report data. + pub fn to_report_data(&self, content: &[u8]) -> [u8; 64] { + self.to_report_data_with_hash(content, "") + .or_panic("sha512 hash should not fail") + } + + /// Convert the content to the report data with a specific hash algorithm. + pub fn to_report_data_with_hash(&self, content: &[u8], hash: &str) -> Result<[u8; 64]> { + macro_rules! do_hash { + ($hash: ty) => {{ + // The format is: + // hash(:) + let mut hasher = <$hash>::new(); + hasher.update(self.tag().as_bytes()); + hasher.update(b":"); + hasher.update(content); + let output = hasher.finalize(); + + let mut padded = [0u8; 64]; + padded[..output.len()].copy_from_slice(&output); + padded + }}; + } + let hash = if hash.is_empty() { + DEFAULT_HASH_ALGORITHM + } else { + hash + }; + let output = match hash { + "sha256" => do_hash!(sha2::Sha256), + "sha384" => do_hash!(sha2::Sha384), + "sha512" => do_hash!(sha2::Sha512), + "sha3-256" => do_hash!(sha3::Sha3_256), + "sha3-384" => do_hash!(sha3::Sha3_384), + "sha3-512" => do_hash!(sha3::Sha3_512), + "keccak256" => do_hash!(sha3::Keccak256), + "keccak384" => do_hash!(sha3::Keccak384), + "keccak512" => do_hash!(sha3::Keccak512), + "raw" => content.try_into().ok().context("invalid content length")?, + _ => bail!("invalid hash algorithm"), + }; + Ok(output) + } +} + +/// Represents a verified attestation +#[derive(Clone)] +pub struct DstackVerifiedReport { + /// The verified TDX report + pub tdx_report: Option, + /// The verified TPM report + pub tpm_report: Option, +} + +fn get_report_data(report: &TdxVerifiedReport) -> [u8; 64] { + match report.report { + Report::SgxEnclave(enclave_report) => enclave_report.report_data, + Report::TD10(tdreport10) => tdreport10.report_data, + Report::TD15(tdreport15) => tdreport15.base.report_data, + } +} + +impl DstackVerifiedReport { + /// Check if the report is empty + pub fn is_empty(&self) -> bool { + self.tdx_report.is_none() && self.tpm_report.is_none() + } + + /// Ensure report data matches + pub fn ensure_report_data(&self, report_data: Option<[u8; 64]>) -> Result<[u8; 64]> { + let expected = match (&self.tdx_report, report_data) { + (Some(tdx_report), Some(rd)) => { + let td_report_data = get_report_data(tdx_report); + if td_report_data != rd { + bail!("tdx report_data mismatch"); + } + td_report_data + } + (Some(tdx_report), None) => get_report_data(tdx_report), + (None, Some(rd)) => rd, + (None, None) => bail!("no verified report"), + }; + + if let Some(tpm_report) = &self.tpm_report { + let expected_tpm_report_data = sha256(&[&expected]).to_vec(); + if tpm_report.attest.qualified_data != expected_tpm_report_data { + bail!( + "tpm qualified_data mismatch, expected: {}, actual: {}, report_data: {}", + hex_fmt::HexFmt(&expected_tpm_report_data), + hex_fmt::HexFmt(&tpm_report.attest.qualified_data), + hex_fmt::HexFmt(&expected) + ); + } + } + Ok(expected) + } +} + +/// Represents a verified attestation +pub type VerifiedAttestation = Attestation; + +/// Represents a TDX quote +#[derive(Clone, Encode, Decode)] +pub struct TdxQuote { + /// The quote gererated by Intel QE + pub quote: Vec, + /// The event log + pub event_log: Vec, +} + +/// Represents a versioned attestation +#[derive(Clone, Encode, Decode)] +pub enum VersionedAttestation { + /// Version 0 + V0 { + /// The attestation report + attestation: Attestation, + }, +} + +impl VersionedAttestation { + /// Decode VerifiedAttestation from scale encoded bytes + pub fn from_scale(scale: &[u8]) -> Result { + Self::decode(&mut &scale[..]).context("Failed to decode VersionedAttestation") + } + + /// Encode to scale encoded bytes + pub fn to_scale(&self) -> Vec { + self.encode() + } + + /// Turn into latest version of attestation + pub fn into_inner(self) -> Attestation { + match self { + Self::V0 { attestation } => attestation, + } + } + + /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). + pub fn into_stripped(mut self) -> Self { + let VersionedAttestation::V0 { attestation } = &mut self; + if let Some(tdx_quote) = &mut attestation.tdx_quote { + tdx_quote.event_log = tdx_quote + .event_log + .iter() + .filter(|e| e.imr == 3) + .map(|e| e.stripped()) + .collect(); + } + self + } +} + +/// Attestation data +#[derive(Clone, Encode, Decode)] +pub struct Attestation { + /// Attestation mode + pub mode: AttestationMode, + + /// TDX quote (only for TDX mode) + pub tdx_quote: Option, + + /// TPM quote (only for TPM mode) + pub tpm_quote: Option, + + /// Runtime events (only for TDX mode) + pub runtime_events: Vec, + + /// The configuration of the VM + pub config: String, + + /// Verified report + pub report: R, +} + +impl Attestation { + /// Get TDX quote bytes + pub fn get_tdx_quote_bytes(&self) -> Option> { + self.tdx_quote.as_ref().map(|q| q.quote.clone()) + } + + /// Get TDX event log bytes + pub fn get_tdx_event_log_bytes(&self) -> Option> { + self.tdx_quote + .as_ref() + .map(|q| serde_json::to_vec(&q.event_log).unwrap_or_default()) + } + + /// Get TDX event log string + pub fn get_tdx_event_log_string(&self) -> Option { + self.tdx_quote + .as_ref() + .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) + } +} + +impl Attestation { + /// Decode the quote + pub fn decode_tdx_quote(&self) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + Quote::parse(&tdx_quote.quote) + } + + fn find_event(&self, imr: u32, name: &str) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + for event in &tdx_quote.event_log { + if event.imr == 3 && event.event == "system-ready" { + break; + } + if event.imr == imr && event.event == name { + return Ok(event.clone()); + } + } + Err(anyhow!("event {name} not found")) + } + + /// Replay event logs + pub fn replay_runtime_events(&self, to_event: Option<&str>) -> H::Output { + cc_eventlog::replay_events::(&self.runtime_events, to_event) + } + + fn find_event_payload(&self, event: &str) -> Result> { + self.find_event(3, event).map(|event| event.event_payload) + } + + /// Decode the app-id from the event log + pub fn decode_app_id(&self) -> Result { + self.find_event(3, "app-id") + .map(|event| hex::encode(&event.event_payload)) + } + + /// Decode the instance-id from the event log + pub fn decode_instance_id(&self) -> Result { + self.find_event(3, "instance-id") + .map(|event| hex::encode(&event.event_payload)) + } + + /// Decode the upgraded app-id from the event log + pub fn decode_compose_hash(&self) -> Result { + let event = self.find_event(3, "compose-hash").or_else(|_| { + // Old images use this event name + self.find_event(3, "upgraded-app-id") + })?; + Ok(hex::encode(&event.event_payload)) + } + + /// Decode the app info from the event log + pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + let quote = self.decode_tdx_quote()?; + let device_id = sha256(&["e.header.user_data]).to_vec(); + let td_report = quote.report.as_td10().context("TDX report not found")?; + let key_provider_info = if boottime_mr { + vec![] + } else { + self.find_event_payload("key-provider").unwrap_or_default() + }; + let rtmr3 = self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); + let mr_key_provider = if key_provider_info.is_empty() { + [0u8; 32] + } else { + sha256(&[&key_provider_info]) + }; + let mr_system = sha256(&[ + &td_report.mr_td, + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + &mr_key_provider, + ]); + let mr_aggregated = { + use sha2::{Digest as _, Sha256}; + let mut hasher = Sha256::new(); + for d in [ + &td_report.mr_td, + &td_report.rt_mr0, + &td_report.rt_mr1, + &td_report.rt_mr2, + &rtmr3, + ] { + hasher.update(d); + } + // For backward compatibility. Don't include mr_config_id, mr_owner, mr_owner_config if they are all 0. + if td_report.mr_config_id != [0u8; 48] + || td_report.mr_owner != [0u8; 48] + || td_report.mr_owner_config != [0u8; 48] + { + hasher.update(td_report.mr_config_id); + hasher.update(td_report.mr_owner); + hasher.update(td_report.mr_owner_config); + } + hasher.finalize().into() + }; + Ok(AppInfo { + app_id: self.find_event_payload("app-id").unwrap_or_default(), + compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), + instance_id: self.find_event_payload("instance-id").unwrap_or_default(), + device_id, + mrtd: td_report.mr_td, + rtmr0: td_report.rt_mr0, + rtmr1: td_report.rt_mr1, + rtmr2: td_report.rt_mr2, + rtmr3, + os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), + mr_system, + mr_aggregated, + key_provider_info, + }) + } + + /// Decode the rootfs hash from the event log + pub fn decode_rootfs_hash(&self) -> Result { + self.find_event(3, "rootfs-hash") + .map(|event| hex::encode(event.digest())) + } + + /// Decode the report data in the quote + pub fn decode_tdx_report_data(&self) -> Result<[u8; 64]> { + match self.decode_tdx_quote()?.report { + Report::SgxEnclave(report) => Ok(report.report_data), + Report::TD10(report) => Ok(report.report_data), + Report::TD15(report) => Ok(report.base.report_data), + } + } +} + +#[cfg(feature = "quote")] +impl Attestation { + /// Create an attestation for local machine (auto-detect mode) + pub fn local() -> Result { + Self::quote(&[0u8; 64]) + } + + /// Reconstruct from tdx quote and event log, for backward compatibility + pub fn from_tdx_quote(quote: Vec, event_log: &[u8]) -> Result { + let tdx_eventlog: Vec = + serde_json::from_slice(event_log).context("Failed to parse tdx_event_log")?; + let runtime_events = tdx_eventlog + .iter() + .flat_map(|event| event.to_runtime_event()) + .collect(); + Ok(Attestation { + mode: AttestationMode::DstackTdx, + tpm_quote: None, + tdx_quote: Some(TdxQuote { + quote, + event_log: tdx_eventlog, + }), + runtime_events, + config: "".into(), + report: (), + }) + } + + /// Create an attestation from a report data + pub fn quote(report_data: &[u8; 64]) -> Result { + let mode = AttestationMode::detect()?; + let runtime_events = RuntimeEvent::read_all().context("Failed to read runtime events")?; + let tdx_quote = if mode.has_tdx() { + let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; + let event_log = + cc_eventlog::tdx::read_event_log().context("Failed to read event log")?; + Some(TdxQuote { quote, event_log }) + } else { + None + }; + let tpm_quote = if mode.has_tpm() { + let qualifying_data = ez_hash::sha256(report_data); + let tpm_ctx = tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; + let quote = tpm_ctx + .create_quote(&qualifying_data, &tpm_attest::dstack_pcr_policy()) + .context("Failed to create TPM quote")?; + Some(quote) + } else { + None + }; + // TODO: Find a better way handling this hardcode path + let config = + fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default(); + Ok(Self { + mode, + tdx_quote, + tpm_quote, + runtime_events, + config, + report: (), + }) + } +} + +impl Attestation { + /// Wrap into a versioned attestation for encoding + pub fn into_versioned(self) -> VersionedAttestation { + VersionedAttestation::V0 { attestation: self } + } + + /// Verify the quote + pub async fn verify_with_ra_pubkey( + self, + ra_pubkey_der: &[u8], + pccs_url: Option<&str>, + ) -> Result { + let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); + self.verify(Some(expected_report_data), pccs_url).await + } + + /// Verify the quote + pub async fn verify( + self, + report_data: Option<[u8; 64]>, + pccs_url: Option<&str>, + ) -> Result { + let tpm_report = if self.mode.has_tpm() { + let report = self + .verify_tpm() + .await + .context("Failed to verify TPM quote")?; + let pcr_ind = self + .mode + .tpm_runtime_pcr() + .context("Failed to get runtime PCR no")?; + let replayed_rt_pcr = self.replay_runtime_events::(None); + let quoted_rt_pcr = report + .get_pcr(pcr_ind) + .context("No runtime PCR in TPM report")?; + if replayed_rt_pcr != quoted_rt_pcr[..] { + bail!( + "PCR{pcr_ind} mismatch, quoted: {}, replayed: {}", + hex::encode(quoted_rt_pcr), + hex::encode(replayed_rt_pcr), + ); + } + Some(report) + } else { + None + }; + let tdx_report = if self.mode.has_tdx() { + let report = self.verify_tdx(pccs_url).await?; + let td_report = report.report.as_td10().context("no td report")?; + let replayed_rtmr = self.replay_runtime_events::(None); + if replayed_rtmr != td_report.rt_mr3 { + bail!( + "RTMR3 mismatch, quoted: {}, replayed: {}", + hex::encode(td_report.rt_mr3), + hex::encode(replayed_rtmr) + ); + } + Some(report) + } else { + None + }; + let report = DstackVerifiedReport { + tdx_report, + tpm_report, + }; + if report.is_empty() { + bail!("nothing verified"); + } + report.ensure_report_data(report_data)?; + Ok(VerifiedAttestation { + mode: self.mode, + tdx_quote: self.tdx_quote, + tpm_quote: self.tpm_quote, + runtime_events: self.runtime_events, + config: self.config, + report, + }) + } + + async fn verify_tpm(&self) -> Result { + let tpm_quote = self.tpm_quote.as_ref().context("TPM quote missing")?; + tpm_qvl::get_collateral_and_verify(tpm_quote).await + } + + async fn verify_tdx(&self, pccs_url: Option<&str>) -> Result { + let quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; + let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); + if pccs_url.is_empty() { + // try to read from PCCS_URL env var + pccs_url = match std::env::var("PCCS_URL") { + Ok(url) => Cow::Owned(url), + Err(_) => Cow::Borrowed(""), + }; + } + let tdx_report = + dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) + .await + .context("Failed to get collateral")?; + validate_tcb(&tdx_report)?; + Ok(tdx_report) + } +} + +impl Attestation {} + +/// Validate the TCB attributes +pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> { + fn validate_td10(report: &TDReport10) -> Result<()> { + let is_debug = report.td_attributes[0] & 0x01 != 0; + if is_debug { + bail!("Debug mode is not allowed"); + } + if report.mr_signer_seam != [0u8; 48] { + bail!("Invalid mr signer seam"); + } + Ok(()) + } + fn validate_td15(report: &TDReport15) -> Result<()> { + if report.mr_service_td != [0u8; 48] { + bail!("Invalid mr service td"); + } + validate_td10(&report.base) + } + fn validate_sgx(report: &EnclaveReport) -> Result<()> { + let is_debug = report.attributes[0] & 0x02 != 0; + if is_debug { + bail!("Debug mode is not allowed"); + } + Ok(()) + } + match &report.report { + Report::TD15(report) => validate_td15(report), + Report::TD10(report) => validate_td10(report), + Report::SgxEnclave(report) => validate_sgx(report), + } +} + +/// Information about the app extracted from event log +#[derive(Debug, Clone, Serialize)] +pub struct AppInfo { + /// App ID + #[serde(with = "hex_bytes")] + pub app_id: Vec, + /// SHA256 of the app compose file + #[serde(with = "hex_bytes")] + pub compose_hash: Vec, + /// ID of the CVM instance + #[serde(with = "hex_bytes")] + pub instance_id: Vec, + /// ID of the device + #[serde(with = "hex_bytes")] + pub device_id: Vec, + /// TCB info + #[serde(with = "hex_bytes")] + pub mrtd: [u8; 48], + /// Runtime MR0 + #[serde(with = "hex_bytes")] + pub rtmr0: [u8; 48], + /// Runtime MR1 + #[serde(with = "hex_bytes")] + pub rtmr1: [u8; 48], + /// Runtime MR2 + #[serde(with = "hex_bytes")] + pub rtmr2: [u8; 48], + /// Runtime MR3 + #[serde(with = "hex_bytes")] + pub rtmr3: [u8; 48], + /// Measurement of everything except the app info + #[serde(with = "hex_bytes")] + pub mr_system: [u8; 32], + /// Measurement of the entire vm execution environment + #[serde(with = "hex_bytes")] + pub mr_aggregated: [u8; 32], + /// Measurement of the app image + #[serde(with = "hex_bytes")] + pub os_image_hash: Vec, + /// Key provider info + #[serde(with = "hex_bytes")] + pub key_provider_info: Vec, +} + +fn sha256(data: &[&[u8]]) -> [u8; 32] { + use sha2::{Digest as _, Sha256}; + let mut hasher = Sha256::new(); + for d in data { + hasher.update(d); + } + hasher.finalize().into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_report_data_with_hash() { + let content_type = QuoteContentType::AppData; + let content = b"test content"; + + let report_data = content_type.to_report_data(content); + assert_eq!(hex::encode(report_data), "7ea0b744ed5e9c0c83ff9f575668e1697652cd349f2027cdf26f918d4c53e8cd50b5ea9b449b4c3d50e20ae00ec29688d5a214e8daff8a10041f5d624dae8a01"); + + // Test SHA-256 + let result = content_type + .to_report_data_with_hash(content, "sha256") + .unwrap(); + assert_eq!(result[32..], [0u8; 32]); // Check padding + assert_ne!(result[..32], [0u8; 32]); // Check hash is non-zero + + // Test SHA-384 + let result = content_type + .to_report_data_with_hash(content, "sha384") + .unwrap(); + assert_eq!(result[48..], [0u8; 16]); // Check padding + assert_ne!(result[..48], [0u8; 48]); // Check hash is non-zero + + // Test default + let result = content_type.to_report_data_with_hash(content, "").unwrap(); + assert_ne!(result, [0u8; 64]); // Should fill entire buffer + + // Test raw content + let exact_content = [42u8; 64]; + let result = content_type + .to_report_data_with_hash(&exact_content, "raw") + .unwrap(); + assert_eq!(result, exact_content); + + // Test invalid raw content length + let invalid_content = [42u8; 65]; + assert!(content_type + .to_report_data_with_hash(&invalid_content, "raw") + .is_err()); + + // Test invalid hash algorithm + assert!(content_type + .to_report_data_with_hash(content, "invalid") + .is_err()); + } +} diff --git a/dstack-attest/src/lib.rs b/dstack-attest/src/lib.rs new file mode 100644 index 000000000..ac171c686 --- /dev/null +++ b/dstack-attest/src/lib.rs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use cc_eventlog::RuntimeEvent; + +use crate::attestation::AttestationMode; + +pub mod attestation; + +/// Emit a runtime event that extends RTMR3 and logs the event. +pub fn emit_runtime_event(event: &str, payload: &[u8]) -> anyhow::Result<()> { + let event = RuntimeEvent::new(event.to_string(), payload.to_vec()); + + let mode = AttestationMode::detect()?; + + event.emit().context("Failed to emit runtime event")?; + + if mode.has_tdx() { + let digest = event.sha384_digest(); + let event_type = event.cc_event_type(); + tdx_attest::extend_rtmr(3, event_type, digest).context("Failed to extend TDX RTMR")?; + } + if let Some(pcr) = mode.tpm_runtime_pcr() { + let digest = event.sha256_digest(); + let tpm = tpm_attest::TpmContext::detect().context("Failed to detect TPM device")?; + tpm.pcr_extend_sha256(pcr, &digest) + .context("Failed to extend TPM RTMR")?; + } + Ok(()) +} diff --git a/dstack-util/Cargo.toml b/dstack-util/Cargo.toml index f08c366c1..71bf295fe 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -45,6 +45,7 @@ k256 = { workspace = true, features = ["ecdsa"] } dstack-types.workspace = true rand.workspace = true sha3.workspace = true +dstack-attest.workspace = true cert-client.workspace = true x509-parser.workspace = true yaml-rust2.workspace = true @@ -55,6 +56,7 @@ luks2.workspace = true scopeguard.workspace = true tempfile.workspace = true ez-hash.workspace = true +cc-eventlog.workspace = true [dev-dependencies] rand.workspace = true diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index df15d0097..40840ecd4 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +use dstack_attest::emit_runtime_event; use dstack_types::{KeyProvider, KeyProviderKind}; use fs_err as fs; use getrandom::fill as getrandom; @@ -408,7 +409,7 @@ fn cmd_attest_info(args: AttestInfoArgs) -> Result<()> { VersionedAttestation::V0 { attestation } => { println!("version: V0"); println!("mode: {:?}", attestation.mode); - println!("config_bytes: {}", attestation.config.as_bytes().len()); + println!("config_bytes: {}", attestation.config.len()); match attestation.tdx_quote { Some(tdx) => { let event_log_json = serde_json::to_vec(&tdx.event_log) @@ -507,7 +508,7 @@ fn cmd_quote() -> Result<()> { } fn cmd_eventlog() -> Result<()> { - let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; + let event_logs = cc_eventlog::tdx::read_event_log().context("Failed to read event logs")?; serde_json::to_writer_pretty(io::stdout(), &event_logs) .context("Failed to write event logs")?; Ok(()) @@ -515,7 +516,7 @@ fn cmd_eventlog() -> Result<()> { fn cmd_extend(extend_args: ExtendArgs) -> Result<()> { let payload = hex::decode(&extend_args.payload).context("Failed to decode payload")?; - att::extend_rtmr3(&extend_args.event, &payload).context("Failed to extend RTMR") + emit_runtime_event(&extend_args.event, &payload).context("Failed to extend RTMR") } fn cmd_rand(rand_args: RandArgs) -> Result<()> { @@ -547,7 +548,7 @@ fn cmd_replay_imr() -> Result<()> { println!("=== Event Log Replay: Calculated IMR/RTMR Values ===\n"); // Read and replay event logs - let event_logs = att::eventlog::read_event_logs().context("Failed to read event logs")?; + let event_logs = att::eventlog::tdx::read_event_log().context("Failed to read event logs")?; println!("Total events: {}", event_logs.len()); diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index fdf96aadf..748444a78 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -13,6 +13,7 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; +use dstack_attest::emit_runtime_event; use dstack_kms_rpc as rpc; use dstack_types::{ shared_filenames::{ @@ -31,7 +32,6 @@ use ra_tls::cert::generate_ra_cert; use rand::Rng as _; use scopeguard::defer; use serde::{Deserialize, Serialize}; -use tdx_attest::extend_rtmr3; use tracing::{info, warn}; use crate::{ @@ -507,7 +507,7 @@ fn truncate(s: &[u8], len: usize) -> &[u8] { fn emit_key_provider_info(provider_info: &KeyProviderInfo) -> Result<()> { info!("Key provider info: {provider_info:?}"); let provider_info_json = serde_json::to_vec(&provider_info)?; - extend_rtmr3("key-provider", &provider_info_json)?; + emit_runtime_event("key-provider", &provider_info_json)?; Ok(()) } @@ -639,7 +639,7 @@ impl<'a> Stage0<'a> { let kms_info = att .decode_app_info(false) .context("Failed to decode app_info")?; - extend_rtmr3("mr-kms", &kms_info.mr_aggregated) + emit_runtime_event("mr-kms", &kms_info.mr_aggregated) .context("Failed to extend mr-kms to RTMR3")?; } Ok(()) @@ -656,7 +656,7 @@ impl<'a> Stage0<'a> { .await .context("Failed to get app key")?; - extend_rtmr3("os-image-hash", &response.os_image_hash) + emit_runtime_event("os-image-hash", &response.os_image_hash) .context("Failed to extend os-image-hash to RTMR3")?; let (_, ca_pem) = x509_parser::pem::parse_x509_pem(tmp_ca.ca_cert.as_bytes()) @@ -734,22 +734,12 @@ impl<'a> Stage0<'a> { Ok(app_keys) } - fn generate_tpm_app_keys(&self, app_compose_hash: &[u8; 32]) -> Result { + fn generate_tpm_app_keys(&self) -> Result { let tpm = TpmContext::detect().context("failed to detect TPM context")?; // Get PCR policy for sealing (boot chain + app PCR) let pcr_policy = tpm::dstack_pcr_policy(); - // Extend app PCR with app-compose hash BEFORE unsealing - // This ensures the sealed data is bound to this specific app configuration - tpm.pcr_extend_sha256(tpm::APP_PCR, app_compose_hash)?; - - // Dump all PCR values (0-15) AFTER extending for debugging - let all_pcrs = - tpm::PcrSelection::sha256(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); - info!("PCR values AFTER extending PCR {}:", tpm::APP_PCR); - tpm.dump_pcr_values(&all_pcrs); - // Try to read sealed seed (bound to PCR values including app PCR) if let Some(seed) = tpm .unseal::<32>(tpm::SEALED_NV_INDEX, tpm::PRIMARY_KEY_HANDLE, &pcr_policy) @@ -779,7 +769,7 @@ impl<'a> Stage0<'a> { .context("failed to generate TPM app keys") } - async fn request_app_keys(&self, app_compose_hash: &[u8; 32]) -> Result { + async fn request_app_keys(&self) -> Result { let key_provider = self.shared.app_compose.key_provider(); match key_provider { KeyProviderKind::Kms => self.request_app_keys_from_kms().await, @@ -792,7 +782,7 @@ impl<'a> Stage0<'a> { } KeyProviderKind::Tpm => { info!("Generating app keys from TPM"); - self.generate_tpm_app_keys(app_compose_hash) + self.generate_tpm_app_keys() } } } @@ -1152,11 +1142,11 @@ impl<'a> Stage0<'a> { truncated_compose_hash.to_vec() }; - extend_rtmr3("system-preparing", &[])?; - extend_rtmr3("app-id", &app_id)?; - extend_rtmr3("compose-hash", &compose_hash)?; - extend_rtmr3("instance-id", &instance_id)?; - extend_rtmr3("boot-mr-done", &[])?; + emit_runtime_event("system-preparing", &[])?; + emit_runtime_event("app-id", &app_id)?; + emit_runtime_event("compose-hash", &compose_hash)?; + emit_runtime_event("instance-id", &instance_id)?; + emit_runtime_event("boot-mr-done", &[])?; Ok(AppInfo { instance_info, compose_hash, @@ -1203,7 +1193,7 @@ impl<'a> Stage0<'a> { self.vmm .notify_q("boot.progress", "requesting app keys") .await; - let app_keys = self.request_app_keys(&app_info.compose_hash).await?; + let app_keys = self.request_app_keys().await?; if app_keys.disk_crypt_key.is_empty() { bail!("Failed to get valid key phrase from KMS"); } @@ -1217,7 +1207,7 @@ impl<'a> Stage0<'a> { // Parse kernel command line options let opts = parse_dstack_options(&self.shared).context("Failed to parse kernel cmdline")?; - extend_rtmr3("storage-fs", opts.storage_fs.to_string().as_bytes())?; + emit_runtime_event("storage-fs", opts.storage_fs.to_string().as_bytes())?; info!( "Filesystem options: encryption={}, filesystem={:?}", opts.storage_encrypted, opts.storage_fs @@ -1233,7 +1223,7 @@ impl<'a> Stage0<'a> { &serde_json::to_string(&app_info.instance_info)?, ) .await; - extend_rtmr3("system-ready", &[])?; + emit_runtime_event("system-ready", &[])?; self.vmm.notify_q("boot.progress", "data disk ready").await; if !self.shared.app_compose.key_provider().is_kms() { diff --git a/guest-agent/Cargo.toml b/guest-agent/Cargo.toml index 7d97c92c5..fe8813bab 100644 --- a/guest-agent/Cargo.toml +++ b/guest-agent/Cargo.toml @@ -47,8 +47,10 @@ dstack-types.workspace = true sha3.workspace = true strip-ansi-escapes.workspace = true cert-client.workspace = true +dstack-attest.workspace = true ring.workspace = true ed25519-dalek.workspace = true tempfile.workspace = true rand.workspace = true or-panic.workspace = true +cc-eventlog.workspace = true diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index fc15c4395..bcbec4316 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -6,7 +6,9 @@ use std::sync::{Arc, RwLock}; use anyhow::{Context, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; +use cc_eventlog::tdx::read_event_log; use cert_client::CertRequestClient; +use dstack_attest::emit_runtime_event; use dstack_guest_agent_rpc::{ dstack_guest_server::{DstackGuestRpc, DstackGuestServer}, tappd_server::{TappdRpc, TappdServer}, @@ -34,7 +36,6 @@ use rcgen::KeyPair; use ring::rand::{SecureRandom, SystemRandom}; use serde_json::json; use sha3::{Digest, Keccak256}; -use tdx_attest::eventlog::read_event_logs; use tracing::error; use crate::config::Config; @@ -315,7 +316,7 @@ impl DstackGuestRpc for InternalRpcHandler { if self.state.config().simulator.enabled { return Ok(()); } - tdx_attest::extend_rtmr3(&request.event, &request.payload) + emit_runtime_event(&request.event, &request.payload) } async fn info(self) -> Result { @@ -547,7 +548,7 @@ impl TappdRpc for InternalRpcHandlerV0 { prefix, }); } - let event_log = read_event_logs().context("Failed to decode event log")?; + let event_log = read_event_log().context("Failed to decode event log")?; let event_log = serde_json::to_string(&event_log).context("Failed to serialize event log")?; let quote = tdx_attest::get_quote(&report_data).context("Failed to get quote")?; @@ -645,7 +646,7 @@ impl WorkerRpc for ExternalRpcHandler { let ed25519_quote = tdx_attest::get_quote(&ed25519_report_data) .context("Failed to get ed25519 quote")?; let event_log = serde_json::to_string( - &read_event_logs().context("Failed to read event log")?, + &read_event_log().context("Failed to read event log")?, )?; Ok(GetQuoteResponse { quote: ed25519_quote, @@ -676,7 +677,7 @@ impl WorkerRpc for ExternalRpcHandler { let secp256k1_quote = tdx_attest::get_quote(&secp256k1_report_data) .context("Failed to get secp256k1 quote")?; let event_log = serde_json::to_string( - &read_event_logs().context("Failed to read event log")?, + &read_event_log().context("Failed to read event log")?, )?; Ok(GetQuoteResponse { diff --git a/ra-rpc/src/client.rs b/ra-rpc/src/client.rs index 7e4563176..1f68ad831 100644 --- a/ra-rpc/src/client.rs +++ b/ra-rpc/src/client.rs @@ -9,10 +9,7 @@ use prpc::{ client::{Error, RequestClient}, Message, }; -use ra_tls::{ - attestation::{Attestation, VerifiedAttestation}, - traits::CertExt, -}; +use ra_tls::{attestation::VerifiedAttestation, traits::CertExt}; use reqwest::{tls::TlsInfo, Certificate, Client, Identity, Response}; use serde::{de::DeserializeOwned, Serialize}; @@ -135,7 +132,7 @@ impl RaClient { let attestation = if !self.verify_server_attestation { None } else { - match Attestation::from_cert(&cert).context("Failed to parse attestation")? { + match ra_tls::attestation::from_cert(&cert).context("Failed to parse attestation")? { None => None, Some(attestation) => { let verified_attestation = attestation diff --git a/ra-rpc/src/rocket_helper.rs b/ra-rpc/src/rocket_helper.rs index dcdbfa158..88e32060e 100644 --- a/ra-rpc/src/rocket_helper.rs +++ b/ra-rpc/src/rocket_helper.rs @@ -12,7 +12,7 @@ use rocket::response::content::{RawHtml, RawJson}; use std::{borrow::Cow, sync::Arc}; use anyhow::{Context, Result}; -use ra_tls::{attestation::Attestation, traits::CertExt}; +use ra_tls::traits::CertExt; use rocket::{ data::{ByteUnit, Data, Limits, ToByteUnit}, http::{uri::Origin, ContentType, Method, Status}, @@ -301,7 +301,7 @@ pub async fn handle_prpc_impl>( let attestation = request .certificate .as_ref() - .map(|cert| Attestation::from_der(cert.as_bytes())) + .map(|cert| ra_tls::attestation::from_der(cert.as_bytes())) .transpose()? .flatten(); let attestation = match (request.quote_verifier, attestation) { diff --git a/ra-tls/Cargo.toml b/ra-tls/Cargo.toml index b953340fa..aed0ee5d3 100644 --- a/ra-tls/Cargo.toml +++ b/ra-tls/Cargo.toml @@ -36,13 +36,12 @@ serde-human-bytes.workspace = true flate2.workspace = true or-panic.workspace = true rand.workspace = true -tpm-attest = { workspace = true, optional = true } tpm-types.workspace = true dstack-types.workspace = true tpm-qvl.workspace = true hex_fmt.workspace = true ez-hash.workspace = true +dstack-attest.workspace = true [features] -default = ["quote"] -quote = ["dep:tpm-attest"] +quote = ["dstack-attest/quote"] diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index 683096d76..cd4107bf6 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -1,789 +1,58 @@ -// SPDX-FileCopyrightText: © 2024-2025 Phala Network -// -// SPDX-License-Identifier: Apache-2.0 +//! Embedding and extracting attestation from/to TLS certificate -//! Attestation functions - -use std::{borrow::Cow, str::FromStr}; - -use anyhow::{anyhow, bail, Context, Result}; -use dcap_qvl::{ - quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15}, - verify::VerifiedReport as TdxVerifiedReport, -}; -use dstack_types::Platform; -use scale::{Decode, Encode}; -use serde::{Deserialize, Serialize}; -use sha2::Digest as _; -use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; -use x509_parser::parse_x509_certificate; +use cc_eventlog::TdxEvent; +pub use dstack_attest::attestation::*; use crate::{oids, traits::CertExt}; -use cc_eventlog::TdxEventLogEntry as EventLog; -use or_panic::ResultOrPanic; -use serde_human_bytes as hex_bytes; - -// Re-export TpmQuote from tpm-types -pub use tpm_types::TpmQuote; - -/// Attestation mode -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] -pub enum AttestationMode { - /// Intel TDX with DCAP quote only - #[serde(rename = "dstack-tdx")] - #[default] - DstackTdx, - /// GCP TDX with DCAP quote only - #[serde(rename = "gcp-tdx")] - GcpTdx, - /// Dstack attestation SDK in AWS Nitro Enclave - #[serde(rename = "dstack-nitro")] - DstackNitro, -} - -impl FromStr for AttestationMode { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "dstack-tdx" => Ok(Self::DstackTdx), - "gcp-tdx" => Ok(Self::GcpTdx), - "dstack-nitro" => Ok(Self::DstackNitro), - _ => bail!("Invalid attestation mode: {s}"), - } - } -} - -impl AttestationMode { - /// Get string representation - pub fn as_str(&self) -> &str { - match self { - Self::DstackTdx => "dstack-tdx", - Self::GcpTdx => "gcp-tdx", - Self::DstackNitro => "dstack-nitro", - } - } - - /// Detect attestation mode from system - pub fn detect() -> Result { - let has_tdx = std::path::Path::new("/dev/tdx_guest").exists(); - - // First, try to detect platform from DMI product name - let platform = Platform::detect_or_dstack(); - match platform { - Platform::Dstack => { - if has_tdx { - return Ok(Self::DstackTdx); - } - bail!("Unsupported platform: Dstack(-tdx)"); - } - Platform::Gcp => { - // GCP platform: TDX + TPM dual mode - if has_tdx { - return Ok(Self::GcpTdx); - } - bail!("Unsupported platform: GCP(-tdx)"); - } - } - } - - /// Check if TDX quote should be included - pub fn has_tdx(&self) -> bool { - match self { - Self::DstackTdx => true, - Self::GcpTdx => true, - Self::DstackNitro => false, - } - } - - /// Check if TPM quote should be included - pub fn has_tpm(&self) -> bool { - match self { - Self::DstackTdx => false, - Self::GcpTdx => true, - Self::DstackNitro => true, - } - } -} - -/// The content type of a quote. A CVM should only generate quotes for these types. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum QuoteContentType<'a> { - /// The public key of KMS root CA - KmsRootCa, - /// The public key of the RA-TLS certificate - RaTlsCert, - /// App defined data - AppData, - /// The custom content type - Custom(&'a str), -} - -/// The default hash algorithm used to hash the report data. -pub const DEFAULT_HASH_ALGORITHM: &str = "sha512"; - -impl QuoteContentType<'_> { - /// The tag of the content type used in the report data. - pub fn tag(&self) -> &str { - match self { - Self::KmsRootCa => "kms-root-ca", - Self::RaTlsCert => "ratls-cert", - Self::AppData => "app-data", - Self::Custom(tag) => tag, - } - } - - /// Convert the content to the report data. - pub fn to_report_data(&self, content: &[u8]) -> [u8; 64] { - self.to_report_data_with_hash(content, "") - .or_panic("sha512 hash should not fail") - } - - /// Convert the content to the report data with a specific hash algorithm. - pub fn to_report_data_with_hash(&self, content: &[u8], hash: &str) -> Result<[u8; 64]> { - macro_rules! do_hash { - ($hash: ty) => {{ - // The format is: - // hash(:) - let mut hasher = <$hash>::new(); - hasher.update(self.tag().as_bytes()); - hasher.update(b":"); - hasher.update(content); - let output = hasher.finalize(); - - let mut padded = [0u8; 64]; - padded[..output.len()].copy_from_slice(&output); - padded - }}; - } - let hash = if hash.is_empty() { - DEFAULT_HASH_ALGORITHM - } else { - hash - }; - let output = match hash { - "sha256" => do_hash!(sha2::Sha256), - "sha384" => do_hash!(sha2::Sha384), - "sha512" => do_hash!(sha2::Sha512), - "sha3-256" => do_hash!(sha3::Sha3_256), - "sha3-384" => do_hash!(sha3::Sha3_384), - "sha3-512" => do_hash!(sha3::Sha3_512), - "keccak256" => do_hash!(sha3::Keccak256), - "keccak384" => do_hash!(sha3::Keccak384), - "keccak512" => do_hash!(sha3::Keccak512), - "raw" => content.try_into().ok().context("invalid content length")?, - _ => bail!("invalid hash algorithm"), - }; - Ok(output) - } -} - -/// Represents a verified attestation -#[derive(Clone)] -pub struct DstackVerifiedReport { - /// The verified TDX report - pub tdx_report: Option, - /// The verified TPM report - pub tpm_report: Option, -} - -fn get_report_data(report: &TdxVerifiedReport) -> [u8; 64] { - match report.report { - Report::SgxEnclave(enclave_report) => enclave_report.report_data, - Report::TD10(tdreport10) => tdreport10.report_data, - Report::TD15(tdreport15) => tdreport15.base.report_data, - } -} - -impl DstackVerifiedReport { - /// Check if the report is empty - pub fn is_empty(&self) -> bool { - self.tdx_report.is_none() && self.tpm_report.is_none() - } - - /// Ensure report data matches - pub fn ensure_report_data(&self, report_data: Option<[u8; 64]>) -> Result<[u8; 64]> { - let expected = match (&self.tdx_report, report_data) { - (Some(tdx_report), Some(rd)) => { - let td_report_data = get_report_data(tdx_report); - if td_report_data != rd { - bail!("tdx report_data mismatch"); - } - td_report_data - } - (Some(tdx_report), None) => get_report_data(tdx_report), - (None, Some(rd)) => rd, - (None, None) => bail!("no verified report"), - }; - - if let Some(tpm_report) = &self.tpm_report { - let expected_tpm_report_data = sha256(&[&expected]).to_vec(); - if tpm_report.attest.qualified_data != expected_tpm_report_data { - bail!( - "tpm qualified_data mismatch, expected: {}, actual: {}, report_data: {}", - hex_fmt::HexFmt(&expected_tpm_report_data), - hex_fmt::HexFmt(&tpm_report.attest.qualified_data), - hex_fmt::HexFmt(&expected) - ); - } - } - Ok(expected) - } -} - -/// Represents a verified attestation -pub type VerifiedAttestation = Attestation; - -/// Represents a TDX quote -#[derive(Clone, Encode, Decode)] -pub struct TdxQuote { - /// The quote gererated by Intel QE - pub quote: Vec, - /// The event log - pub event_log: Vec, -} - -/// Represents a versioned attestation -#[derive(Clone, Encode, Decode)] -pub enum VersionedAttestation { - /// Version 0 - V0 { - /// The attestation report - attestation: Attestation, - }, -} - -impl VersionedAttestation { - /// Decode VerifiedAttestation from scale encoded bytes - pub fn from_scale(scale: &[u8]) -> Result { - Self::decode(&mut &scale[..]).context("Failed to decode VersionedAttestation") - } - - /// Encode to scale encoded bytes - pub fn to_scale(&self) -> Vec { - self.encode() - } - - /// Turn into latest version of attestation - pub fn into_inner(self) -> Attestation { - match self { - Self::V0 { attestation } => attestation, - } - } - - /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). - pub fn into_stripped(mut self) -> Self { - let VersionedAttestation::V0 { attestation } = &mut self; - if let Some(tdx_quote) = &mut attestation.tdx_quote { - let rtmr3_only: Vec<_> = tdx_quote - .event_log - .iter() - .filter(|e| e.imr == 3) - .cloned() - .collect(); - tdx_quote.event_log = cc_eventlog::strip_event_log_payloads(&rtmr3_only); - } - self - } -} - -/// Attestation data -#[derive(Clone, Encode, Decode)] -pub struct Attestation { - /// Attestation mode - pub mode: AttestationMode, - - /// TDX quote (only for TDX mode) - pub tdx_quote: Option, - - /// TPM quote (only for TPM mode) - pub tpm_quote: Option, - - /// The configuration of the VM - pub config: String, - - /// Verified report - pub report: R, -} - -impl Attestation { - /// Get TDX quote bytes - pub fn get_tdx_quote_bytes(&self) -> Option> { - self.tdx_quote.as_ref().map(|q| q.quote.clone()) - } - - /// Get TDX event log bytes - pub fn get_tdx_event_log_bytes(&self) -> Option> { - self.tdx_quote - .as_ref() - .map(|q| serde_json::to_vec(&q.event_log).unwrap_or_default()) - } - - /// Get TDX event log string - pub fn get_tdx_event_log_string(&self) -> Option { - self.tdx_quote - .as_ref() - .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) - } -} - -impl Attestation { - /// Decode the quote - pub fn decode_tdx_quote(&self) -> Result { - let Some(tdx_quote) = &self.tdx_quote else { - bail!("tdx_quote not found"); - }; - Quote::parse(&tdx_quote.quote) - } - - fn find_event(&self, imr: u32, name: &str) -> Result { - let Some(tdx_quote) = &self.tdx_quote else { - bail!("tdx_quote not found"); - }; - for event in &tdx_quote.event_log { - if event.imr == 3 && event.event == "system-ready" { - break; - } - if event.imr == imr && event.event == name { - return Ok(event.clone()); - } - } - Err(anyhow!("event {name} not found")) - } - - /// Replay event logs - pub fn replay_rtmr3(&self, to_event: Option<&str>) -> Result<[u8; 48]> { - let Some(tdx_quote) = &self.tdx_quote else { - bail!("tdx_quote not found"); - }; - cc_eventlog::replay_event_logs(&tdx_quote.event_log, to_event, 3) - } - - fn find_event_payload(&self, event: &str) -> Result> { - self.find_event(3, event).map(|event| event.event_payload) - } - - /// Decode the app-id from the event log - pub fn decode_app_id(&self) -> Result { - self.find_event(3, "app-id") - .map(|event| hex::encode(&event.event_payload)) - } - - /// Decode the instance-id from the event log - pub fn decode_instance_id(&self) -> Result { - self.find_event(3, "instance-id") - .map(|event| hex::encode(&event.event_payload)) - } - - /// Decode the upgraded app-id from the event log - pub fn decode_compose_hash(&self) -> Result { - let event = self.find_event(3, "compose-hash").or_else(|_| { - // Old images use this event name - self.find_event(3, "upgraded-app-id") - })?; - Ok(hex::encode(&event.event_payload)) - } - - /// Decode the app info from the event log - pub fn decode_app_info(&self, boottime_mr: bool) -> Result { - let rtmr3 = self - .replay_rtmr3(boottime_mr.then_some("boot-mr-done")) - .context("Failed to replay event logs")?; - let quote = self.decode_tdx_quote()?; - let device_id = sha256(&["e.header.user_data]).to_vec(); - let td_report = quote.report.as_td10().context("TDX report not found")?; - let key_provider_info = if boottime_mr { - vec![] - } else { - self.find_event_payload("key-provider").unwrap_or_default() - }; - let mr_key_provider = if key_provider_info.is_empty() { - [0u8; 32] - } else { - sha256(&[&key_provider_info]) - }; - let mr_system = sha256(&[ - &td_report.mr_td, - &td_report.rt_mr0, - &td_report.rt_mr1, - &td_report.rt_mr2, - &mr_key_provider, - ]); - let mr_aggregated = { - use sha2::{Digest as _, Sha256}; - let mut hasher = Sha256::new(); - for d in [ - &td_report.mr_td, - &td_report.rt_mr0, - &td_report.rt_mr1, - &td_report.rt_mr2, - &rtmr3, - ] { - hasher.update(d); - } - // For backward compatibility. Don't include mr_config_id, mr_owner, mr_owner_config if they are all 0. - if td_report.mr_config_id != [0u8; 48] - || td_report.mr_owner != [0u8; 48] - || td_report.mr_owner_config != [0u8; 48] - { - hasher.update(td_report.mr_config_id); - hasher.update(td_report.mr_owner); - hasher.update(td_report.mr_owner_config); - } - hasher.finalize().into() - }; - Ok(AppInfo { - app_id: self.find_event_payload("app-id").unwrap_or_default(), - compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), - instance_id: self.find_event_payload("instance-id").unwrap_or_default(), - device_id, - mrtd: td_report.mr_td, - rtmr0: td_report.rt_mr0, - rtmr1: td_report.rt_mr1, - rtmr2: td_report.rt_mr2, - rtmr3, - os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), - mr_system, - mr_aggregated, - key_provider_info, - }) - } - - /// Decode the rootfs hash from the event log - pub fn decode_rootfs_hash(&self) -> Result { - self.find_event(3, "rootfs-hash") - .map(|event| hex::encode(event.digest())) - } - - /// Decode the report data in the quote - pub fn decode_tdx_report_data(&self) -> Result<[u8; 64]> { - match self.decode_tdx_quote()?.report { - Report::SgxEnclave(report) => Ok(report.report_data), - Report::TD10(report) => Ok(report.report_data), - Report::TD15(report) => Ok(report.base.report_data), - } - } -} - -#[cfg(feature = "quote")] -impl Attestation { - /// Create an attestation for local machine (auto-detect mode) - pub fn local() -> Result { - Self::quote(&[0u8; 64]) - } - - /// Create an attestation from a report data - pub fn quote(report_data: &[u8; 64]) -> Result { - let mode = AttestationMode::detect()?; - let tdx_quote = if mode.has_tdx() { - let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; - let event_log = - tdx_attest::eventlog::read_event_logs().context("Failed to read event logs")?; - Some(TdxQuote { quote, event_log }) - } else { - None - }; - let tpm_quote = if mode.has_tpm() { - let qualifying_data = ez_hash::sha256(report_data); - let tpm_ctx = tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; - let quote = tpm_ctx - .create_quote(&qualifying_data, &tpm_attest::dstack_pcr_policy()) - .context("Failed to create TPM quote")?; - Some(quote) - } else { - None - }; - // TODO: Find a better way handling this hardcode path - let config = - fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default(); - Ok(Self { - mode, - tdx_quote, - tpm_quote, - config, - report: (), - }) - } - - /// Wrap into a versioned attestation for encoding - pub fn into_versioned(self) -> VersionedAttestation { - VersionedAttestation::V0 { attestation: self } - } -} - -impl Attestation { - /// Extract attestation data from a certificate - pub fn from_cert(cert: &impl CertExt) -> Result> { - Self::from_ext_getter(|oid| cert.get_extension_bytes(oid)) - } - - /// From an extension getter - pub fn from_ext_getter( - get_ext: impl Fn(&[u64]) -> Result>>, - ) -> Result> { - // Try to detect attestation mode from certificate extension - if let Some(attestation_bytes) = get_ext(oids::PHALA_RATLS_ATTESTATION)? { - let VersionedAttestation::V0 { attestation } = - VersionedAttestation::from_scale(&attestation_bytes) - .context("Failed to decode attestation from cert extension")?; - return Ok(Some(attestation)); - } - // Backward compatibility: if PHALA_RATLS_ATTESTATION - let Some(tdx_quote) = get_ext(oids::PHALA_RATLS_TDX_QUOTE)? else { - return Ok(None); - }; - let raw_event_log = - get_ext(oids::PHALA_RATLS_EVENT_LOG)?.context("TDX event log missing")?; - let tdx_event_log = if !raw_event_log.is_empty() { - // Decompress if needed (handles both compressed and uncompressed formats) - serde_json::from_slice(&raw_event_log).context("invalid event log")? - } else { - vec![] - }; - - Ok(Some(Self { - mode: AttestationMode::DstackTdx, - tdx_quote: Some(TdxQuote { - quote: tdx_quote, - event_log: tdx_event_log, - }), - tpm_quote: None, - config: "".into(), - report: (), - })) - } - - /// Extract attestation from x509 certificate - pub fn from_der(cert: &[u8]) -> Result> { - let (_, cert) = parse_x509_certificate(cert).context("Failed to parse certificate")?; - Self::from_cert(&cert) - } - - /// Extract attestation from x509 certificate in PEM format - pub fn from_pem(cert: &[u8]) -> Result> { - let (_, pem) = - x509_parser::pem::parse_x509_pem(cert).context("Failed to parse certificate")?; - let cert = pem.parse_x509().context("Failed to parse certificate")?; - Self::from_cert(&cert) - } - - /// Verify the quote - pub async fn verify_with_ra_pubkey( - self, - ra_pubkey_der: &[u8], - pccs_url: Option<&str>, - ) -> Result { - let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); - self.verify(Some(expected_report_data), pccs_url).await - } - - /// Verify the quote - pub async fn verify( - self, - report_data: Option<[u8; 64]>, - pccs_url: Option<&str>, - ) -> Result { - let tpm_report = if self.mode.has_tpm() { - let report = self - .verify_tpm() - .await - .context("Failed to verify TPM quote")?; - Some(report) - } else { - None - }; - let tdx_report = if self.mode.has_tdx() { - let report = self.verify_tdx(pccs_url).await?; - Some(report) - } else { - None - }; - let report = DstackVerifiedReport { - tdx_report, - tpm_report, - }; - if report.is_empty() { - bail!("nothing verified"); - } - report.ensure_report_data(report_data)?; - Ok(VerifiedAttestation { - mode: self.mode, - tdx_quote: self.tdx_quote, - tpm_quote: self.tpm_quote, - config: self.config, - report, - }) - } - - async fn verify_tpm(&self) -> Result { - let tpm_quote = self.tpm_quote.as_ref().context("TPM quote missing")?; - tpm_qvl::get_collateral_and_verify(tpm_quote).await - } - - async fn verify_tdx(&self, pccs_url: Option<&str>) -> Result { - let quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; - let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); - if pccs_url.is_empty() { - // try to read from PCCS_URL env var - pccs_url = match std::env::var("PCCS_URL") { - Ok(url) => Cow::Owned(url), - Err(_) => Cow::Borrowed(""), - }; - } - let tdx_report = - dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref())) - .await - .context("Failed to get collateral")?; - let report = tdx_report.report.as_td10().context("TD10 report missing")?; - // Replay the event logs - let rtmr3 = self - .replay_rtmr3(None) - .context("Failed to replay event logs")?; - if rtmr3 != report.rt_mr3 { - bail!( - "RTMR3 mismatch, quoted: {}, replayed: {}", - hex::encode(report.rt_mr3), - hex::encode(rtmr3), - ); - } - validate_tcb(&tdx_report)?; - Ok(tdx_report) - } -} - -impl Attestation {} - -/// Validate the TCB attributes -pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> { - fn validate_td10(report: &TDReport10) -> Result<()> { - let is_debug = report.td_attributes[0] & 0x01 != 0; - if is_debug { - bail!("Debug mode is not allowed"); - } - if report.mr_signer_seam != [0u8; 48] { - bail!("Invalid mr signer seam"); - } - Ok(()) - } - fn validate_td15(report: &TDReport15) -> Result<()> { - if report.mr_service_td != [0u8; 48] { - bail!("Invalid mr service td"); - } - validate_td10(&report.base) - } - fn validate_sgx(report: &EnclaveReport) -> Result<()> { - let is_debug = report.attributes[0] & 0x02 != 0; - if is_debug { - bail!("Debug mode is not allowed"); - } - Ok(()) - } - match &report.report { - Report::TD15(report) => validate_td15(report), - Report::TD10(report) => validate_td10(report), - Report::SgxEnclave(report) => validate_sgx(report), - } -} - -/// Information about the app extracted from event log -#[derive(Debug, Clone, Serialize)] -pub struct AppInfo { - /// App ID - #[serde(with = "hex_bytes")] - pub app_id: Vec, - /// SHA256 of the app compose file - #[serde(with = "hex_bytes")] - pub compose_hash: Vec, - /// ID of the CVM instance - #[serde(with = "hex_bytes")] - pub instance_id: Vec, - /// ID of the device - #[serde(with = "hex_bytes")] - pub device_id: Vec, - /// TCB info - #[serde(with = "hex_bytes")] - pub mrtd: [u8; 48], - /// Runtime MR0 - #[serde(with = "hex_bytes")] - pub rtmr0: [u8; 48], - /// Runtime MR1 - #[serde(with = "hex_bytes")] - pub rtmr1: [u8; 48], - /// Runtime MR2 - #[serde(with = "hex_bytes")] - pub rtmr2: [u8; 48], - /// Runtime MR3 - #[serde(with = "hex_bytes")] - pub rtmr3: [u8; 48], - /// Measurement of everything except the app info - #[serde(with = "hex_bytes")] - pub mr_system: [u8; 32], - /// Measurement of the entire vm execution environment - #[serde(with = "hex_bytes")] - pub mr_aggregated: [u8; 32], - /// Measurement of the app image - #[serde(with = "hex_bytes")] - pub os_image_hash: Vec, - /// Key provider info - #[serde(with = "hex_bytes")] - pub key_provider_info: Vec, -} - -fn sha256(data: &[&[u8]]) -> [u8; 32] { - use sha2::{Digest as _, Sha256}; - let mut hasher = Sha256::new(); - for d in data { - hasher.update(d); - } - hasher.finalize().into() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_to_report_data_with_hash() { - let content_type = QuoteContentType::AppData; - let content = b"test content"; - - let report_data = content_type.to_report_data(content); - assert_eq!(hex::encode(report_data), "7ea0b744ed5e9c0c83ff9f575668e1697652cd349f2027cdf26f918d4c53e8cd50b5ea9b449b4c3d50e20ae00ec29688d5a214e8daff8a10041f5d624dae8a01"); - - // Test SHA-256 - let result = content_type - .to_report_data_with_hash(content, "sha256") - .unwrap(); - assert_eq!(result[32..], [0u8; 32]); // Check padding - assert_ne!(result[..32], [0u8; 32]); // Check hash is non-zero - - // Test SHA-384 - let result = content_type - .to_report_data_with_hash(content, "sha384") - .unwrap(); - assert_eq!(result[48..], [0u8; 16]); // Check padding - assert_ne!(result[..48], [0u8; 48]); // Check hash is non-zero - - // Test default - let result = content_type.to_report_data_with_hash(content, "").unwrap(); - assert_ne!(result, [0u8; 64]); // Should fill entire buffer - - // Test raw content - let exact_content = [42u8; 64]; - let result = content_type - .to_report_data_with_hash(&exact_content, "raw") - .unwrap(); - assert_eq!(result, exact_content); - - // Test invalid raw content length - let invalid_content = [42u8; 65]; - assert!(content_type - .to_report_data_with_hash(&invalid_content, "raw") - .is_err()); - - // Test invalid hash algorithm - assert!(content_type - .to_report_data_with_hash(content, "invalid") - .is_err()); - } +use anyhow::{Context, Result}; + +/// Extract attestation from x509 certificate +pub fn from_der(cert: &[u8]) -> Result> { + let (_, cert) = + x509_parser::parse_x509_certificate(cert).context("Failed to parse certificate")?; + from_cert(&cert) +} + +/// Extract attestation from a certificate +pub fn from_cert(cert: &impl CertExt) -> Result> { + from_ext_getter(|oid| cert.get_extension_bytes(oid)) +} + +/// Extract attestation from a certificate extension getter +pub fn from_ext_getter( + get_ext: impl Fn(&[u64]) -> Result>>, +) -> Result> { + // Try to detect attestation mode from certificate extension + if let Some(attestation_bytes) = get_ext(oids::PHALA_RATLS_ATTESTATION)? { + let VersionedAttestation::V0 { attestation } = + VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode attestation from cert extension")?; + return Ok(Some(attestation)); + } + // Backward compatibility: if PHALA_RATLS_ATTESTATION + let Some(tdx_quote) = get_ext(oids::PHALA_RATLS_TDX_QUOTE)? else { + return Ok(None); + }; + let raw_event_log = get_ext(oids::PHALA_RATLS_EVENT_LOG)?.context("TDX event log missing")?; + let tdx_event_log: Vec = if !raw_event_log.is_empty() { + // Decompress if needed (handles both compressed and uncompressed formats) + serde_json::from_slice(&raw_event_log).context("invalid event log")? + } else { + vec![] + }; + let runtime_events = tdx_event_log + .iter() + .flat_map(|event| event.to_runtime_event()) + .collect(); + Ok(Some(Attestation { + mode: AttestationMode::DstackTdx, + tdx_quote: Some(TdxQuote { + quote: tdx_quote, + event_log: tdx_event_log, + }), + tpm_quote: None, + runtime_events, + config: "".into(), + report: (), + })) } diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index 6545e3f8f..c2bbbbb1a 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -23,14 +23,14 @@ use x509_parser::prelude::{FromDer as _, X509Certificate}; use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; -use crate::attestation::{ - Attestation, AttestationMode, QuoteContentType, TdxQuote, VersionedAttestation, -}; use crate::oids::{ PHALA_RATLS_APP_ID, PHALA_RATLS_ATTESTATION, PHALA_RATLS_CERT_USAGE, PHALA_RATLS_EVENT_LOG, PHALA_RATLS_TDX_QUOTE, }; use crate::traits::CertExt; +use dstack_attest::attestation::{ + Attestation, AttestationMode, QuoteContentType, VersionedAttestation, +}; /// A CA certificate and private key. pub struct CaCert { @@ -252,19 +252,7 @@ impl TryFrom for CertSigningRequestV2 { confirm: v0.confirm, pubkey: v0.pubkey, config: v0.config, - attestation: VersionedAttestation::V0 { - attestation: Attestation { - mode: AttestationMode::DstackTdx, - tpm_quote: None, - tdx_quote: Some(TdxQuote { - quote: v0.quote, - event_log: serde_json::from_slice(&v0.event_log) - .context("Failed to parse tdx_event_log")?, - }), - config: "".into(), - report: (), - }, - }, + attestation: Attestation::from_tdx_quote(v0.quote, &v0.event_log)?.into_versioned(), }) } } diff --git a/ra-tls/src/lib.rs b/ra-tls/src/lib.rs index 2d24b82e7..70bb712e8 100644 --- a/ra-tls/src/lib.rs +++ b/ra-tls/src/lib.rs @@ -8,6 +8,7 @@ pub extern crate rcgen; pub mod attestation; + pub mod cert; pub mod kdf; pub mod oids; diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index 0d372704d..56808b2af 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -21,18 +21,3 @@ pub type TdxReportData = [u8; 64]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TdxReport(pub [u8; 1024]); - -pub fn extend_rtmr3(event: &str, payload: &[u8]) -> anyhow::Result<()> { - use anyhow::Context; - let event_type = eventlog::DSTACK_RUNTIME_EVENT_TYPE; - let index = 3; - let log = - eventlog::TdxEventLogEntry::new(index, event_type, event.to_string(), payload.to_vec()); - let digest = log - .digest() - .try_into() - .ok() - .context("Invalid digest size")?; - extend_rtmr(index, event_type, digest).context("Failed to extend RTMR")?; - log_rtmr_event(&log).context("Failed to log RTMR event") -} diff --git a/tdx-attest/src/linux.rs b/tdx-attest/src/linux.rs index e25670046..b8d87393f 100644 --- a/tdx-attest/src/linux.rs +++ b/tdx-attest/src/linux.rs @@ -2,10 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -use anyhow::{bail, Context}; -use cc_eventlog::TdxEventLogEntry; - -use std::io::Write; +use anyhow::bail; use std::path::PathBuf; use fs_err as fs; @@ -48,29 +45,6 @@ pub fn get_quote(report_data: &TdxReportData) -> Result> { }) } -pub fn log_rtmr_event(log: &TdxEventLogEntry) -> anyhow::Result<()> { - let logline = serde_json::to_string(&log).context("failed to serialize event log")?; - - let logfile_path = std::path::Path::new(cc_eventlog::RUNTIME_EVENT_LOG_FILE); - let logfile_dir = logfile_path - .parent() - .context("failed to get event log directory")?; - fs::create_dir_all(logfile_dir).context("failed to create event log directory")?; - - let mut logfile = fs::OpenOptions::new() - .append(true) - .create(true) - .open(logfile_path) - .context("failed to open event log file")?; - logfile - .write_all(logline.as_bytes()) - .context("failed to write to event log file")?; - logfile - .write_all(b"\n") - .context("failed to write to event log file")?; - Ok(()) -} - /// Find the TSM measurements sysfs directory fn find_tsm_measurements_dir() -> Option { let path = PathBuf::from(TSM_MEASUREMENTS_PATH); diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs index 73875062a..73359d854 100644 --- a/tpm-attest/src/lib.rs +++ b/tpm-attest/src/lib.rs @@ -28,7 +28,7 @@ pub const SEALED_NV_INDEX: u32 = 0x01801101; /// 0: The firmware version and NonHostInfo (representing the memory encryption technology) /// 2: The uki image (kernel + initrd + initramfs) /// 14: The app compose hash -pub const APP_PCR: u32 = 14; +const APP_PCR: u32 = 14; pub fn dstack_pcr_policy() -> PcrSelection { PcrSelection::sha256(&[0, 2, APP_PCR]) } diff --git a/verifier/src/main.rs b/verifier/src/main.rs index 79dc9213e..ea80a8d61 100644 --- a/verifier/src/main.rs +++ b/verifier/src/main.rs @@ -44,7 +44,7 @@ async fn verify_cvm( verifier: &State>, request: Json, ) -> Json { - match verifier.verify(&request.into_inner()).await { + match verifier.verify(request.into_inner()).await { Ok(response) => Json(response), Err(e) => { error!("Verification failed: {:?}", e); @@ -100,7 +100,7 @@ async fn run_oneshot(file_path: &str, config: &Config) -> anyhow::Result<()> { // Run verification info!("Starting verification..."); - let response = verifier.verify(&request).await?; + let response = verifier.verify(request).await?; // Persist response next to the input file for convenience let output_path = format!("{file_path}.verification.json"); diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 49b326380..47c2c5327 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -9,11 +9,11 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; -use cc_eventlog::TdxEventLogEntry as EventLog; +use cc_eventlog::TdxEvent; use dstack_mr::{RtmrLog, TdxMeasurementDetails, TdxMeasurements}; use dstack_types::VmConfig; use ra_tls::attestation::{ - Attestation, AttestationMode, TdxQuote, TpmQuote, VerifiedAttestation, VersionedAttestation, + Attestation, AttestationMode, TpmQuote, VerifiedAttestation, VersionedAttestation, }; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; @@ -31,7 +31,7 @@ fn collect_rtmr_mismatch( actual: &[u8], expected_sequence: &RtmrLog, actual_indices: &[usize], - event_log: &[EventLog], + event_log: &[TdxEvent], ) -> RtmrMismatch { let expected_hex = hex::encode(expected); let actual_hex = hex::encode(actual); @@ -380,27 +380,17 @@ impl CvmVerifier { ) } - pub async fn verify(&self, request: &VerificationRequest) -> Result { + pub async fn verify(&self, request: VerificationRequest) -> Result { let attestation = if let Some(attestation) = &request.attestation { VersionedAttestation::from_scale(attestation).context("Failed to decode attestaion")? - } else if let Some(tpm_quote) = &request.quote { + } else if let Some(tdx_quote) = request.quote { let event_log = request .event_log .as_ref() .context("Event log is required")?; - let event_log = - serde_json::from_str(event_log).context("Failed to decode event log")?; - Attestation { - mode: AttestationMode::DstackTdx, - tdx_quote: Some(TdxQuote { - quote: tpm_quote.to_vec(), - event_log, - }), - tpm_quote: None, - config: "".into(), - report: (), - } - .into_versioned() + Attestation::from_tdx_quote(tdx_quote, event_log.as_bytes()) + .context("Failed to create attestation")? + .into_versioned() } else { bail!("Quote is required"); }; From 25f68eb44a47d97385224e8bae2ec829234b26ba Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 15:25:46 +0000 Subject: [PATCH 039/264] Link tdx quote to tpm quote --- dstack-attest/src/attestation.rs | 127 +++++++++++++++++-------------- dstack-attest/src/lib.rs | 4 + kms/src/main_service.rs | 2 +- ra-tls/src/attestation.rs | 26 +------ verifier/src/verification.rs | 8 +- 5 files changed, 81 insertions(+), 86 deletions(-) diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index dc45a820e..50c9e43cf 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -13,7 +13,7 @@ use dcap_qvl::{ verify::VerifiedReport as TdxVerifiedReport, }; use dstack_types::Platform; -use ez_hash::{Hasher, Sha256, Sha384}; +use ez_hash::{sha256, Hasher, Sha256, Sha384}; use or_panic::ResultOrPanic; use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; @@ -188,6 +188,8 @@ impl QuoteContentType<'_> { /// Represents a verified attestation #[derive(Clone)] pub struct DstackVerifiedReport { + /// The attestation mode + pub mode: AttestationMode, /// The verified TDX report pub tdx_report: Option, /// The verified TPM report @@ -207,35 +209,6 @@ impl DstackVerifiedReport { pub fn is_empty(&self) -> bool { self.tdx_report.is_none() && self.tpm_report.is_none() } - - /// Ensure report data matches - pub fn ensure_report_data(&self, report_data: Option<[u8; 64]>) -> Result<[u8; 64]> { - let expected = match (&self.tdx_report, report_data) { - (Some(tdx_report), Some(rd)) => { - let td_report_data = get_report_data(tdx_report); - if td_report_data != rd { - bail!("tdx report_data mismatch"); - } - td_report_data - } - (Some(tdx_report), None) => get_report_data(tdx_report), - (None, Some(rd)) => rd, - (None, None) => bail!("no verified report"), - }; - - if let Some(tpm_report) = &self.tpm_report { - let expected_tpm_report_data = sha256(&[&expected]).to_vec(); - if tpm_report.attest.qualified_data != expected_tpm_report_data { - bail!( - "tpm qualified_data mismatch, expected: {}, actual: {}, report_data: {}", - hex_fmt::HexFmt(&expected_tpm_report_data), - hex_fmt::HexFmt(&tpm_report.attest.qualified_data), - hex_fmt::HexFmt(&expected) - ); - } - } - Ok(expected) - } } /// Represents a verified attestation @@ -308,6 +281,9 @@ pub struct Attestation { /// Runtime events (only for TDX mode) pub runtime_events: Vec, + /// The report data + pub report_data: [u8; 64], + /// The configuration of the VM pub config: String, @@ -393,7 +369,9 @@ impl Attestation { /// Decode the app info from the event log pub fn decode_app_info(&self, boottime_mr: bool) -> Result { let quote = self.decode_tdx_quote()?; - let device_id = sha256(&["e.header.user_data]).to_vec(); + let todo = "use ppid as device_id"; + let todo = "trim mrs in AppInfo and BootInfo"; + let device_id = sha256(quote.header.user_data).to_vec(); let td_report = quote.report.as_td10().context("TDX report not found")?; let key_provider_info = if boottime_mr { vec![] @@ -404,10 +382,10 @@ impl Attestation { let mr_key_provider = if key_provider_info.is_empty() { [0u8; 32] } else { - sha256(&[&key_provider_info]) + sha256(&key_provider_info) }; - let mr_system = sha256(&[ - &td_report.mr_td, + let mr_system = sha256([ + &td_report.mr_td[..], &td_report.rt_mr0, &td_report.rt_mr1, &td_report.rt_mr2, @@ -484,6 +462,11 @@ impl Attestation { .iter() .flat_map(|event| event.to_runtime_event()) .collect(); + let report_data = { + let quote = dcap_qvl::quote::Quote::parse("e).context("Invalid TDX quote")?; + let report = quote.report.as_td10().context("Invalid TDX report")?; + report.report_data + }; Ok(Attestation { mode: AttestationMode::DstackTdx, tpm_quote: None, @@ -492,6 +475,7 @@ impl Attestation { event_log: tdx_eventlog, }), runtime_events, + report_data, config: "".into(), report: (), }) @@ -501,19 +485,22 @@ impl Attestation { pub fn quote(report_data: &[u8; 64]) -> Result { let mode = AttestationMode::detect()?; let runtime_events = RuntimeEvent::read_all().context("Failed to read runtime events")?; - let tdx_quote = if mode.has_tdx() { + let tpm_qualifying_data; + let tdx_quote; + if mode.has_tdx() { let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; let event_log = cc_eventlog::tdx::read_event_log().context("Failed to read event log")?; - Some(TdxQuote { quote, event_log }) + tpm_qualifying_data = sha256("e); + tdx_quote = Some(TdxQuote { quote, event_log }); } else { - None + tpm_qualifying_data = sha256(report_data); + tdx_quote = None; }; let tpm_quote = if mode.has_tpm() { - let qualifying_data = ez_hash::sha256(report_data); let tpm_ctx = tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; let quote = tpm_ctx - .create_quote(&qualifying_data, &tpm_attest::dstack_pcr_policy()) + .create_quote(&tpm_qualifying_data, &tpm_attest::dstack_pcr_policy()) .context("Failed to create TPM quote")?; Some(quote) } else { @@ -527,6 +514,7 @@ impl Attestation { tdx_quote, tpm_quote, runtime_events, + report_data: *report_data, config, report: (), }) @@ -546,15 +534,14 @@ impl Attestation { pccs_url: Option<&str>, ) -> Result { let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); - self.verify(Some(expected_report_data), pccs_url).await + if self.report_data != expected_report_data { + bail!("report data mismatch"); + } + self.verify(pccs_url).await } /// Verify the quote - pub async fn verify( - self, - report_data: Option<[u8; 64]>, - pccs_url: Option<&str>, - ) -> Result { + pub async fn verify(self, pccs_url: Option<&str>) -> Result { let tpm_report = if self.mode.has_tpm() { let report = self .verify_tpm() @@ -594,19 +581,56 @@ impl Attestation { } else { None }; + + match self.mode { + AttestationMode::DstackTdx => { + // For bare dstack TDX machine, we only make sure the report data is correct + let Some(tdx_report) = &tdx_report else { + bail!("TDX report is missing in dstack-tdx mode"); + }; + let td_report_data = get_report_data(tdx_report); + if td_report_data != self.report_data[..] { + bail!("tdx report_data mismatch"); + } + } + AttestationMode::GcpTdx => { + let Some(tdx_report) = &tdx_report else { + bail!("TDX report is missing in gcp-tdx mode"); + }; + let Some(td10_report) = tdx_report.report.as_td10() else { + bail!("TD10 report is missing in gcp-tdx mode"); + }; + if td10_report.report_data != self.report_data[..] { + bail!("tdx report_data mismatch"); + } + let tdx_quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; + let Some(tpm_report) = &tpm_report else { + bail!("TPM report is missing in gcp-tdx mode"); + }; + // TPM quote the TDX quote + let qualifying_data = sha256(tdx_quote); + if qualifying_data != tpm_report.attest.qualified_data[..] { + bail!("tpm qualified_data mismatch"); + } + } + AttestationMode::DstackNitro => { + bail!("Nitro not supported"); + } + } let report = DstackVerifiedReport { + mode: self.mode, tdx_report, tpm_report, }; if report.is_empty() { bail!("nothing verified"); } - report.ensure_report_data(report_data)?; Ok(VerifiedAttestation { mode: self.mode, tdx_quote: self.tdx_quote, tpm_quote: self.tpm_quote, runtime_events: self.runtime_events, + report_data: self.report_data, config: self.config, report, }) @@ -636,8 +660,6 @@ impl Attestation { } } -impl Attestation {} - /// Validate the TCB attributes pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> { fn validate_td10(report: &TDReport10) -> Result<()> { @@ -714,15 +736,6 @@ pub struct AppInfo { pub key_provider_info: Vec, } -fn sha256(data: &[&[u8]]) -> [u8; 32] { - use sha2::{Digest as _, Sha256}; - let mut hasher = Sha256::new(); - for d in data { - hasher.update(d); - } - hasher.finalize().into() -} - #[cfg(test)] mod tests { use super::*; diff --git a/dstack-attest/src/lib.rs b/dstack-attest/src/lib.rs index ac171c686..b8577d007 100644 --- a/dstack-attest/src/lib.rs +++ b/dstack-attest/src/lib.rs @@ -5,6 +5,10 @@ use anyhow::Context; use cc_eventlog::RuntimeEvent; +pub use cc_eventlog as ccel; +pub use tdx_attest as tdx; +pub use tpm_attest as tpm; + use crate::attestation::AttestationMode; pub mod attestation; diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 938c354a1..66f9f4643 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -177,10 +177,10 @@ impl RpcHandler { .report .as_td10() .context("Failed to decode TD report")?; - let app_info = att.decode_app_info(use_boottime_mr)?; debug!("vm_config: {vm_config_str}"); let vm_config: VmConfig = serde_json::from_str(vm_config_str).context("Failed to decode VM config")?; + let app_info = att.decode_app_info(use_boottime_mr)?; let os_image_hash = vm_config.os_image_hash.clone(); let boot_info = BootInfo { mrtd: report.mr_td.to_vec(), diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index cd4107bf6..591862a2c 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -1,6 +1,5 @@ //! Embedding and extracting attestation from/to TLS certificate -use cc_eventlog::TdxEvent; pub use dstack_attest::attestation::*; use crate::{oids, traits::CertExt}; @@ -34,25 +33,8 @@ pub fn from_ext_getter( return Ok(None); }; let raw_event_log = get_ext(oids::PHALA_RATLS_EVENT_LOG)?.context("TDX event log missing")?; - let tdx_event_log: Vec = if !raw_event_log.is_empty() { - // Decompress if needed (handles both compressed and uncompressed formats) - serde_json::from_slice(&raw_event_log).context("invalid event log")? - } else { - vec![] - }; - let runtime_events = tdx_event_log - .iter() - .flat_map(|event| event.to_runtime_event()) - .collect(); - Ok(Some(Attestation { - mode: AttestationMode::DstackTdx, - tdx_quote: Some(TdxQuote { - quote: tdx_quote, - event_log: tdx_event_log, - }), - tpm_quote: None, - runtime_events, - config: "".into(), - report: (), - })) + Ok(Some( + Attestation::from_tdx_quote(tdx_quote, &raw_event_log) + .context("Failed to create attestation from TDX quote")?, + )) } diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 47c2c5327..2a7254dc8 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -408,7 +408,7 @@ impl CvmVerifier { let attestation = attestation.into_inner(); let debug = request.debug.unwrap_or(false); - let verified = attestation.verify(None, request.pccs_url.as_deref()).await; + let verified = attestation.verify(request.pccs_url.as_deref()).await; let verified_attestation = match verified { Ok(att) => { details.quote_verified = true; @@ -419,11 +419,7 @@ impl CvmVerifier { .as_ref() .map(|r| r.advisory_ids.clone()) .unwrap_or_default(); - let report_data = att - .report - .ensure_report_data(None) - .context("Failed to decode report data")?; - details.report_data = Some(hex::encode(report_data)); + details.report_data = Some(hex::encode(att.report_data)); att } Err(e) => { From 4ee124f3241d51243e9c2cf85d5439342a0ce793 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 15:35:17 +0000 Subject: [PATCH 040/264] Fix cargo clippy --- cert-client/src/lib.rs | 3 ++- gateway/src/main.rs | 2 +- gateway/src/main_service.rs | 2 +- gateway/src/proxy/sni.rs | 2 +- serde-duration/src/lib.rs | 6 +++--- supervisor/src/process.rs | 2 +- vmm/src/app.rs | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cert-client/src/lib.rs b/cert-client/src/lib.rs index 30a366995..a41fa3bf6 100644 --- a/cert-client/src/lib.rs +++ b/cert-client/src/lib.rs @@ -14,7 +14,7 @@ use ra_tls::{ pub enum CertRequestClient { Local { - ca: CaCert, + ca: Box, }, Kms { client: KmsClient, @@ -67,6 +67,7 @@ impl CertRequestClient { | KeyProvider::Tpm { key, .. } => { let ca = CaCert::new(keys.ca_cert.clone(), key.clone()) .context("Failed to create CA")?; + let ca = Box::new(ca); Ok(CertRequestClient::Local { ca }) } KeyProvider::Kms { diff --git a/gateway/src/main.rs b/gateway/src/main.rs index d4544ffd4..5d86e84f6 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -106,7 +106,7 @@ async fn maybe_gen_certs(config: &Config, tls_config: &TlsConfig) -> Result<()> let cert = ra_tls::cert::CertRequest::builder() .key(&key) .subject("dstack-gateway") - .alt_names(&[config.rpc_domain.clone()]) + .alt_names(std::slice::from_ref(&config.rpc_domain)) .usage_server_auth(true) .build() .self_signed() diff --git a/gateway/src/main_service.rs b/gateway/src/main_service.rs index 25befd26c..e6bc67754 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -100,7 +100,7 @@ impl Proxy { } impl ProxyInner { - pub(crate) fn lock(&self) -> MutexGuard { + pub(crate) fn lock(&self) -> MutexGuard<'_, ProxyState> { self.state.lock().or_panic("Failed to lock AppState") } diff --git a/gateway/src/proxy/sni.rs b/gateway/src/proxy/sni.rs index 5235765b3..4901cc45f 100644 --- a/gateway/src/proxy/sni.rs +++ b/gateway/src/proxy/sni.rs @@ -10,7 +10,7 @@ pub fn extract_sni(b: &[u8]) -> Option<&[u8]> { extract_sni_inner(b).ok().map(|r| r.1) } -fn extract_sni_inner(b: &[u8]) -> Result<(usize, &[u8]), PErr> { +fn extract_sni_inner(b: &[u8]) -> Result<(usize, &[u8]), PErr<'_, u8>> { const HANDSHAKE_TYPE_CLIENT_HELLO: usize = 1; const EXTENSION_TYPE_SNI: usize = 0; const NAME_TYPE_HOST_NAME: usize = 0; diff --git a/serde-duration/src/lib.rs b/serde-duration/src/lib.rs index 41a223b3d..345cc4f6b 100644 --- a/serde-duration/src/lib.rs +++ b/serde-duration/src/lib.rs @@ -12,11 +12,11 @@ where if duration == &Duration::MAX { return serializer.serialize_str("never"); } - let (value, unit) = if duration.as_secs() % (24 * 3600) == 0 { + let (value, unit) = if duration.as_secs().is_multiple_of(24 * 3600) { (duration.as_secs() / (24 * 3600), "d") - } else if duration.as_secs() % 3600 == 0 { + } else if duration.as_secs().is_multiple_of(3600) { (duration.as_secs() / 3600, "h") - } else if duration.as_secs() % 60 == 0 { + } else if duration.as_secs().is_multiple_of(60) { (duration.as_secs() / 60, "m") } else { (duration.as_secs(), "s") diff --git a/supervisor/src/process.rs b/supervisor/src/process.rs index 887aac14a..c424f9b51 100644 --- a/supervisor/src/process.rs +++ b/supervisor/src/process.rs @@ -167,7 +167,7 @@ impl Process { } } - pub(crate) fn lock(&self) -> MutexGuard { + pub(crate) fn lock(&self) -> MutexGuard<'_, ProcessStateRT> { self.state.lock().or_panic("lock should never fail") } diff --git a/vmm/src/app.rs b/vmm/src/app.rs index a46376276..1533e6e45 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -123,7 +123,7 @@ pub struct App { } impl App { - fn lock(&self) -> MutexGuard { + fn lock(&self) -> MutexGuard<'_, AppState> { self.state.lock().or_panic("mutex poisoned") } From 3c75b2ada70e40fc605a190a1c6639b4faac5b9a Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 15:46:55 +0000 Subject: [PATCH 041/264] Use ppid hash as device_id --- dstack-attest/src/attestation.rs | 122 ++++++++++++++++++------------- 1 file changed, 71 insertions(+), 51 deletions(-) diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index 50c9e43cf..8d9539c2c 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -312,66 +312,31 @@ impl Attestation { } } -impl Attestation { - /// Decode the quote - pub fn decode_tdx_quote(&self) -> Result { - let Some(tdx_quote) = &self.tdx_quote else { - bail!("tdx_quote not found"); - }; - Quote::parse(&tdx_quote.quote) - } - - fn find_event(&self, imr: u32, name: &str) -> Result { - let Some(tdx_quote) = &self.tdx_quote else { - bail!("tdx_quote not found"); - }; - for event in &tdx_quote.event_log { - if event.imr == 3 && event.event == "system-ready" { - break; - } - if event.imr == imr && event.event == name { - return Ok(event.clone()); - } - } - Err(anyhow!("event {name} not found")) - } - - /// Replay event logs - pub fn replay_runtime_events(&self, to_event: Option<&str>) -> H::Output { - cc_eventlog::replay_events::(&self.runtime_events, to_event) - } - - fn find_event_payload(&self, event: &str) -> Result> { - self.find_event(3, event).map(|event| event.event_payload) - } +pub trait GetPpid { + fn get_ppid(&self) -> Vec; +} - /// Decode the app-id from the event log - pub fn decode_app_id(&self) -> Result { - self.find_event(3, "app-id") - .map(|event| hex::encode(&event.event_payload)) - } - - /// Decode the instance-id from the event log - pub fn decode_instance_id(&self) -> Result { - self.find_event(3, "instance-id") - .map(|event| hex::encode(&event.event_payload)) +impl GetPpid for () { + fn get_ppid(&self) -> Vec { + Vec::new() } +} - /// Decode the upgraded app-id from the event log - pub fn decode_compose_hash(&self) -> Result { - let event = self.find_event(3, "compose-hash").or_else(|_| { - // Old images use this event name - self.find_event(3, "upgraded-app-id") - })?; - Ok(hex::encode(&event.event_payload)) +impl GetPpid for DstackVerifiedReport { + fn get_ppid(&self) -> Vec { + let Some(tdx_report) = &self.tdx_report else { + return Vec::new(); + }; + tdx_report.ppid.clone() } +} +impl Attestation { /// Decode the app info from the event log pub fn decode_app_info(&self, boottime_mr: bool) -> Result { let quote = self.decode_tdx_quote()?; - let todo = "use ppid as device_id"; let todo = "trim mrs in AppInfo and BootInfo"; - let device_id = sha256(quote.header.user_data).to_vec(); + let device_id = sha256(self.report.get_ppid()).to_vec(); let td_report = quote.report.as_td10().context("TDX report not found")?; let key_provider_info = if boottime_mr { vec![] @@ -430,6 +395,61 @@ impl Attestation { key_provider_info, }) } +} + +impl Attestation { + /// Decode the quote + pub fn decode_tdx_quote(&self) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + Quote::parse(&tdx_quote.quote) + } + + fn find_event(&self, imr: u32, name: &str) -> Result { + let Some(tdx_quote) = &self.tdx_quote else { + bail!("tdx_quote not found"); + }; + for event in &tdx_quote.event_log { + if event.imr == 3 && event.event == "system-ready" { + break; + } + if event.imr == imr && event.event == name { + return Ok(event.clone()); + } + } + Err(anyhow!("event {name} not found")) + } + + /// Replay event logs + pub fn replay_runtime_events(&self, to_event: Option<&str>) -> H::Output { + cc_eventlog::replay_events::(&self.runtime_events, to_event) + } + + fn find_event_payload(&self, event: &str) -> Result> { + self.find_event(3, event).map(|event| event.event_payload) + } + + /// Decode the app-id from the event log + pub fn decode_app_id(&self) -> Result { + self.find_event(3, "app-id") + .map(|event| hex::encode(&event.event_payload)) + } + + /// Decode the instance-id from the event log + pub fn decode_instance_id(&self) -> Result { + self.find_event(3, "instance-id") + .map(|event| hex::encode(&event.event_payload)) + } + + /// Decode the upgraded app-id from the event log + pub fn decode_compose_hash(&self) -> Result { + let event = self.find_event(3, "compose-hash").or_else(|_| { + // Old images use this event name + self.find_event(3, "upgraded-app-id") + })?; + Ok(hex::encode(&event.event_payload)) + } /// Decode the rootfs hash from the event log pub fn decode_rootfs_hash(&self) -> Result { From a32f2fc387cf295f918b28ac8740c0748869a7f1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sat, 20 Dec 2025 16:35:18 +0000 Subject: [PATCH 042/264] Remove tdx mrs from AppInfo and BootInfo --- dstack-attest/src/attestation.rs | 141 +++++++++++----------- guest-agent/src/rpc_service.rs | 5 - kms/src/main_service.rs | 9 -- kms/src/main_service/upgrade_authority.rs | 10 -- verifier/src/main.rs | 4 - 5 files changed, 72 insertions(+), 97 deletions(-) diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index 8d9539c2c..a965869ed 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -331,34 +331,48 @@ impl GetPpid for DstackVerifiedReport { } } +struct Mrs { + mr_system: [u8; 32], + mr_aggregated: [u8; 32], +} + impl Attestation { - /// Decode the app info from the event log - pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + fn decode_mr_tpm(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { + let os_image_hash = self.find_event_payload("os-image-hash").unwrap_or_default(); + let mr_system = sha256([&os_image_hash, mr_key_provider]); + let tpm_quote = self.tpm_quote.as_ref().context("TPM quote not found")?; + let pcr0 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 0) + .context("PCR 0 not found")?; + let pcr2 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 2) + .context("PCR 2 not found")?; + let runtime_pcr = + self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); + let mr_aggregated = sha256([&pcr0.value[..], &pcr2.value, &runtime_pcr]); + Ok(Mrs { + mr_system, + mr_aggregated, + }) + } + + fn decode_mr_tdx(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { let quote = self.decode_tdx_quote()?; - let todo = "trim mrs in AppInfo and BootInfo"; - let device_id = sha256(self.report.get_ppid()).to_vec(); - let td_report = quote.report.as_td10().context("TDX report not found")?; - let key_provider_info = if boottime_mr { - vec![] - } else { - self.find_event_payload("key-provider").unwrap_or_default() - }; let rtmr3 = self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); - let mr_key_provider = if key_provider_info.is_empty() { - [0u8; 32] - } else { - sha256(&key_provider_info) - }; + let td_report = quote.report.as_td10().context("TDX report not found")?; let mr_system = sha256([ &td_report.mr_td[..], &td_report.rt_mr0, &td_report.rt_mr1, &td_report.rt_mr2, - &mr_key_provider, + mr_key_provider, ]); let mr_aggregated = { - use sha2::{Digest as _, Sha256}; - let mut hasher = Sha256::new(); + let mut hasher = sha2::Sha256::new(); for d in [ &td_report.mr_td, &td_report.rt_mr0, @@ -379,19 +393,37 @@ impl Attestation { } hasher.finalize().into() }; + Ok(Mrs { + mr_system, + mr_aggregated, + }) + } + + /// Decode the app info from the event log + pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + let key_provider_info = if boottime_mr { + vec![] + } else { + self.find_event_payload("key-provider").unwrap_or_default() + }; + let mr_key_provider = if key_provider_info.is_empty() { + [0u8; 32] + } else { + sha256(&key_provider_info) + }; + let mrs = match self.mode { + AttestationMode::DstackTdx => self.decode_mr_tdx(boottime_mr, &mr_key_provider)?, + AttestationMode::GcpTdx => self.decode_mr_tpm(boottime_mr, &mr_key_provider)?, + AttestationMode::DstackNitro => bail!("Nitro attestation is not supported"), + }; Ok(AppInfo { app_id: self.find_event_payload("app-id").unwrap_or_default(), compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), instance_id: self.find_event_payload("instance-id").unwrap_or_default(), - device_id, - mrtd: td_report.mr_td, - rtmr0: td_report.rt_mr0, - rtmr1: td_report.rt_mr1, - rtmr2: td_report.rt_mr2, - rtmr3, os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), - mr_system, - mr_aggregated, + device_id: sha256(self.report.get_ppid()).to_vec(), + mr_system: mrs.mr_system, + mr_aggregated: mrs.mr_aggregated, key_provider_info, }) } @@ -406,15 +438,12 @@ impl Attestation { Quote::parse(&tdx_quote.quote) } - fn find_event(&self, imr: u32, name: &str) -> Result { - let Some(tdx_quote) = &self.tdx_quote else { - bail!("tdx_quote not found"); - }; - for event in &tdx_quote.event_log { - if event.imr == 3 && event.event == "system-ready" { + fn find_event(&self, name: &str) -> Result { + for event in &self.runtime_events { + if event.event == "system-ready" { break; } - if event.imr == imr && event.event == name { + if event.event == name { return Ok(event.clone()); } } @@ -427,43 +456,32 @@ impl Attestation { } fn find_event_payload(&self, event: &str) -> Result> { - self.find_event(3, event).map(|event| event.event_payload) + self.find_event(event).map(|event| event.payload) + } + + fn find_event_hex_payload(&self, event: &str) -> Result { + self.find_event(event) + .map(|event| hex::encode(&event.payload)) } /// Decode the app-id from the event log pub fn decode_app_id(&self) -> Result { - self.find_event(3, "app-id") - .map(|event| hex::encode(&event.event_payload)) + self.find_event_hex_payload("app-id") } /// Decode the instance-id from the event log pub fn decode_instance_id(&self) -> Result { - self.find_event(3, "instance-id") - .map(|event| hex::encode(&event.event_payload)) + self.find_event_hex_payload("instance-id") } /// Decode the upgraded app-id from the event log pub fn decode_compose_hash(&self) -> Result { - let event = self.find_event(3, "compose-hash").or_else(|_| { - // Old images use this event name - self.find_event(3, "upgraded-app-id") - })?; - Ok(hex::encode(&event.event_payload)) + self.find_event_hex_payload("compose-hash") } /// Decode the rootfs hash from the event log pub fn decode_rootfs_hash(&self) -> Result { - self.find_event(3, "rootfs-hash") - .map(|event| hex::encode(event.digest())) - } - - /// Decode the report data in the quote - pub fn decode_tdx_report_data(&self) -> Result<[u8; 64]> { - match self.decode_tdx_quote()?.report { - Report::SgxEnclave(report) => Ok(report.report_data), - Report::TD10(report) => Ok(report.report_data), - Report::TD15(report) => Ok(report.base.report_data), - } + self.find_event_hex_payload("rootfs-hash") } } @@ -727,21 +745,6 @@ pub struct AppInfo { /// ID of the device #[serde(with = "hex_bytes")] pub device_id: Vec, - /// TCB info - #[serde(with = "hex_bytes")] - pub mrtd: [u8; 48], - /// Runtime MR0 - #[serde(with = "hex_bytes")] - pub rtmr0: [u8; 48], - /// Runtime MR1 - #[serde(with = "hex_bytes")] - pub rtmr1: [u8; 48], - /// Runtime MR2 - #[serde(with = "hex_bytes")] - pub rtmr2: [u8; 48], - /// Runtime MR3 - #[serde(with = "hex_bytes")] - pub rtmr3: [u8; 48], /// Measurement of everything except the app info #[serde(with = "hex_bytes")] pub mr_system: [u8; 32], diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index bcbec4316..2c99e1c7b 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -174,11 +174,6 @@ pub async fn get_info(state: &AppState, external: bool) -> Result { } else { let app_compose = state.config().app_compose.raw.clone(); serde_json::to_string_pretty(&json!({ - "mrtd": hex::encode(app_info.mrtd), - "rtmr0": hex::encode(app_info.rtmr0), - "rtmr1": hex::encode(app_info.rtmr1), - "rtmr2": hex::encode(app_info.rtmr2), - "rtmr3": hex::encode(app_info.rtmr3), "mr_aggregated": hex::encode(app_info.mr_aggregated), "os_image_hash": hex::encode(&app_info.os_image_hash), "compose_hash": hex::encode(&app_info.compose_hash), diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 66f9f4643..24b9e06df 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -173,21 +173,12 @@ impl RpcHandler { let Some(tdx_report) = &att.report.tdx_report else { bail!("No TD report in attestation"); }; - let report = tdx_report - .report - .as_td10() - .context("Failed to decode TD report")?; debug!("vm_config: {vm_config_str}"); let vm_config: VmConfig = serde_json::from_str(vm_config_str).context("Failed to decode VM config")?; let app_info = att.decode_app_info(use_boottime_mr)?; let os_image_hash = vm_config.os_image_hash.clone(); let boot_info = BootInfo { - mrtd: report.mr_td.to_vec(), - rtmr0: report.rt_mr0.to_vec(), - rtmr1: report.rt_mr1.to_vec(), - rtmr2: report.rt_mr2.to_vec(), - rtmr3: report.rt_mr3.to_vec(), mr_aggregated: app_info.mr_aggregated.to_vec(), os_image_hash: os_image_hash.clone(), mr_system: app_info.mr_system.to_vec(), diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index 6909e6258..b3dd2db3e 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -10,16 +10,6 @@ use serde_human_bytes as hex_bytes; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct BootInfo { - #[serde(with = "hex_bytes")] - pub mrtd: Vec, - #[serde(with = "hex_bytes")] - pub rtmr0: Vec, - #[serde(with = "hex_bytes")] - pub rtmr1: Vec, - #[serde(with = "hex_bytes")] - pub rtmr2: Vec, - #[serde(with = "hex_bytes")] - pub rtmr3: Vec, #[serde(with = "hex_bytes")] pub mr_aggregated: Vec, #[serde(with = "hex_bytes")] diff --git a/verifier/src/main.rs b/verifier/src/main.rs index ea80a8d61..1bd72a918 100644 --- a/verifier/src/main.rs +++ b/verifier/src/main.rs @@ -149,10 +149,6 @@ async fn run_oneshot(file_path: &str, config: &Config) -> anyhow::Result<()> { println!("App ID: {}", hex::encode(&app_info.app_id)); println!("Instance ID: {}", hex::encode(&app_info.instance_id)); println!("Compose hash: {}", hex::encode(&app_info.compose_hash)); - println!("MRTD: {}", hex::encode(app_info.mrtd)); - println!("RTMR0: {}", hex::encode(app_info.rtmr0)); - println!("RTMR1: {}", hex::encode(app_info.rtmr1)); - println!("RTMR2: {}", hex::encode(app_info.rtmr2)); } // Exit with appropriate code From a308135b8a113d6e724858affbcfa08711a5908b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 21 Dec 2025 01:51:14 +0000 Subject: [PATCH 043/264] Add back rtmrs in GetInfo api --- dstack-attest/src/attestation.rs | 9 ++++++++- guest-agent/src/rpc_service.rs | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index a965869ed..a031efe16 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -310,6 +310,13 @@ impl Attestation { .as_ref() .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) } + + pub fn get_td10_report(&self) -> Option { + self.tdx_quote + .as_ref() + .and_then(|q| Quote::parse(&q.quote).ok()) + .and_then(|quote| quote.report.as_td10().cloned()) + } } pub trait GetPpid { @@ -501,7 +508,7 @@ impl Attestation { .flat_map(|event| event.to_runtime_event()) .collect(); let report_data = { - let quote = dcap_qvl::quote::Quote::parse("e).context("Invalid TDX quote")?; + let quote = Quote::parse("e).context("Invalid TDX quote")?; let report = quote.report.as_td10().context("Invalid TDX report")?; report.report_data }; diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 2c99e1c7b..0b2876f77 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -173,7 +173,22 @@ pub async fn get_info(state: &AppState, external: bool) -> Result { "".to_string() } else { let app_compose = state.config().app_compose.raw.clone(); + let td_report = match attestation.get_td10_report() { + Some(report) => json!({ + "mrtd": hex::encode(report.mr_td), + "rtmr0": hex::encode(report.rt_mr0), + "rtmr1": hex::encode(report.rt_mr1), + "rtmr2": hex::encode(report.rt_mr2), + "rtmr3": hex::encode(report.rt_mr3), + }), + None => json!({}), + }; serde_json::to_string_pretty(&json!({ + "mrtd": td_report["mrtd"], + "rtmr0": td_report["rtmr0"], + "rtmr1": td_report["rtmr1"], + "rtmr2": td_report["rtmr2"], + "rtmr3": td_report["rtmr3"], "mr_aggregated": hex::encode(app_info.mr_aggregated), "os_image_hash": hex::encode(&app_info.os_image_hash), "compose_hash": hex::encode(&app_info.compose_hash), From 2d7d07c54b4b0f0d955cdcecf819a0ed845f5140 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 21 Dec 2025 01:53:09 +0000 Subject: [PATCH 044/264] Fix some tests --- guest-agent/src/rpc_service.rs | 1 + ra-tls/src/cert.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 0b2876f77..275d5216f 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -757,6 +757,7 @@ mod tests { enabled: true, quote_file: dummy_quote_file.path().to_str().unwrap().to_string(), event_log_file: dummy_event_log_file.path().to_str().unwrap().to_string(), + attestation_file: String::new(), }; let dummy_appcompose = AppCompose { diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index c2bbbbb1a..eb5bb2c88 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -510,6 +510,7 @@ pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result Date: Sun, 21 Dec 2025 04:14:33 +0000 Subject: [PATCH 045/264] eventlog: Encode runtime event payload to base64 --- cc-eventlog/src/runtime_events.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cc-eventlog/src/runtime_events.rs b/cc-eventlog/src/runtime_events.rs index fa566bb3b..8b6993c5c 100644 --- a/cc-eventlog/src/runtime_events.rs +++ b/cc-eventlog/src/runtime_events.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use fs_err as fs; use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; +use serde_human_bytes::base64; use std::io::Write; use ez_hash::{Hasher, Sha256, Sha384}; @@ -19,6 +20,7 @@ pub struct RuntimeEvent { /// Event name pub event: String, /// Event payload + #[serde(with = "base64")] pub payload: Vec, } From 432d555260a71aed4d6543df5b18716bcd187fe2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 21 Dec 2025 04:15:06 +0000 Subject: [PATCH 046/264] ra-tls: Change Attestation oid to .8 --- ra-tls/src/oids.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ra-tls/src/oids.rs b/ra-tls/src/oids.rs index 801360e9e..38e71d1bc 100644 --- a/ra-tls/src/oids.rs +++ b/ra-tls/src/oids.rs @@ -13,4 +13,4 @@ pub const PHALA_RATLS_APP_ID: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 3]; /// OID for Special Certificate Usage. pub const PHALA_RATLS_CERT_USAGE: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 4]; /// OID for dstack attestation (the successor of TDX quote) -pub const PHALA_RATLS_ATTESTATION: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 5]; +pub const PHALA_RATLS_ATTESTATION: &[u64] = &[1, 3, 6, 1, 4, 1, 62397, 1, 8]; From 5b3c7c7f0133ac71daff47c4b874f8f58845da24 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 21 Dec 2025 04:15:48 +0000 Subject: [PATCH 047/264] Use PcrHandle::try_from for PCR Handle --- tpm-attest/src/esapi_impl.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/tpm-attest/src/esapi_impl.rs b/tpm-attest/src/esapi_impl.rs index b274fe51d..b8ff962ab 100644 --- a/tpm-attest/src/esapi_impl.rs +++ b/tpm-attest/src/esapi_impl.rs @@ -253,7 +253,7 @@ impl EsapiContext { /// Extend a PCR with a hash value pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { - use tss_esapi::handles::{PcrHandle, PcrTpmHandle}; + use tss_esapi::handles::PcrHandle; use tss_esapi::structures::Digest; let hash_alg = match bank { @@ -263,13 +263,9 @@ impl EsapiContext { _ => bail!("unsupported hash algorithm: {bank}"), }; - // Create PCR handle from index - let pcr_tpm_handle = - PcrTpmHandle::new(pcr).with_context(|| format!("invalid PCR index: {pcr}"))?; - let object_handle = self - .context - .tr_from_tpm_public(TpmHandle::Pcr(pcr_tpm_handle)) - .context("failed to get PCR handle")?; + // PCR handles are pre-defined; do not resolve via ReadPublic. + let pcr_handle = + PcrHandle::try_from(pcr).with_context(|| format!("invalid PCR index: {pcr}"))?; // Create digest from hash bytes let digest = @@ -279,10 +275,6 @@ impl EsapiContext { let mut digest_values = DigestValues::new(); digest_values.set(hash_alg, digest); - // Extend PCR - convert object_handle via Into trait - let pcr_handle: PcrHandle = object_handle - .try_into() - .context("failed to convert object handle to PCR handle")?; self.context .execute_with_nullauth_session(|ctx| ctx.pcr_extend(pcr_handle, digest_values)) .with_context(|| format!("failed to extend PCR {pcr} with {bank}"))?; From 7718200af4dc9697d20ab05665644c470e8da291 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 21 Dec 2025 04:16:22 +0000 Subject: [PATCH 048/264] Fix unit tests --- tpm-attest/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs index 73359d854..ba5dcc8d7 100644 --- a/tpm-attest/src/lib.rs +++ b/tpm-attest/src/lib.rs @@ -330,7 +330,7 @@ mod tests { #[test] fn test_default_pcr_policy() { let policy = dstack_pcr_policy(); - assert_eq!(policy.to_arg(), "sha256:0,1,2,3,4,5,6,7,8,9,14"); + assert_eq!(policy.to_arg(), "sha256:0,2,14"); } } From 2b885a37b28d25a2d6a0911ef9b24d6f303a3ccf Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 21 Dec 2025 04:16:44 +0000 Subject: [PATCH 049/264] Update attestation.bin --- sdk/simulator/attestation.bin | Bin 38158 -> 39090 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/sdk/simulator/attestation.bin b/sdk/simulator/attestation.bin index 18da636435dfbb371629ad76d43925defdb946c6..e757b3da4d3275485806bba237e737c717809c87 100644 GIT binary patch delta 4195 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 Date: Sun, 21 Dec 2025 12:44:53 +0000 Subject: [PATCH 050/264] verifier: use hex_literal --- Cargo.lock | 1 + verifier/Cargo.toml | 1 + verifier/src/verification.rs | 14 ++++++++------ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb3e752ce..5aa21f50d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2493,6 +2493,7 @@ dependencies = [ "figment", "fs-err", "hex", + "hex-literal 1.1.0", "ra-tls", "reqwest", "rocket", diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 1256e149c..0b8917b7e 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -46,6 +46,7 @@ sha2.workspace = true tpm-qvl.workspace = true tpm-types.workspace = true serde-human-bytes.workspace = true +hex-literal.workspace = true [features] default = ["binary"] diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 2a7254dc8..2ef335421 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -12,6 +12,7 @@ use anyhow::{anyhow, bail, Context, Result}; use cc_eventlog::TdxEvent; use dstack_mr::{RtmrLog, TdxMeasurementDetails, TdxMeasurements}; use dstack_types::VmConfig; +use hex_literal::hex; use ra_tls::attestation::{ Attestation, AttestationMode, TpmQuote, VerifiedAttestation, VersionedAttestation, }; @@ -641,8 +642,8 @@ impl CvmVerifier { tpm_quote: &TpmQuote, ) -> Result<()> { // Verify PCR 0 (GCP OVMF firmware) - const EXPECTED_PCR0: &str = - "0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802"; + const EXPECTED_PCR0: [u8; 32] = + hex!("0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802"); let pcr0 = tpm_quote .pcr_values @@ -650,8 +651,6 @@ impl CvmVerifier { .find(|p| p.index == 0) .context("PCR 0 not found in TPM quote")?; - let pcr0_hex = hex::encode(&pcr0.value); - // Get expected UKI hash from os_image_hash (which should be set to UKI Authenticode hash) let expected_uki_hash = &vm_config.os_image_hash; @@ -664,8 +663,11 @@ impl CvmVerifier { // Extract Event 28 (3rd event, 0-indexed as 2) // NOTE: This is GCP OVMF-specific behavior let event_28_digest = { - if pcr0_hex != EXPECTED_PCR0 { - bail!("PCR 0 mismatch: expected GCP OVMF v2 ({EXPECTED_PCR0}), got {pcr0_hex}"); + if pcr0.value != EXPECTED_PCR0 { + bail!( + "PCR 0 mismatch: expected GCP OVMF v2, got {}", + hex::encode(&pcr0.value) + ); } &pcr2_events.get(2).context("Event 28 not found")?.digest }; From 72ffe344dacf2316c7e4a42803eda2af4e3cd15b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 02:13:44 +0000 Subject: [PATCH 051/264] vmm-ui: Default to zfs in clone config --- vmm/ui/src/composables/useVmManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vmm/ui/src/composables/useVmManager.ts b/vmm/ui/src/composables/useVmManager.ts index 023bd5608..d7553420f 100644 --- a/vmm/ui/src/composables/useVmManager.ts +++ b/vmm/ui/src/composables/useVmManager.ts @@ -1101,7 +1101,7 @@ type CreateVmPayloadSource = { attachAllGpus: false, encryptedEnvs: [], // Clear environment variables ports: [], // Clear port mappings - storage_fs: theVm.appCompose?.storage_fs || 'ext4', + storage_fs: theVm.appCompose?.storage_fs || 'zfs', app_id: config.app_id || '', kms_urls: config.kms_urls || [], key_provider: getKeyProvider(theVm), From 678226c6188a2014129daa92a374a5facf5bfb99 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 04:32:50 +0000 Subject: [PATCH 052/264] cvm: Skip host notify on GCP --- dstack-util/src/host_api.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index 0fdb264a0..5577e4cec 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -4,7 +4,10 @@ use crate::utils::{deserialize_json_file, sha256, SysConfig}; use anyhow::{anyhow, bail, Context, Result}; -use dstack_types::shared_filenames::{HOST_SHARED_DIR, SYS_CONFIG}; +use dstack_types::{ + shared_filenames::{HOST_SHARED_DIR, SYS_CONFIG}, + Platform, +}; use host_api::{ client::{new_client, DefaultClient}, Notification, @@ -53,6 +56,13 @@ impl HostApi { } pub async fn notify(&self, event: &str, payload: &str) -> Result<()> { + match Platform::detect_or_dstack() { + Platform::Dstack => {} + Platform::Gcp => { + // Skip notify on GCP as no host dstack-vmm there. + return Ok(()); + } + } self.client .notify(Notification { event: event.to_string(), From 328273adfa27d96dab459fbc4bf8495b24a76627 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 11:50:57 +0000 Subject: [PATCH 053/264] Create containerd dir and let containerd start after dstack-prepare --- basefiles/containerd.service.d/dstack-prepare.conf | 7 +++++++ basefiles/docker.service.d/dstack-prepare.conf | 7 +++++++ basefiles/dstack-prepare.sh | 2 ++ 3 files changed, 16 insertions(+) create mode 100644 basefiles/containerd.service.d/dstack-prepare.conf create mode 100644 basefiles/docker.service.d/dstack-prepare.conf diff --git a/basefiles/containerd.service.d/dstack-prepare.conf b/basefiles/containerd.service.d/dstack-prepare.conf new file mode 100644 index 000000000..6bbda8246 --- /dev/null +++ b/basefiles/containerd.service.d/dstack-prepare.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[Unit] +Wants=dstack-prepare.service +After=dstack-prepare.service diff --git a/basefiles/docker.service.d/dstack-prepare.conf b/basefiles/docker.service.d/dstack-prepare.conf new file mode 100644 index 000000000..6bbda8246 --- /dev/null +++ b/basefiles/docker.service.d/dstack-prepare.conf @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[Unit] +Wants=dstack-prepare.service +After=dstack-prepare.service diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index 43f61713b..7bee227f1 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -262,7 +262,9 @@ dstack-util setup --work-dir $WORK_DIR --device "$DATA_DEVICE" --mount-point $DA log "Mounting docker dirs to persistent storage" # Mount docker dirs to DATA_MNT mkdir -p $DATA_MNT/var/lib/docker +mkdir -p $DATA_MNT/var/lib/containerd mount --rbind $DATA_MNT/var/lib/docker /var/lib/docker +mount --rbind $DATA_MNT/var/lib/containerd /var/lib/containerd mount --rbind $WORK_DIR /dstack cd /dstack From 236108163ea5de560036eecf2654c1f336596ee0 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 11:51:24 +0000 Subject: [PATCH 054/264] cvm: Print df -h on start --- basefiles/dstack-prepare.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index 7bee227f1..fc863a86b 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -267,6 +267,10 @@ mount --rbind $DATA_MNT/var/lib/docker /var/lib/docker mount --rbind $DATA_MNT/var/lib/containerd /var/lib/containerd mount --rbind $WORK_DIR /dstack +echo "======== Disk usage ========" +df -h +echo "============================" + cd /dstack if [ $(jq 'has("init_script")' app-compose.json) == true ]; then From bccc20dd1dfcde88fc3f4860b19333b523b1a7cf Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 11:51:49 +0000 Subject: [PATCH 055/264] remove-orphans: Don't print Docker containers directory does not exist --- dstack-util/src/docker_compose.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dstack-util/src/docker_compose.rs b/dstack-util/src/docker_compose.rs index d20170506..b5f02eb78 100644 --- a/dstack-util/src/docker_compose.rs +++ b/dstack-util/src/docker_compose.rs @@ -161,10 +161,6 @@ pub fn remove_orphans_direct( let containers_dir = docker_root.as_ref().join("containers"); if !containers_dir.exists() { - println!( - "Docker containers directory does not exist: {}", - containers_dir.display() - ); return Ok(()); } From 7427b781ddbfca317ea0f37117151346eae3bd75 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 12:08:01 +0000 Subject: [PATCH 056/264] Update rust to 1.92.0 --- .github/workflows/rust.yml | 3 ++- .github/workflows/sdk.yaml | 3 ++- gateway/dstack-app/builder/Dockerfile | 2 +- kms/dstack-app/builder/Dockerfile | 2 +- kms/dstack-app/docker-compose.yaml | 4 ++-- verifier/builder/Dockerfile | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 006c7808b..58e709e2b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -20,8 +20,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@1.86 + uses: dtolnay/rust-toolchain@master with: + toolchain: 1.92.0 components: clippy, rustfmt - name: Run Clippy diff --git a/.github/workflows/sdk.yaml b/.github/workflows/sdk.yaml index d64ad9d9a..e8abf7afe 100644 --- a/.github/workflows/sdk.yaml +++ b/.github/workflows/sdk.yaml @@ -23,8 +23,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@1.86 + uses: dtolnay/rust-toolchain@master with: + toolchain: 1.92.0 components: clippy, rustfmt # This additional target is needed for wasm32 compatibility check. targets: wasm32-unknown-unknown, thumbv6m-none-eabi diff --git a/gateway/dstack-app/builder/Dockerfile b/gateway/dstack-app/builder/Dockerfile index ba5bedb0c..3f0076429 100644 --- a/gateway/dstack-app/builder/Dockerfile +++ b/gateway/dstack-app/builder/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 AS gateway-builder +FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS gateway-builder COPY ./shared /build ARG DSTACK_REV WORKDIR /build diff --git a/kms/dstack-app/builder/Dockerfile b/kms/dstack-app/builder/Dockerfile index e9c9448b5..d62015359 100644 --- a/kms/dstack-app/builder/Dockerfile +++ b/kms/dstack-app/builder/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 AS kms-builder +FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS kms-builder COPY ./shared /build ARG DSTACK_REV ARG DSTACK_SRC_URL=https://github.com/Dstack-TEE/dstack.git diff --git a/kms/dstack-app/docker-compose.yaml b/kms/dstack-app/docker-compose.yaml index 43fe43ede..3d8a18f5d 100644 --- a/kms/dstack-app/docker-compose.yaml +++ b/kms/dstack-app/docker-compose.yaml @@ -8,7 +8,7 @@ services: build: context: . dockerfile_inline: | - FROM rust:1.86-alpine@sha256:661d708cc863ce32007cf46807a72062a80d2944a6fae9e0d83742d2e04d5375 + FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 WORKDIR /app RUN apk add --no-cache git build-base openssl-dev protobuf protobuf-dev perl RUN git clone https://github.com/a16z/helios && \ @@ -57,7 +57,7 @@ services: build: context: . dockerfile_inline: | - FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 + FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 WORKDIR /app RUN apt-get update && apt-get install -y \ git \ diff --git a/verifier/builder/Dockerfile b/verifier/builder/Dockerfile index cef0d128a..2f7d6a7e7 100644 --- a/verifier/builder/Dockerfile +++ b/verifier/builder/Dockerfile @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -FROM rust:1.86.0@sha256:300ec56abce8cc9448ddea2172747d048ed902a3090e6b57babb2bf19f754081 AS verifier-builder +FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS verifier-builder COPY builder/shared /build/shared ARG DSTACK_REV ARG DSTACK_SRC_URL=https://github.com/Dstack-TEE/dstack.git From 26e4f0ac693fec5f31d060ede9c3052feb107cb2 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 12:15:41 +0000 Subject: [PATCH 057/264] Fix reuse lint --- LICENSES/BSD-3-Clause.txt | 11 ----------- REUSE.toml | 9 ++++----- cc-eventlog/src/runtime_events.rs | 4 ++++ cc-eventlog/src/tdx.rs | 4 ++++ dstack-util/tests/test_remove_orphans.sh | 4 ++++ ra-tls/src/attestation.rs | 4 ++++ tpm-attest/src/gcp_ak.rs | 4 ++++ 7 files changed, 24 insertions(+), 16 deletions(-) delete mode 100644 LICENSES/BSD-3-Clause.txt diff --git a/LICENSES/BSD-3-Clause.txt b/LICENSES/BSD-3-Clause.txt deleted file mode 100644 index ea890afbc..000000000 --- a/LICENSES/BSD-3-Clause.txt +++ /dev/null @@ -1,11 +0,0 @@ -Copyright (c) . - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/REUSE.toml b/REUSE.toml index 8f345c27f..35055f15a 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -66,8 +66,12 @@ path = [ "docs/security/dstack-audit.pdf", "dstack_Technical_Charter_Final_10-17-2025.pdf", "sdk/simulator/quote.hex", + "sdk/simulator/attestation.bin", "ra-tls/assets/tdx_quote", "cc-eventlog/samples/ccel.bin", + "cc-eventlog/samples/tpm_eventlog.bin", + "tpm-attest/tests/tpm_quote_sample.bin", + "tpm-qvl/certs/gcp-root-ca.pem", ] SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "CC0-1.0" @@ -102,11 +106,6 @@ path = "kms/auth-eth/lib/forge-std/**" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "Apache-2.0" -[[annotations]] -path = "tdx-attest-sys/csrc/*" -SPDX-FileCopyrightText = "Copyright (C) 2011-2021 Intel Corporation. All rights reserved." -SPDX-License-Identifier = "BSD-3-Clause" - [[annotations]] path = "mod-tdx-guest/Kconfig" SPDX-FileCopyrightText = "© 2022 Intel Corporation" diff --git a/cc-eventlog/src/runtime_events.rs b/cc-eventlog/src/runtime_events.rs index 8b6993c5c..ace6cd970 100644 --- a/cc-eventlog/src/runtime_events.rs +++ b/cc-eventlog/src/runtime_events.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + use anyhow::{Context, Result}; use fs_err as fs; use scale::{Decode, Encode}; diff --git a/cc-eventlog/src/tdx.rs b/cc-eventlog/src/tdx.rs index da046afa6..bf7d677c0 100644 --- a/cc-eventlog/src/tdx.rs +++ b/cc-eventlog/src/tdx.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + use anyhow::Result; use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; diff --git a/dstack-util/tests/test_remove_orphans.sh b/dstack-util/tests/test_remove_orphans.sh index d73c04194..0400693b8 100755 --- a/dstack-util/tests/test_remove_orphans.sh +++ b/dstack-util/tests/test_remove_orphans.sh @@ -1,4 +1,8 @@ #!/bin/bash + +# SPDX-FileCopyrightText: © 2025 Phala Network +# SPDX-License-Identifier: Apache-2.0 + # Test script for remove-orphans command (both online and offline modes) # Uses real docker compose to create containers for accurate testing diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index 591862a2c..fcd6815a6 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + //! Embedding and extracting attestation from/to TLS certificate pub use dstack_attest::attestation::*; diff --git a/tpm-attest/src/gcp_ak.rs b/tpm-attest/src/gcp_ak.rs index 2f28182eb..d4bf76fae 100644 --- a/tpm-attest/src/gcp_ak.rs +++ b/tpm-attest/src/gcp_ak.rs @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + //! GCP vTPM pre-provisioned AK loading using tss-esapi //! //! This module provides native Rust implementation for loading GCP's From c613616b1d1e740858bcc0cf04ee1f435890e0d6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 14:59:48 +0000 Subject: [PATCH 058/264] vmm: Minor comment update --- vmm/src/app/qemu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index 62bb37d1b..735ca785f 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -95,7 +95,7 @@ fn create_hd( Ok(()) } -/// Create a FAT32 disk image from a directory using pure Rust (no external commands) +/// Create a FAT32 disk image from a directory fn create_shared_disk(disk_path: impl AsRef, shared_dir: impl AsRef) -> Result<()> { use fatfs::{FileSystem, FormatVolumeOptions, FsOptions}; use std::io::{Cursor, Seek, SeekFrom, Write}; From 31def0250a6264b234455f1dd7aae9c95c3990cb Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 22 Dec 2025 14:59:32 +0000 Subject: [PATCH 059/264] Pure Rust tpmp2 crate implementation --- Cargo.lock | 161 +------ Cargo.toml | 2 + tpm-attest/Cargo.toml | 4 +- tpm-attest/src/esapi.rs | 138 ++++++ tpm-attest/src/esapi_impl.rs | 657 -------------------------- tpm-attest/src/gcp_ak.rs | 348 +++++--------- tpm-attest/src/lib.rs | 6 +- tpm2/Cargo.toml | 29 ++ tpm2/src/bin/tpm2-test.rs | 833 +++++++++++++++++++++++++++++++++ tpm2/src/commands.rs | 648 ++++++++++++++++++++++++++ tpm2/src/constants.rs | 380 +++++++++++++++ tpm2/src/device.rs | 313 +++++++++++++ tpm2/src/lib.rs | 49 ++ tpm2/src/marshal.rs | 264 +++++++++++ tpm2/src/session.rs | 183 ++++++++ tpm2/src/types.rs | 876 +++++++++++++++++++++++++++++++++++ 16 files changed, 3839 insertions(+), 1052 deletions(-) create mode 100644 tpm-attest/src/esapi.rs delete mode 100644 tpm-attest/src/esapi_impl.rs create mode 100644 tpm2/Cargo.toml create mode 100644 tpm2/src/bin/tpm2-test.rs create mode 100644 tpm2/src/commands.rs create mode 100644 tpm2/src/constants.rs create mode 100644 tpm2/src/device.rs create mode 100644 tpm2/src/lib.rs create mode 100644 tpm2/src/marshal.rs create mode 100644 tpm2/src/session.rs create mode 100644 tpm2/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 5aa21f50d..7991ea67d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -948,12 +948,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -1021,12 +1015,6 @@ dependencies = [ "hex-conservative", ] -[[package]] -name = "bitfield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" - [[package]] name = "bitflags" version = "1.3.2" @@ -2724,26 +2712,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "env_home" version = "0.1.0" @@ -3504,12 +3472,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "hostname-validator" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" - [[package]] name = "http" version = "1.4.0" @@ -4326,16 +4288,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" -[[package]] -name = "mbox" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d142aeadbc4e8c679fc6d93fbe7efe1c021fa7d80629e615915b519e3bc6de" -dependencies = [ - "libc", - "stable_deref_trait", -] - [[package]] name = "md-5" version = "0.10.6" @@ -4671,17 +4623,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -4776,15 +4717,6 @@ dependencies = [ "ruzstd", ] -[[package]] -name = "oid" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" -dependencies = [ - "serde", -] - [[package]] name = "oid-registry" version = "0.7.1" @@ -5118,41 +5050,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "picky-asn1" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295eea0f33c16be21e2a98b908fdd4d73c04dd48c8480991b76dbcf0cb58b212" -dependencies = [ - "oid", - "serde", - "serde_bytes", -] - -[[package]] -name = "picky-asn1-der" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df7873a9e36d42dadb393bea5e211fe83d793c172afad5fb4ec846ec582793f" -dependencies = [ - "picky-asn1", - "serde", - "serde_bytes", -] - -[[package]] -name = "picky-asn1-x509" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c5f20f71a68499ff32310f418a6fad8816eac1a2859ed3f0c5c741389dd6208" -dependencies = [ - "base64 0.21.7", - "oid", - "picky-asn1", - "picky-asn1-der", - "serde", -] - [[package]] name = "pin-project" version = "1.1.10" @@ -5206,12 +5103,6 @@ dependencies = [ "spki", ] -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "poly1305" version = "0.8.0" @@ -7353,12 +7244,6 @@ dependencies = [ "xattr", ] -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tdx-attest" version = "0.5.5" @@ -7749,8 +7634,8 @@ dependencies = [ "sha2 0.10.9", "tempfile", "tpm-types", + "tpm2", "tracing", - "tss-esapi", ] [[package]] @@ -7788,6 +7673,17 @@ dependencies = [ "serde-human-bytes", ] +[[package]] +name = "tpm2" +version = "0.5.5" +dependencies = [ + "anyhow", + "hex", + "sha2 0.10.9", + "tempfile", + "tracing", +] + [[package]] name = "tracing" version = "0.1.43" @@ -7856,39 +7752,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tss-esapi" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ea9ccde878b029392ac97b5be1f470173d06ea41d18ad0bb3c92794c16a0f2" -dependencies = [ - "bitfield", - "enumflags2", - "getrandom 0.2.16", - "hostname-validator", - "log", - "mbox", - "num-derive", - "num-traits", - "oid", - "picky-asn1", - "picky-asn1-x509", - "regex", - "serde", - "tss-esapi-sys", - "zeroize", -] - -[[package]] -name = "tss-esapi-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535cd192581c2ec4d5f82e670b1d3fbba6a23ccce8c85de387642051d7cad5b5" -dependencies = [ - "pkg-config", - "target-lexicon", -] - [[package]] name = "twox-hash" version = "1.6.3" diff --git a/Cargo.toml b/Cargo.toml index 0b2e19391..6072b6c4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "ra-tls", "tdx-attest", "tpm-attest", + "tpm2", "tpm-types", "tpm-qvl", "dstack-attest", @@ -72,6 +73,7 @@ supervisor = { path = "supervisor" } supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } tpm-attest = { path = "tpm-attest" } +tpm2 = { path = "tpm2" } tpm-types = { path = "tpm-types" } dstack-attest = { path = "dstack-attest" } tpm-qvl = { path = "tpm-qvl" } diff --git a/tpm-attest/Cargo.toml b/tpm-attest/Cargo.toml index 01857e319..add86c812 100644 --- a/tpm-attest/Cargo.toml +++ b/tpm-attest/Cargo.toml @@ -22,5 +22,5 @@ fs-err.workspace = true tpm-types.workspace = true dstack-types.workspace = true -# TPM 2.0 TSS ESAPI for low-level TPM operations -tss-esapi = "7.5" +# Pure Rust TPM 2.0 implementation - no C library dependencies +tpm2.workspace = true diff --git a/tpm-attest/src/esapi.rs b/tpm-attest/src/esapi.rs new file mode 100644 index 000000000..cfe7cdf98 --- /dev/null +++ b/tpm-attest/src/esapi.rs @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +use crate::{PcrSelection, PcrValue}; +use anyhow::{bail, Result}; +use tpm2::{TpmAlgId, TpmContext as RawTpmContext, TpmlPcrSelection}; + +pub struct EsapiContext { + context: RawTpmContext, +} + +impl EsapiContext { + /// Create a new ESAPI context with the given TCTI path + pub fn new(tcti_path: Option<&str>) -> Result { + let context = RawTpmContext::new(tcti_path)?; + Ok(Self { context }) + } + + // ==================== NV Operations ==================== + + /// Check if an NV index exists + pub fn nv_exists(&mut self, index: u32) -> Result { + self.context.nv_exists(index) + } + + /// Read data from an NV index + pub fn nv_read(&mut self, index: u32) -> Result>> { + self.context.nv_read(index) + } + + /// Write data to an NV index + pub fn nv_write(&mut self, index: u32, data: &[u8]) -> Result { + self.context.nv_write(index, data) + } + + /// Define a new NV index + pub fn nv_define(&mut self, index: u32, size: usize, owner_read_write: bool) -> Result { + self.context.nv_define(index, size, owner_read_write) + } + + /// Undefine (delete) an NV index + pub fn nv_undefine(&mut self, index: u32) -> Result { + self.context.nv_undefine(index) + } + + // ==================== PCR Operations ==================== + + /// Read PCR values for the given selection + pub fn pcr_read(&mut self, pcr_selection: &PcrSelection) -> Result> { + let hash_alg = Self::parse_hash_alg(&pcr_selection.bank)?; + let tpm_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + let values = self.context.pcr_read(&tpm_selection)?; + + Ok(values + .into_iter() + .map(|(index, value)| PcrValue { + index, + algorithm: pcr_selection.bank.clone(), + value, + }) + .collect()) + } + + /// Extend a PCR with a hash value + pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { + let hash_alg = Self::parse_hash_alg(bank)?; + self.context.pcr_extend(pcr, hash, hash_alg) + } + + // ==================== Random Number Generation ==================== + + /// Generate random bytes using the TPM's hardware RNG + pub fn get_random(&mut self) -> Result<[u8; N]> { + self.context.get_random_array::() + } + + // ==================== Primary Key Operations ==================== + + /// Check if a persistent handle exists + pub fn handle_exists(&mut self, handle: u32) -> Result { + self.context.handle_exists(handle) + } + + /// Ensure a persistent primary key exists at the given handle + pub fn ensure_primary_key(&mut self, handle: u32) -> Result { + self.context.ensure_primary_key(handle) + } + + // ==================== Seal/Unseal Operations ==================== + + /// Seal data to TPM with PCR policy + pub fn seal( + &mut self, + data: &[u8], + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result<(Vec, Vec)> { + let hash_alg = Self::parse_hash_alg(&pcr_selection.bank)?; + let tpm_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + self.context + .seal(data, parent_handle, &tpm_selection, hash_alg) + } + + /// Unseal data from TPM with PCR policy + pub fn unseal( + &mut self, + pub_bytes: &[u8], + priv_bytes: &[u8], + parent_handle: u32, + pcr_selection: &PcrSelection, + ) -> Result> { + let hash_alg = Self::parse_hash_alg(&pcr_selection.bank)?; + let tpm_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); + + self.context.unseal( + pub_bytes, + priv_bytes, + parent_handle, + &tpm_selection, + hash_alg, + ) + } + + // ==================== Helper Functions ==================== + + fn parse_hash_alg(bank: &str) -> Result { + match bank { + "sha256" => Ok(TpmAlgId::Sha256), + "sha384" => Ok(TpmAlgId::Sha384), + "sha512" => Ok(TpmAlgId::Sha512), + "sha1" => Ok(TpmAlgId::Sha1), + _ => bail!("unsupported hash algorithm: {}", bank), + } + } +} diff --git a/tpm-attest/src/esapi_impl.rs b/tpm-attest/src/esapi_impl.rs deleted file mode 100644 index b8ff962ab..000000000 --- a/tpm-attest/src/esapi_impl.rs +++ /dev/null @@ -1,657 +0,0 @@ -// SPDX-FileCopyrightText: © 2025 Phala Network -// -// SPDX-License-Identifier: Apache-2.0 - -//! Pure tss-esapi implementation of TPM operations -//! -//! This module provides a clean implementation using only tss-esapi, -//! without relying on tpm2-tools command-line utilities. - -use anyhow::{bail, Context as _, Result}; -use std::convert::TryFrom; -use tracing::{debug, warn}; -use tss_esapi::{ - abstraction::nv, - constants::SessionType, - handles::{NvIndexTpmHandle, PersistentTpmHandle, SessionHandle, TpmHandle}, - interface_types::{algorithm::HashingAlgorithm, resource_handles::Hierarchy}, - structures::{DigestValues, PcrSelectionListBuilder, PcrSlot, SymmetricDefinition}, - tcti_ldr::{DeviceConfig, TctiNameConf}, - traits::{Marshall, UnMarshall}, - Context as TssContext, -}; - -use crate::{PcrSelection, PcrValue}; - -/// TPM context using tss-esapi -pub struct EsapiContext { - context: TssContext, -} - -impl EsapiContext { - /// Create a new ESAPI context with the given TCTI path - pub fn new(tcti_path: Option<&str>) -> Result { - use std::str::FromStr; - - let tcti_str = tcti_path.unwrap_or("/dev/tpmrm0"); - - // Strip "device:" prefix if present (tss-esapi expects path without prefix) - let device_path = tcti_str.strip_prefix("device:").unwrap_or(tcti_str); - - let device_config = - DeviceConfig::from_str(device_path).context("failed to parse device config")?; - let tcti = TctiNameConf::Device(device_config); - - let context = TssContext::new(tcti).context("failed to create TSS context")?; - - Ok(Self { context }) - } - - // ==================== NV Operations ==================== - - /// Check if an NV index exists - pub fn nv_exists(&mut self, index: u32) -> Result { - let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; - let nv_index = self.context.tr_from_tpm_public(TpmHandle::NvIndex(handle)); - - match nv_index { - Ok(_) => Ok(true), - Err(_) => Ok(false), - } - } - - /// Read data from an NV index - pub fn nv_read(&mut self, index: u32) -> Result>> { - use tss_esapi::interface_types::resource_handles::NvAuth; - - let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; - - // Get NV index handle from TPM - let nv_auth_handle = TpmHandle::NvIndex(handle); - let nv_auth_handle = match self.context.execute_without_session(|ctx| { - ctx.tr_from_tpm_public(nv_auth_handle) - .map(|v| NvAuth::NvIndex(v.into())) - }) { - Ok(h) => h, - Err(e) => { - warn!("failed to get NV index handle for 0x{index:08x}: {e}"); - return Ok(None); - } - }; - - // Read NV data with null auth session - match self - .context - .execute_with_nullauth_session(|ctx| nv::read_full(ctx, nv_auth_handle, handle)) - { - Ok(data) => Ok(Some(data.to_vec())), - Err(e) => { - warn!("nv_read failed for index 0x{index:08x}: {e}"); - Ok(None) - } - } - } - - /// Write data to an NV index - pub fn nv_write(&mut self, index: u32, data: &[u8]) -> Result { - use tss_esapi::handles::NvIndexHandle; - use tss_esapi::interface_types::resource_handles::NvAuth; - use tss_esapi::structures::MaxNvBuffer; - - let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; - - // Get NV index handle from TPM - let nv_index = self - .context - .tr_from_tpm_public(TpmHandle::NvIndex(handle)) - .context("failed to get NV index handle")?; - let nv_index_handle = NvIndexHandle::from(nv_index); - let nv_auth = NvAuth::NvIndex(nv_index.into()); - - // Write data in chunks (TPM has max buffer size) - let max_chunk = 1024usize; // Conservative chunk size - let mut offset = 0u16; - - while offset < data.len() as u16 { - let chunk_end = ((offset as usize) + max_chunk).min(data.len()); - let chunk = &data[(offset as usize)..chunk_end]; - - let nv_data = - MaxNvBuffer::try_from(chunk.to_vec()).context("data exceeds NV buffer size")?; - - self.context - .execute_with_nullauth_session(|ctx| { - ctx.nv_write(nv_auth, nv_index_handle, nv_data, offset) - }) - .with_context(|| { - format!("failed to write to NV index 0x{index:08x} at offset {offset}") - })?; - - offset = chunk_end as u16; - } - - debug!("wrote {} bytes to NV index 0x{index:08x}", data.len()); - Ok(true) - } - - /// Define a new NV index - pub fn nv_define(&mut self, index: u32, size: usize, owner_read_write: bool) -> Result { - use tss_esapi::attributes::NvIndexAttributesBuilder; - use tss_esapi::interface_types::resource_handles::Provision; - use tss_esapi::structures::NvPublicBuilder; - - let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; - - // Build NV index attributes - let mut attributes = NvIndexAttributesBuilder::new(); - if owner_read_write { - attributes = attributes.with_owner_write(true).with_owner_read(true); - } - let attributes = attributes - .build() - .context("failed to build NV attributes")?; - - // Build NV public structure - let nv_public = NvPublicBuilder::new() - .with_nv_index(handle) - .with_index_name_algorithm(HashingAlgorithm::Sha256) - .with_index_attributes(attributes) - .with_data_area_size(size) - .build() - .context("failed to build NV public")?; - - // Define NV space - match self.context.execute_with_nullauth_session(|ctx| { - ctx.nv_define_space(Provision::Owner, None, nv_public) - }) { - Ok(_) => { - debug!("defined NV index 0x{index:08x} with size {size}"); - Ok(true) - } - Err(e) => { - warn!("nv_define failed for index 0x{index:08x}: {e}"); - Ok(false) - } - } - } - - /// Undefine (delete) an NV index - pub fn nv_undefine(&mut self, index: u32) -> Result { - use tss_esapi::handles::NvIndexHandle; - use tss_esapi::interface_types::resource_handles::Provision; - - let handle = NvIndexTpmHandle::new(index).context("invalid NV index")?; - - // Get NV index handle - let nv_idx = match self.context.tr_from_tpm_public(TpmHandle::NvIndex(handle)) { - Ok(h) => NvIndexHandle::from(h), - Err(e) => { - warn!("failed to get NV index handle for 0x{index:08x}: {e}"); - return Ok(false); - } - }; - - // Undefine NV space - match self - .context - .execute_with_nullauth_session(|ctx| ctx.nv_undefine_space(Provision::Owner, nv_idx)) - { - Ok(_) => { - debug!("undefined NV index 0x{index:08x}"); - Ok(true) - } - Err(e) => { - warn!("nv_undefine failed for index 0x{index:08x}: {e}"); - Ok(false) - } - } - } - - // ==================== PCR Operations ==================== - - /// Read PCR values for the given selection - pub fn pcr_read(&mut self, pcr_selection: &PcrSelection) -> Result> { - let hash_alg = match pcr_selection.bank.as_str() { - "sha256" => HashingAlgorithm::Sha256, - "sha384" => HashingAlgorithm::Sha384, - "sha512" => HashingAlgorithm::Sha512, - _ => bail!( - "unsupported hash algorithm: {bank}", - bank = pcr_selection.bank - ), - }; - - let mut pcr_values = Vec::new(); - - // Read each PCR individually to ensure correct index mapping - for pcr_idx in &pcr_selection.pcrs { - let bit_mask = 1u32 << pcr_idx; - let pcr_slot = PcrSlot::try_from(bit_mask) - .with_context(|| format!("invalid PCR index: {pcr_idx}"))?; - - let pcr_selection_list = PcrSelectionListBuilder::new() - .with_selection(hash_alg, &[pcr_slot]) - .build() - .context("failed to build PCR selection list")?; - - let (_update_counter, _pcr_sel_out, digest_list) = self - .context - .execute_without_session(|ctx| ctx.pcr_read(pcr_selection_list)) - .context("failed to read PCR")?; - - if let Some(digest) = digest_list.value().first() { - pcr_values.push(PcrValue { - index: *pcr_idx, - algorithm: pcr_selection.bank.clone(), - value: digest.value().to_vec(), - }); - } - } - - Ok(pcr_values) - } - - /// Extend a PCR with a hash value - pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], bank: &str) -> Result<()> { - use tss_esapi::handles::PcrHandle; - use tss_esapi::structures::Digest; - - let hash_alg = match bank { - "sha256" => HashingAlgorithm::Sha256, - "sha384" => HashingAlgorithm::Sha384, - "sha512" => HashingAlgorithm::Sha512, - _ => bail!("unsupported hash algorithm: {bank}"), - }; - - // PCR handles are pre-defined; do not resolve via ReadPublic. - let pcr_handle = - PcrHandle::try_from(pcr).with_context(|| format!("invalid PCR index: {pcr}"))?; - - // Create digest from hash bytes - let digest = - Digest::try_from(hash.to_vec()).context("failed to create digest from hash")?; - - // Create DigestValues with the hash algorithm - let mut digest_values = DigestValues::new(); - digest_values.set(hash_alg, digest); - - self.context - .execute_with_nullauth_session(|ctx| ctx.pcr_extend(pcr_handle, digest_values)) - .with_context(|| format!("failed to extend PCR {pcr} with {bank}"))?; - - debug!("extended PCR {pcr} with {bank} hash"); - Ok(()) - } - - // ==================== Random Number Generation ==================== - - /// Generate random bytes using the TPM's hardware RNG - pub fn get_random(&mut self) -> Result<[u8; N]> { - let random_bytes = self - .context - .get_random(N) - .context("failed to get random bytes from TPM")?; - - let bytes: [u8; N] = random_bytes - .as_slice() - .try_into() - .context("insufficient random bytes from TPM")?; - - Ok(bytes) - } - - // ==================== Primary Key Operations ==================== - - /// Check if a persistent handle exists - pub fn handle_exists(&mut self, handle: u32) -> Result { - let persistent = PersistentTpmHandle::new(handle).context("invalid persistent handle")?; - - match self - .context - .tr_from_tpm_public(TpmHandle::Persistent(persistent)) - { - Ok(_) => Ok(true), - Err(_) => Ok(false), - } - } - - /// Create a primary key in the owner hierarchy - /// Uses RSA 2048 storage key template for sealing operations - pub fn create_primary(&mut self) -> Result { - use tss_esapi::attributes::ObjectAttributesBuilder; - use tss_esapi::interface_types::{algorithm::PublicAlgorithm, key_bits::RsaKeyBits}; - use tss_esapi::structures::{ - PublicBuilder, PublicKeyRsa, PublicRsaParametersBuilder, RsaScheme, - SymmetricDefinitionObject, - }; - - // Build RSA 2048 storage key template (standard SRK template) - let object_attributes = ObjectAttributesBuilder::new() - .with_fixed_tpm(true) - .with_fixed_parent(true) - .with_sensitive_data_origin(true) - .with_user_with_auth(true) - .with_decrypt(true) - .with_restricted(true) - .build() - .context("failed to build object attributes")?; - - let rsa_params = PublicRsaParametersBuilder::new() - .with_symmetric(SymmetricDefinitionObject::AES_128_CFB) - .with_scheme(RsaScheme::Null) - .with_key_bits(RsaKeyBits::Rsa2048) - .with_exponent(Default::default()) // Use default RSA exponent (65537) - .with_is_signing_key(false) - .with_is_decryption_key(true) - .with_restricted(true) - .build() - .context("failed to build RSA parameters")?; - - let public = PublicBuilder::new() - .with_public_algorithm(PublicAlgorithm::Rsa) - .with_name_hashing_algorithm(HashingAlgorithm::Sha256) - .with_object_attributes(object_attributes) - .with_rsa_parameters(rsa_params) - .with_rsa_unique_identifier(PublicKeyRsa::default()) - .build() - .context("failed to build public structure")?; - - // Create primary key in owner hierarchy - let primary_key = self - .context - .execute_with_nullauth_session(|ctx| { - ctx.create_primary( - Hierarchy::Owner, - public, - None, // auth_value - None, // sensitive_data - None, // outside_info - None, // creation_pcr - ) - }) - .context("failed to create primary key")?; - - debug!("created primary key in owner hierarchy"); - Ok(primary_key.key_handle) - } - - /// Make a key persistent at a given handle - pub fn evict_control( - &mut self, - transient_handle: tss_esapi::handles::KeyHandle, - persistent_handle: u32, - ) -> Result { - use tss_esapi::interface_types::resource_handles::Provision; - - let persistent = - PersistentTpmHandle::new(persistent_handle).context("invalid persistent handle")?; - - self.context - .execute_with_nullauth_session(|ctx| { - ctx.evict_control(Provision::Owner, transient_handle.into(), persistent.into()) - }) - .context("failed to make key persistent")?; - - debug!("made key persistent at 0x{persistent_handle:08x}"); - Ok(true) - } - - /// Ensure a persistent primary key exists at the given handle - pub fn ensure_primary_key(&mut self, handle: u32) -> Result { - if self.handle_exists(handle)? { - return Ok(true); - } - - debug!("creating TPM primary key at 0x{handle:08x}..."); - let transient = self.create_primary()?; - self.evict_control(transient, handle) - } - - // ==================== Seal/Unseal Operations ==================== - - /// Seal data to TPM with PCR policy - pub fn seal( - &mut self, - data: &[u8], - parent_handle: u32, - pcr_selection: &PcrSelection, - ) -> Result<(Vec, Vec)> { - use tss_esapi::attributes::ObjectAttributesBuilder; - use tss_esapi::interface_types::algorithm::PublicAlgorithm; - use tss_esapi::structures::{PublicBuilder, SensitiveData}; - - // Get parent key handle - let parent = PersistentTpmHandle::new(parent_handle).context("invalid parent handle")?; - let parent_key = self - .context - .tr_from_tpm_public(TpmHandle::Persistent(parent)) - .context("failed to get parent key handle")?; - - // Build PCR policy - let hash_alg = match pcr_selection.bank.as_str() { - "sha256" => HashingAlgorithm::Sha256, - "sha384" => HashingAlgorithm::Sha384, - "sha512" => HashingAlgorithm::Sha512, - _ => bail!( - "unsupported hash algorithm: {bank}", - bank = pcr_selection.bank - ), - }; - - // Build PCR selection list - let pcr_slots: Result> = pcr_selection - .pcrs - .iter() - .map(|&idx| { - let bit_mask = 1u32 << idx; - PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {idx}")) - }) - .collect(); - let pcr_slots = pcr_slots?; - - let pcr_selection_list = PcrSelectionListBuilder::new() - .with_selection(hash_alg, &pcr_slots) - .build() - .context("failed to build PCR selection list")?; - - // Create session for PCR policy - let session = self - .context - .start_auth_session( - None, - None, - None, - SessionType::Policy, - SymmetricDefinition::AES_128_CFB, - hash_alg, - ) - .context("failed to start policy session")? - .ok_or_else(|| anyhow::anyhow!("no session returned"))?; - - let policy_session = session.try_into()?; - - // Apply PCR policy - requires digest of current PCR values - let (_update_counter, _pcr_sel_out, digest_list) = self - .context - .pcr_read(pcr_selection_list.clone()) - .context("failed to read PCR values")?; - - // Calculate PCR digest - let pcr_digest = if let Some(digest) = digest_list.value().first() { - digest.clone() - } else { - bail!("no PCR digest found"); - }; - - self.context - .policy_pcr(policy_session, pcr_digest, pcr_selection_list.clone()) - .context("failed to set PCR policy")?; - - // Get policy digest - let policy_digest = self - .context - .policy_get_digest(policy_session) - .context("failed to get policy digest")?; - - // Flush session - let session_handle: SessionHandle = session.into(); - self.context - .flush_context(session_handle.into()) - .context("failed to flush policy session")?; - - // Build sealed data object attributes - let object_attributes = ObjectAttributesBuilder::new() - .with_fixed_tpm(true) - .with_fixed_parent(true) - .with_user_with_auth(false) - .with_admin_with_policy(true) - .build() - .context("failed to build object attributes")?; - - // Build public structure for sealed object - let public = PublicBuilder::new() - .with_public_algorithm(PublicAlgorithm::KeyedHash) - .with_name_hashing_algorithm(hash_alg) - .with_object_attributes(object_attributes) - .with_auth_policy(policy_digest) - .build() - .context("failed to build public structure")?; - - // Create sealed data (sensitive data) - let sensitive_data = - SensitiveData::try_from(data.to_vec()).context("failed to create sensitive data")?; - - // Create sealed object - let create_result = self - .context - .execute_with_nullauth_session(|ctx| { - ctx.create( - parent_key.into(), - public, - None, // auth_value - Some(sensitive_data), - None, // outside_info - None, // creation_pcr - ) - }) - .context("failed to seal data")?; - - debug!("sealed {} bytes to TPM with PCR policy", data.len()); - - // Marshal public and private parts - let pub_bytes = create_result - .out_public - .marshall() - .context("failed to marshal public part")?; - let priv_bytes = create_result.out_private.value().to_vec(); - - Ok((pub_bytes, priv_bytes)) - } - - /// Unseal data from TPM with PCR policy - pub fn unseal( - &mut self, - pub_bytes: &[u8], - priv_bytes: &[u8], - parent_handle: u32, - pcr_selection: &PcrSelection, - ) -> Result> { - use tss_esapi::structures::{Private, Public, SymmetricDefinition}; - - // Get parent key handle - let parent = PersistentTpmHandle::new(parent_handle).context("invalid parent handle")?; - let parent_key = self - .context - .tr_from_tpm_public(TpmHandle::Persistent(parent)) - .context("failed to get parent key handle")?; - - // Unmarshal public and private parts - let public = Public::unmarshall(pub_bytes).context("failed to unmarshal public part")?; - let private = - Private::try_from(priv_bytes.to_vec()).context("failed to create private structure")?; - - // Load sealed object - let sealed_handle = self - .context - .execute_with_nullauth_session(|ctx| ctx.load(parent_key.into(), private, public)) - .context("failed to load sealed object")?; - - // Build PCR policy for unsealing - let hash_alg = match pcr_selection.bank.as_str() { - "sha256" => HashingAlgorithm::Sha256, - "sha384" => HashingAlgorithm::Sha384, - "sha512" => HashingAlgorithm::Sha512, - _ => bail!( - "unsupported hash algorithm: {bank}", - bank = pcr_selection.bank - ), - }; - - let pcr_slots: Result> = pcr_selection - .pcrs - .iter() - .map(|&idx| { - let bit_mask = 1u32 << idx; - PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {idx}")) - }) - .collect(); - let pcr_slots = pcr_slots?; - - let pcr_selection_list = PcrSelectionListBuilder::new() - .with_selection(hash_alg, &pcr_slots) - .build() - .context("failed to build PCR selection list")?; - - // Create policy session - let session = self - .context - .start_auth_session( - None, - None, - None, - SessionType::Policy, - SymmetricDefinition::AES_128_CFB, - hash_alg, - ) - .context("failed to start policy session")? - .ok_or_else(|| anyhow::anyhow!("no session returned"))?; - - let policy_session = session.try_into()?; - - // Apply PCR policy - requires digest of current PCR values - let (_update_counter, _pcr_sel_out, digest_list) = self - .context - .pcr_read(pcr_selection_list.clone()) - .context("failed to read PCR values")?; - - let pcr_digest = if let Some(digest) = digest_list.value().first() { - digest.clone() - } else { - bail!("no PCR digest found"); - }; - - self.context - .policy_pcr(policy_session, pcr_digest, pcr_selection_list) - .context("failed to set PCR policy")?; - - // Unseal with policy session - let unsealed = self - .context - .execute_with_session(Some(session), |ctx| ctx.unseal(sealed_handle.into())) - .context("failed to unseal data (PCR values may have changed)")?; - - // Flush handles - self.context - .flush_context(sealed_handle.into()) - .context("failed to flush sealed object handle")?; - let session_handle: SessionHandle = session.into(); - self.context - .flush_context(session_handle.into()) - .context("failed to flush policy session")?; - - let data = unsealed.to_vec(); - debug!("unsealed {} bytes from TPM", data.len()); - - Ok(data) - } -} diff --git a/tpm-attest/src/gcp_ak.rs b/tpm-attest/src/gcp_ak.rs index d4bf76fae..56a703135 100644 --- a/tpm-attest/src/gcp_ak.rs +++ b/tpm-attest/src/gcp_ak.rs @@ -2,25 +2,18 @@ // // SPDX-License-Identifier: Apache-2.0 -//! GCP vTPM pre-provisioned AK loading using tss-esapi +//! GCP vTPM pre-provisioned AK loading //! //! This module provides native Rust implementation for loading GCP's -//! pre-provisioned Attestation Key using the TSS2 ESAPI. +//! pre-provisioned Attestation Key without C library dependencies. use std::str::FromStr; use anyhow::{Context as _, Result}; use tracing::debug; -use tss_esapi::{ - handles::{KeyHandle, NvIndexTpmHandle, TpmHandle}, - interface_types::resource_handles::{Hierarchy, NvAuth}, - structures::Public, - tcti_ldr::{DeviceConfig, TctiNameConf}, - traits::UnMarshall, - Context as TssContext, -}; -use crate::TpmEventLog; +use crate::{PcrSelection, PcrValue, TpmEventLog, TpmQuote}; +use tpm2::{tpm_rh, TpmAlgId, TpmContext, TpmlPcrSelection}; /// GCP vTPM NV indices for pre-provisioned AK pub mod gcp_nv_index { @@ -34,112 +27,85 @@ pub mod gcp_nv_index { pub const AK_ECC_TEMPLATE: u32 = 0x01C10003; } -/// Load GCP pre-provisioned ECC AK using tss-esapi +/// Loaded AK information +pub struct LoadedAk { + pub context: TpmContext, + pub handle: u32, + pub cert_nv_index: u32, +} + +/// Load GCP pre-provisioned ECC AK /// /// This function: /// 1. Reads the AK template from NV index 0x01C10003 /// 2. Creates a primary key under Endorsement hierarchy with the template /// 3. TPM deterministically recreates the same key pair (same template + same parent) -/// -/// # Parameters -/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) -/// -/// # Returns -/// - `Ok((TssContext, KeyHandle))` - TSS context and handle to the loaded AK -/// - `Err(_)` - Failed to load AK (not on GCP vTPM, or access error) -pub fn load_gcp_ak_ecc(tcti_path: Option<&str>) -> Result<(TssContext, KeyHandle)> { - debug!("loading GCP pre-provisioned ECC AK with tss-esapi..."); - - // Create TSS context - use std::str::FromStr; - let tcti_str = tcti_path.unwrap_or("/dev/tpmrm0"); - let device_path = tcti_str.trim_start_matches("device:"); - let device_config = - DeviceConfig::from_str(device_path).context("failed to parse device config")?; - let tcti = TctiNameConf::Device(device_config); - let mut context = TssContext::new(tcti).context("failed to create TSS context")?; +pub fn load_gcp_ak_ecc(tcti_path: Option<&str>) -> Result { + debug!("loading GCP pre-provisioned ECC AK..."); + + let mut context = TpmContext::new(tcti_path)?; // Read AK template from NV - let template_bytes = read_nv_data(&mut context, gcp_nv_index::AK_ECC_TEMPLATE) - .context("failed to read ECC AK template from NV 0x01C10003")?; + let template_bytes = context + .nv_read(gcp_nv_index::AK_ECC_TEMPLATE)? + .ok_or_else(|| anyhow::anyhow!("ECC AK template not found at NV 0x01C10003"))?; debug!( "read ECC AK template from NV: {} bytes", template_bytes.len() ); - // Parse template as TPM2B_PUBLIC - let public = Public::unmarshall(&template_bytes) - .context("failed to parse ECC AK template as TPM2B_PUBLIC")?; - - // Create primary key under Endorsement hierarchy with null auth session - let ak_handle = context - .execute_with_nullauth_session(|ctx| { - ctx.create_primary(Hierarchy::Endorsement, public, None, None, None, None) - }) - .context("failed to create primary ECC AK")? - .key_handle; + // Create primary key under Endorsement hierarchy + let (handle, _public) = + context.create_primary_from_template(tpm_rh::ENDORSEMENT, &template_bytes)?; - debug!("✓ successfully loaded GCP pre-provisioned ECC AK (handle: {ak_handle:?})"); + debug!( + "✓ successfully loaded GCP pre-provisioned ECC AK (handle: 0x{:08x})", + handle + ); - Ok((context, ak_handle)) + Ok(LoadedAk { + context, + handle, + cert_nv_index: gcp_nv_index::AK_ECC_CERT, + }) } -/// Load GCP pre-provisioned RSA AK using tss-esapi +/// Load GCP pre-provisioned RSA AK /// /// This function: /// 1. Reads the AK template from NV index 0x01C10001 /// 2. Creates a primary key under Endorsement hierarchy with the template /// 3. TPM deterministically recreates the same key pair (same template + same parent) -/// -/// # Parameters -/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) -/// -/// # Returns -/// - `Ok((TssContext, KeyHandle))` - TSS context and handle to the loaded AK -/// - `Err(_)` - Failed to load AK (not on GCP vTPM, or access error) -pub fn load_gcp_ak_rsa(tcti_path: Option<&str>) -> Result<(TssContext, KeyHandle)> { - debug!("loading GCP pre-provisioned RSA AK with tss-esapi..."); - - // Create TSS context - use std::str::FromStr; - // Strip "device:" prefix if present (from TpmContext tcti format) - let tcti_str = tcti_path.unwrap_or("/dev/tpmrm0"); - let device_path = tcti_str.trim_start_matches("device:"); - let device_config = - DeviceConfig::from_str(device_path).context("failed to parse device config")?; - let tcti = TctiNameConf::Device(device_config); - let mut context = TssContext::new(tcti).context("failed to create TSS context")?; +pub fn load_gcp_ak_rsa(tcti_path: Option<&str>) -> Result { + debug!("loading GCP pre-provisioned RSA AK..."); + + let mut context = TpmContext::new(tcti_path)?; // Read AK template from NV - let template_bytes = read_nv_data(&mut context, gcp_nv_index::AK_RSA_TEMPLATE) - .context("failed to read AK template from NV 0x01C10001")?; - - debug!("read AK template from NV: {} bytes", template_bytes.len()); - - // Parse template as TPM2B_PUBLIC - let public = Public::unmarshall(&template_bytes) - .context("failed to parse AK template as TPM2B_PUBLIC")?; - - // Create primary key under Endorsement hierarchy with null auth session - // This recreates the pre-provisioned AK because TPM CreatePrimary is deterministic - let ak_handle = context - .execute_with_nullauth_session(|ctx| { - ctx.create_primary( - Hierarchy::Endorsement, - public, - None, // auth_value - None, // sensitive_data - None, // outside_info - None, // creation_pcr - ) - }) - .context("failed to create primary AK")? - .key_handle; + let template_bytes = context + .nv_read(gcp_nv_index::AK_RSA_TEMPLATE)? + .ok_or_else(|| anyhow::anyhow!("RSA AK template not found at NV 0x01C10001"))?; - debug!("✓ successfully loaded GCP pre-provisioned AK (handle: {ak_handle:?})"); + debug!( + "read RSA AK template from NV: {} bytes", + template_bytes.len() + ); + + // Create primary key under Endorsement hierarchy + let (handle, _public) = + context.create_primary_from_template(tpm_rh::ENDORSEMENT, &template_bytes)?; - Ok((context, ak_handle)) + debug!( + "✓ successfully loaded GCP pre-provisioned RSA AK (handle: 0x{:08x})", + handle + ); + + Ok(LoadedAk { + context, + handle, + cert_nv_index: gcp_nv_index::AK_RSA_CERT, + }) } /// Key algorithm preference for quote generation @@ -156,7 +122,6 @@ pub enum KeyAlgorithm { impl FromStr for KeyAlgorithm { type Err = anyhow::Error; - /// Parse from string ("auto", "ecc", or "rsa") fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "auto" => Ok(KeyAlgorithm::Auto), @@ -168,27 +133,11 @@ impl FromStr for KeyAlgorithm { } /// Generate a TPM quote using GCP pre-provisioned AK (prefers ECC) -/// -/// This function: -/// 1. Loads the GCP pre-provisioned AK (tries ECC first, then falls back to RSA) -/// 2. Reads the specified PCR values -/// 3. Generates a quote signed by the AK -/// 4. Reads the AK certificate from NV -/// 5. Returns a complete TpmQuote structure -/// -/// # Parameters -/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) -/// - `qualifying_data`: Nonce/challenge data to include in quote -/// - `pcr_selection`: PCR registers to include in quote -/// -/// # Returns -/// - `Ok(TpmQuote)` - Complete quote with signature and certificate -/// - `Err(_)` - Failed to generate quote pub fn create_quote_with_gcp_ak( tcti_path: Option<&str>, qualifying_data: &[u8; 32], - pcr_selection: &crate::PcrSelection, -) -> Result { + pcr_selection: &PcrSelection, +) -> Result { create_quote_with_gcp_ak_algo( tcti_path, qualifying_data, @@ -198,165 +147,106 @@ pub fn create_quote_with_gcp_ak( } /// Generate a TPM quote using GCP pre-provisioned AK with manual algorithm selection -/// -/// This function allows specifying which key algorithm to use (ECC, RSA, or Auto). -/// -/// # Parameters -/// - `tcti_path`: Path to TPM device (e.g., "/dev/tpmrm0" or None for default) -/// - `qualifying_data`: Nonce/challenge data to include in quote -/// - `pcr_selection`: PCR registers to include in quote -/// - `key_algo`: Key algorithm preference (Auto, Ecc, or Rsa) -/// -/// # Returns -/// - `Ok(TpmQuote)` - Complete quote with signature and certificate -/// - `Err(_)` - Failed to generate quote pub fn create_quote_with_gcp_ak_algo( tcti_path: Option<&str>, qualifying_data: &[u8; 32], - pcr_selection: &crate::PcrSelection, + pcr_selection: &PcrSelection, key_algo: KeyAlgorithm, -) -> Result { - use tss_esapi::interface_types::algorithm::HashingAlgorithm; - use tss_esapi::structures::{Data, PcrSelectionListBuilder, PcrSlot, SignatureScheme}; - use tss_esapi::traits::Marshall; - +) -> Result { let platform = dstack_types::Platform::detect().context("Unsupported platform")?; debug!("generating TPM quote with GCP pre-provisioned AK..."); // Load GCP pre-provisioned AK based on algorithm preference - let (mut context, ak_handle, ak_cert_nv_index) = match key_algo { + let mut loaded_ak = match key_algo { KeyAlgorithm::Auto => { // Try ECC first (better performance), fallback to RSA match load_gcp_ak_ecc(tcti_path) { - Ok((ctx, handle)) => { + Ok(ak) => { debug!("✓ using ECC AK for quote"); - (ctx, handle, gcp_nv_index::AK_ECC_CERT) + ak } Err(e) => { debug!("ECC AK not available, falling back to RSA: {e}"); - let (ctx, handle) = load_gcp_ak_rsa(tcti_path)?; + let ak = load_gcp_ak_rsa(tcti_path)?; debug!("✓ using RSA AK for quote"); - (ctx, handle, gcp_nv_index::AK_RSA_CERT) + ak } } } KeyAlgorithm::Ecc => { - // Use ECC only - let (ctx, handle) = load_gcp_ak_ecc(tcti_path).context( + let ak = load_gcp_ak_ecc(tcti_path).context( "failed to load ECC AK (use --key-algo=rsa or --key-algo=auto for fallback)", )?; debug!("✓ using ECC AK for quote"); - (ctx, handle, gcp_nv_index::AK_ECC_CERT) + ak } KeyAlgorithm::Rsa => { - // Use RSA only - let (ctx, handle) = load_gcp_ak_rsa(tcti_path).context("failed to load RSA AK")?; + let ak = load_gcp_ak_rsa(tcti_path).context("failed to load RSA AK")?; debug!("✓ using RSA AK for quote"); - (ctx, handle, gcp_nv_index::AK_RSA_CERT) + ak } }; - // Build PCR selection list for tss-esapi - let pcr_selection_list = PcrSelectionListBuilder::new(); - - // Convert hash algorithm string to HashingAlgorithm enum + // Convert hash algorithm let hash_alg = match pcr_selection.bank.as_str() { - "sha256" => HashingAlgorithm::Sha256, - "sha384" => HashingAlgorithm::Sha384, - "sha512" => HashingAlgorithm::Sha512, + "sha256" => TpmAlgId::Sha256, + "sha384" => TpmAlgId::Sha384, + "sha512" => TpmAlgId::Sha512, _ => anyhow::bail!( "unsupported hash algorithm: {bank}", bank = pcr_selection.bank ), }; - // Build all PCR slots at once - // PcrSlot uses bit mask representation: PCR 0 = bit 0 (0x1), PCR 1 = bit 1 (0x2), etc. - let mut pcr_slots = Vec::new(); - for pcr_idx in &pcr_selection.pcrs { - let bit_mask = 1u32 << pcr_idx; - let pcr_slot = - PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {pcr_idx}"))?; - pcr_slots.push(pcr_slot); - } - - let pcr_selection_list = pcr_selection_list - .with_selection(hash_alg, &pcr_slots) - .build() - .context("failed to build PCR selection list")?; - - // Create qualifying data structure. - let qual_data = - Data::try_from(qualifying_data.to_vec()).context("failed to create qualifying data")?; - - // Use default signing scheme (RSASSA with SHA256) - let signing_scheme = SignatureScheme::Null; + // Build PCR selection + let tpm_pcr_selection = TpmlPcrSelection::single(hash_alg, &pcr_selection.pcrs); // Generate quote debug!("calling TPM Quote command..."); - let (attest, signature) = context - .execute_with_nullauth_session(|ctx| { - ctx.quote( - ak_handle, - qual_data, - signing_scheme, - pcr_selection_list.clone(), - ) - }) - .context("failed to generate quote")?; + let (message, signature) = + loaded_ak + .context + .quote(loaded_ak.handle, qualifying_data, &tpm_pcr_selection)?; debug!("✓ quote generated successfully"); - // Marshall attest structure to bytes (TPMS_ATTEST) - let message = attest.marshall().context("failed to marshall attest")?; - - // Marshall signature to bytes (TPMT_SIGNATURE) - let signature = signature - .marshall() - .context("failed to marshall signature")?; - - // Read PCR values - read each PCR individually to ensure correct mapping - let mut pcr_values = Vec::new(); - for pcr_idx in &pcr_selection.pcrs { - // Build selection for single PCR - let bit_mask = 1u32 << pcr_idx; - let pcr_slot = - PcrSlot::try_from(bit_mask).with_context(|| format!("invalid PCR index: {pcr_idx}"))?; - - let single_pcr_sel = PcrSelectionListBuilder::new() - .with_selection(hash_alg, &[pcr_slot]) - .build() - .context("failed to build single PCR selection")?; - - let (_update_counter, _pcr_sel_out, digest_list) = context - .execute_without_session(|ctx| ctx.pcr_read(single_pcr_sel)) - .context("failed to read PCR value")?; - - // Get the first (and only) digest - if let Some(digest) = digest_list.value().first() { - pcr_values.push(crate::PcrValue { - index: *pcr_idx, - algorithm: pcr_selection.bank.clone(), - value: digest.value().to_vec(), - }); - } - } - - // Read AK certificate from NV (ECC or RSA depending on which was loaded) - let ak_cert = read_nv_data(&mut context, ak_cert_nv_index) - .context("failed to read AK certificate from NV")?; + // Read PCR values + let pcr_values_raw = loaded_ak.context.pcr_read(&tpm_pcr_selection)?; + let pcr_values: Vec = pcr_values_raw + .into_iter() + .map(|(index, value)| PcrValue { + index, + algorithm: pcr_selection.bank.clone(), + value, + }) + .collect(); + + // Read AK certificate from NV + let ak_cert = loaded_ak + .context + .nv_read(loaded_ak.cert_nv_index)? + .ok_or_else(|| { + anyhow::anyhow!( + "AK certificate not found at NV 0x{:08x}", + loaded_ak.cert_nv_index + ) + })?; debug!( - "✓ AK certificate read from NV 0x{ak_cert_nv_index:08x}: {} bytes", + "✓ AK certificate read from NV 0x{:08x}: {} bytes", + loaded_ak.cert_nv_index, ak_cert.len() ); + // Flush the AK handle + let _ = loaded_ak.context.flush_context(loaded_ak.handle); + let event_log = TpmEventLog::from_kernel_file() .context("Failed to read TPM event log")? .events; - Ok(crate::TpmQuote { + Ok(TpmQuote { message, signature, pcr_values, @@ -365,27 +255,3 @@ pub fn create_quote_with_gcp_ak_algo( event_log, }) } - -/// Read data from TPM NV index -fn read_nv_data(context: &mut TssContext, nv_index: u32) -> Result> { - use tss_esapi::abstraction::nv; - - // Create NV index TPM handle - let nv_idx = NvIndexTpmHandle::new(nv_index).context("invalid NV index")?; - - // Get NV index handle from TPM - let nv_auth_handle = TpmHandle::NvIndex(nv_idx); - let nv_auth_handle = context - .execute_without_session(|ctx| { - ctx.tr_from_tpm_public(nv_auth_handle) - .map(|v| NvAuth::NvIndex(v.into())) - }) - .context("failed to get NV index handle")?; - - // Read NV data with null auth session - let data = context - .execute_with_nullauth_session(|ctx| nv::read_full(ctx, nv_auth_handle, nv_idx)) - .context("failed to read NV data")?; - - Ok(data.to_vec()) -} diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs index ba5dcc8d7..0b00a7582 100644 --- a/tpm-attest/src/lib.rs +++ b/tpm-attest/src/lib.rs @@ -6,7 +6,7 @@ //! //! This module provides functionality for generating TPM attestation quotes //! on the device side. It handles PCR operations, sealing, unsealing, NV storage, -//! and quote generation using the TPM2 Software Stack (tss-esapi). +//! and quote generation. //! //! This follows the same architecture as tdx-attest: device-side attestation only. //! For quote verification, see the tpm-qvl crate. @@ -18,8 +18,8 @@ use tracing::{debug, warn}; // Re-export tpm-types pub use tpm_types::{PcrSelection, PcrValue, TpmEvent, TpmEventLog, TpmQuote}; -mod esapi_impl; -use esapi_impl::EsapiContext; +mod esapi; +use esapi::EsapiContext; pub const PRIMARY_KEY_HANDLE: u32 = 0x81000100; pub const SEALED_NV_INDEX: u32 = 0x01801101; diff --git a/tpm2/Cargo.toml b/tpm2/Cargo.toml new file mode 100644 index 000000000..f51a07896 --- /dev/null +++ b/tpm2/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "tpm2" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "Pure Rust TPM 2.0 implementation" +keywords = ["tpm", "tpm2", "security", "attestation"] +categories = ["cryptography", "hardware-support"] + +[dependencies] +anyhow.workspace = true +sha2 = { workspace = true, features = ["oid"] } +tracing.workspace = true + +[dev-dependencies] +tempfile.workspace = true + +[[bin]] +name = "tpm2-test" +path = "src/bin/tpm2-test.rs" + +[dependencies.hex] +workspace = true +features = ["alloc"] diff --git a/tpm2/src/bin/tpm2-test.rs b/tpm2/src/bin/tpm2-test.rs new file mode 100644 index 000000000..94f69f821 --- /dev/null +++ b/tpm2/src/bin/tpm2-test.rs @@ -0,0 +1,833 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 Test CLI +//! +//! A simple CLI tool to test TPM 2.0 operations on real hardware. +//! +//! Usage: +//! tpm2-test [command] +//! +//! Commands: +//! info - Show TPM device info +//! random - Generate random bytes +//! pcr-read - Read PCR values +//! pcr-extend - Test PCR extend +//! nv-test - Test NV read/write operations +//! nv-full - Full NV test (define/write/read/undefine) +//! primary - Test primary key creation +//! evict - Test EvictControl (persistent key) +//! seal - Test seal/unseal operations (no PCR policy) +//! seal-pcr - Test seal/unseal operations with PCR policy +//! quote - Generate a TPM quote with RSA AK (requires GCP vTPM) +//! quote-ecc - Generate a TPM quote with ECC AK (requires GCP vTPM) +//! all - Run all tests + +use std::env; +use tpm2::{tpm_rh, TpmAlgId, TpmContext, TpmlPcrSelection, TpmtPublic}; + +fn main() { + let args: Vec = env::args().collect(); + let command = args.get(1).map(|s| s.as_str()).unwrap_or("all"); + + println!("=== TPM 2.0 Pure Rust Test Tool ===\n"); + + match command { + "info" => test_info(), + "random" => test_random(), + "pcr-read" => test_pcr_read(), + "pcr-extend" => test_pcr_extend(), + "nv-test" => test_nv_operations(), + "nv-full" => test_nv_full(), + "primary" => test_primary_key(), + "evict" => test_evict_control(), + "seal" => test_seal_unseal(), + "quote" => test_quote_rsa(), + "quote-ecc" => test_quote_ecc(), + "seal-pcr" => test_seal_unseal_with_pcr(), + "all" => { + test_info(); + test_random(); + test_pcr_read(); + test_primary_key(); + test_nv_operations(); + test_seal_unseal(); + test_seal_unseal_with_pcr(); + test_quote_rsa(); + test_quote_ecc(); + } + _ => { + eprintln!("Unknown command: {}", command); + eprintln!("Available commands: info, random, pcr-read, pcr-extend, nv-test, nv-full, primary, evict, seal, seal-pcr, quote, quote-ecc, all"); + std::process::exit(1); + } + } +} + +fn test_info() { + println!("--- Test: Device Info ---"); + + match TpmContext::new(None) { + Ok(ctx) => { + println!("✓ TPM device opened: {}", ctx.device_path()); + } + Err(e) => { + println!("✗ Failed to open TPM device: {}", e); + } + } + println!(); +} + +fn test_random() { + println!("--- Test: Random Number Generation ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Test getting 32 random bytes + match ctx.get_random(32) { + Ok(bytes) => { + println!("✓ Generated 32 random bytes:"); + println!(" {}", hex::encode(&bytes)); + } + Err(e) => { + println!("✗ GetRandom failed: {}", e); + } + } + + // Test getting 64 random bytes (tests chunking) + match ctx.get_random(64) { + Ok(bytes) => { + println!("✓ Generated 64 random bytes:"); + println!(" {}...", &hex::encode(&bytes)[..64]); + } + Err(e) => { + println!("✗ GetRandom (64 bytes) failed: {}", e); + } + } + println!(); +} + +fn test_pcr_read() { + println!("--- Test: PCR Read ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Read PCRs 0, 1, 2, 7 + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 1, 2, 7]); + + match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + println!("✓ Read {} PCR values:", values.len()); + for (idx, value) in values { + println!(" PCR[{}] = {}", idx, hex::encode(&value)); + } + } + Err(e) => { + println!("✗ PCR_Read failed: {}", e); + } + } + println!(); +} + +fn test_primary_key() { + println!("--- Test: Primary Key ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Check if a persistent handle exists + let test_handle: u32 = 0x81000100; + + match ctx.handle_exists(test_handle) { + Ok(exists) => { + println!(" Handle 0x{:08x} exists: {}", test_handle, exists); + } + Err(e) => { + println!("✗ ReadPublic failed: {}", e); + } + } + + // Try to create a transient primary key + println!(" Creating transient primary key under Owner hierarchy..."); + let template = tpm2::TpmtPublic::rsa_storage_key(); + match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok((handle, public)) => { + println!("✓ Created primary key:"); + println!(" Handle: 0x{:08x}", handle); + println!(" Public size: {} bytes", public.len()); + + // Flush the transient handle + if let Err(e) = ctx.flush_context(handle) { + println!(" Warning: Failed to flush handle: {}", e); + } else { + println!(" Flushed transient handle"); + } + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + } + } + println!(); +} + +fn test_nv_operations() { + println!("--- Test: NV Operations ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Test NV index (use a test index in the owner range) + let test_nv_index: u32 = 0x01800100; + + // Check if NV index exists + match ctx.nv_exists(test_nv_index) { + Ok(exists) => { + println!(" NV index 0x{:08x} exists: {}", test_nv_index, exists); + + if exists { + // Try to read it + match ctx.nv_read(test_nv_index) { + Ok(Some(data)) => { + println!("✓ Read {} bytes from NV", data.len()); + if data.len() <= 64 { + println!(" Data: {}", hex::encode(&data)); + } + } + Ok(None) => { + println!(" NV index exists but couldn't read (auth required?)"); + } + Err(e) => { + println!("✗ NV_Read failed: {}", e); + } + } + } + } + Err(e) => { + println!("✗ NV_ReadPublic failed: {}", e); + } + } + + // Try to read GCP AK certificate (if on GCP) + let gcp_ak_cert_index: u32 = 0x01C10000; + println!( + "\n Checking GCP AK certificate at 0x{:08x}...", + gcp_ak_cert_index + ); + + match ctx.nv_exists(gcp_ak_cert_index) { + Ok(true) => { + println!(" GCP AK certificate NV index exists!"); + match ctx.nv_read(gcp_ak_cert_index) { + Ok(Some(data)) => { + println!("✓ Read GCP AK certificate: {} bytes", data.len()); + } + Ok(None) => { + println!(" Couldn't read certificate data"); + } + Err(e) => { + println!("✗ Failed to read certificate: {}", e); + } + } + } + Ok(false) => { + println!(" GCP AK certificate not found (not on GCP vTPM?)"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_quote_rsa() { + println!("--- Test: Quote Generation with RSA AK (GCP vTPM) ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Check if GCP AK template exists + let gcp_ak_template_index: u32 = 0x01C10001; // RSA AK template + + match ctx.nv_exists(gcp_ak_template_index) { + Ok(true) => { + println!(" GCP AK template found, attempting to load AK..."); + + // Read template + match ctx.nv_read(gcp_ak_template_index) { + Ok(Some(template)) => { + println!(" Read AK template: {} bytes", template.len()); + + // Create primary with template + match ctx.create_primary_from_template(tpm_rh::ENDORSEMENT, &template) { + Ok((handle, _public)) => { + println!("✓ Loaded GCP AK: handle 0x{:08x}", handle); + + // Generate quote + let qualifying_data = [0u8; 32]; // Test nonce + let pcr_selection = + TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 2, 14]); + + match ctx.quote(handle, &qualifying_data, &pcr_selection) { + Ok((quoted, signature)) => { + println!("✓ Generated quote:"); + println!(" Quoted size: {} bytes", quoted.len()); + println!(" Signature size: {} bytes", signature.len()); + } + Err(e) => { + println!("✗ Quote failed: {}", e); + } + } + + // Flush handle + let _ = ctx.flush_context(handle); + } + Err(e) => { + println!("✗ Failed to load AK: {}", e); + } + } + } + Ok(None) => { + println!(" Couldn't read AK template"); + } + Err(e) => { + println!("✗ Failed to read template: {}", e); + } + } + } + Ok(false) => { + println!(" GCP AK template not found (not on GCP vTPM)"); + println!(" Skipping quote test"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_quote_ecc() { + println!("--- Test: Quote Generation with ECC AK (GCP vTPM) ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Check if GCP ECC AK template exists + let gcp_ak_template_index: u32 = 0x01C10003; // ECC AK template + + match ctx.nv_exists(gcp_ak_template_index) { + Ok(true) => { + println!(" GCP ECC AK template found, attempting to load AK..."); + + // Read template + match ctx.nv_read(gcp_ak_template_index) { + Ok(Some(template)) => { + println!(" Read ECC AK template: {} bytes", template.len()); + + // Create primary with template + match ctx.create_primary_from_template(tpm_rh::ENDORSEMENT, &template) { + Ok((handle, _public)) => { + println!("✓ Loaded GCP ECC AK: handle 0x{:08x}", handle); + + // Generate quote + let qualifying_data = [0u8; 32]; // Test nonce + let pcr_selection = + TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 2, 14]); + + match ctx.quote(handle, &qualifying_data, &pcr_selection) { + Ok((quoted, signature)) => { + println!("✓ Generated ECC quote:"); + println!(" Quoted size: {} bytes", quoted.len()); + println!(" Signature size: {} bytes", signature.len()); + } + Err(e) => { + println!("✗ Quote failed: {}", e); + } + } + + // Flush handle + let _ = ctx.flush_context(handle); + } + Err(e) => { + println!("✗ Failed to load ECC AK: {}", e); + } + } + } + Ok(None) => { + println!(" Couldn't read ECC AK template"); + } + Err(e) => { + println!("✗ Failed to read template: {}", e); + } + } + } + Ok(false) => { + println!(" GCP ECC AK template not found (not on GCP vTPM)"); + println!(" Skipping ECC quote test"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_pcr_extend() { + println!("--- Test: PCR Extend ---"); + println!(" Note: This test extends PCR 23 which is typically resettable"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // Read PCR 23 before extend + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[23]); + let before = match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + if let Some((_, value)) = values.first() { + println!(" PCR[23] before: {}", hex::encode(value)); + value.clone() + } else { + println!("✗ No PCR value returned"); + return; + } + } + Err(e) => { + println!("✗ PCR_Read failed: {}", e); + return; + } + }; + + // Extend PCR 23 with test data + let test_hash = [0x42u8; 32]; // Test hash value + match ctx.pcr_extend(23, &test_hash, TpmAlgId::Sha256) { + Ok(()) => { + println!("✓ PCR_Extend succeeded"); + } + Err(e) => { + println!("✗ PCR_Extend failed: {}", e); + return; + } + } + + // Read PCR 23 after extend + match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + if let Some((_, value)) = values.first() { + println!(" PCR[23] after: {}", hex::encode(value)); + if value != &before { + println!("✓ PCR value changed as expected"); + } else { + println!("✗ PCR value did not change!"); + } + } + } + Err(e) => { + println!("✗ PCR_Read after extend failed: {}", e); + } + } + println!(); +} + +fn test_nv_full() { + println!("--- Test: Full NV Operations (Define/Write/Read/Undefine) ---"); + println!(" Warning: This test creates and deletes NV index 0x01800200"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + let test_nv_index: u32 = 0x01800200; + let test_data = b"Hello TPM NV!"; + + // Clean up if index exists from previous failed test + if ctx.nv_exists(test_nv_index).unwrap_or(false) { + println!(" Cleaning up existing NV index..."); + let _ = ctx.nv_undefine(test_nv_index); + } + + // Define NV index + println!( + " Defining NV index 0x{:08x} with size {}...", + test_nv_index, + test_data.len() + ); + match ctx.nv_define(test_nv_index, test_data.len(), true) { + Ok(true) => { + println!("✓ NV_DefineSpace succeeded"); + } + Ok(false) => { + println!("✗ NV_DefineSpace returned false"); + return; + } + Err(e) => { + println!("✗ NV_DefineSpace failed: {}", e); + return; + } + } + + // Write to NV index + println!(" Writing {} bytes to NV...", test_data.len()); + match ctx.nv_write(test_nv_index, test_data) { + Ok(true) => { + println!("✓ NV_Write succeeded"); + } + Ok(false) => { + println!("✗ NV_Write returned false"); + } + Err(e) => { + println!("✗ NV_Write failed: {}", e); + } + } + + // Read from NV index + println!(" Reading from NV..."); + match ctx.nv_read(test_nv_index) { + Ok(Some(data)) => { + println!("✓ NV_Read succeeded: {} bytes", data.len()); + if data == test_data { + println!("✓ Data matches!"); + } else { + println!("✗ Data mismatch!"); + println!(" Expected: {:?}", String::from_utf8_lossy(test_data)); + println!(" Got: {:?}", String::from_utf8_lossy(&data)); + } + } + Ok(None) => { + println!("✗ NV_Read returned None"); + } + Err(e) => { + println!("✗ NV_Read failed: {}", e); + } + } + + // Undefine NV index + println!(" Undefining NV index..."); + match ctx.nv_undefine(test_nv_index) { + Ok(true) => { + println!("✓ NV_UndefineSpace succeeded"); + } + Ok(false) => { + println!("✗ NV_UndefineSpace returned false"); + } + Err(e) => { + println!("✗ NV_UndefineSpace failed: {}", e); + } + } + + // Verify it's gone + match ctx.nv_exists(test_nv_index) { + Ok(false) => { + println!("✓ NV index successfully removed"); + } + Ok(true) => { + println!("✗ NV index still exists after undefine!"); + } + Err(e) => { + println!("✗ NV check failed: {}", e); + } + } + println!(); +} + +fn test_evict_control() { + println!("--- Test: EvictControl (Persistent Key) ---"); + println!(" Warning: This test creates and removes persistent key at 0x81000200"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + let persistent_handle: u32 = 0x81000200; + + // Clean up if handle exists from previous failed test + if ctx.handle_exists(persistent_handle).unwrap_or(false) { + println!(" Cleaning up existing persistent handle..."); + // Need to evict it first - create a dummy transient and evict to remove + let _ = ctx.evict_control(persistent_handle, persistent_handle); + } + + // Create a transient primary key + println!(" Creating transient primary key..."); + let template = TpmtPublic::rsa_storage_key(); + let (transient_handle, _public) = match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok(result) => { + println!("✓ Created transient key: 0x{:08x}", result.0); + result + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + return; + } + }; + + // Make it persistent + println!(" Making key persistent at 0x{:08x}...", persistent_handle); + match ctx.evict_control(transient_handle, persistent_handle) { + Ok(true) => { + println!("✓ EvictControl succeeded - key is now persistent"); + } + Ok(false) => { + println!("✗ EvictControl returned false"); + let _ = ctx.flush_context(transient_handle); + return; + } + Err(e) => { + println!("✗ EvictControl failed: {}", e); + let _ = ctx.flush_context(transient_handle); + return; + } + } + + // Flush the transient handle (no longer needed) + let _ = ctx.flush_context(transient_handle); + + // Verify persistent handle exists + match ctx.handle_exists(persistent_handle) { + Ok(true) => { + println!("✓ Persistent handle exists"); + } + Ok(false) => { + println!("✗ Persistent handle not found!"); + return; + } + Err(e) => { + println!("✗ Handle check failed: {}", e); + return; + } + } + + // Remove the persistent key + println!(" Removing persistent key..."); + match ctx.evict_control(persistent_handle, persistent_handle) { + Ok(true) => { + println!("✓ Persistent key removed"); + } + Ok(false) => { + println!("✗ EvictControl (remove) returned false"); + } + Err(e) => { + println!("✗ EvictControl (remove) failed: {}", e); + } + } + + // Verify it's gone + match ctx.handle_exists(persistent_handle) { + Ok(false) => { + println!("✓ Persistent handle successfully removed"); + } + Ok(true) => { + println!("✗ Persistent handle still exists!"); + } + Err(_) => { + // Expected - handle doesn't exist + println!("✓ Persistent handle successfully removed"); + } + } + println!(); +} + +fn test_seal_unseal() { + println!("--- Test: Seal/Unseal Operations ---"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + // First, ensure we have a primary key + println!(" Creating primary storage key..."); + let template = TpmtPublic::rsa_storage_key(); + let (parent_handle, _) = match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok(result) => { + println!("✓ Created parent key: 0x{:08x}", result.0); + result + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + return; + } + }; + + // Data to seal + let secret_data = b"This is my secret data for TPM sealing test!"; + println!(" Sealing {} bytes of data...", secret_data.len()); + + // Seal the data without PCR policy (simpler test) + let empty_pcr_selection = TpmlPcrSelection::default(); + let (pub_blob, priv_blob) = match ctx.seal( + secret_data, + parent_handle, + &empty_pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(result) => { + println!("✓ Seal succeeded:"); + println!(" Public blob: {} bytes", result.0.len()); + println!(" Private blob: {} bytes", result.1.len()); + result + } + Err(e) => { + println!("✗ Seal failed: {}", e); + let _ = ctx.flush_context(parent_handle); + return; + } + }; + + // Unseal the data (use same empty PCR selection as seal) + println!(" Unsealing data..."); + match ctx.unseal( + &pub_blob, + &priv_blob, + parent_handle, + &empty_pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(unsealed) => { + println!("✓ Unseal succeeded: {} bytes", unsealed.len()); + if unsealed == secret_data { + println!("✓ Data matches original!"); + println!(" Content: {:?}", String::from_utf8_lossy(&unsealed)); + } else { + println!("✗ Data mismatch!"); + println!(" Expected: {:?}", String::from_utf8_lossy(secret_data)); + println!(" Got: {:?}", String::from_utf8_lossy(&unsealed)); + } + } + Err(e) => { + println!("✗ Unseal failed: {}", e); + } + } + + // Clean up + let _ = ctx.flush_context(parent_handle); + println!(); +} + +fn test_seal_unseal_with_pcr() { + println!("--- Test: Seal/Unseal Operations with PCR Policy ---"); + println!(" This seals data bound to PCR[0,7] (SHA256)"); + + let mut ctx = match TpmContext::new(None) { + Ok(ctx) => ctx, + Err(e) => { + println!("✗ Failed to open TPM: {}", e); + return; + } + }; + + println!(" Creating primary storage key..."); + let template = TpmtPublic::rsa_storage_key(); + let (parent_handle, _) = match ctx.create_primary(tpm_rh::OWNER, &template) { + Ok(result) => { + println!("✓ Created parent key: 0x{:08x}", result.0); + result + } + Err(e) => { + println!("✗ CreatePrimary failed: {}", e); + return; + } + }; + + let secret_data = b"PCR protected secret data!"; + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 7]); + println!( + " Sealing {} bytes of data with PCR policy...", + secret_data.len() + ); + + let (pub_blob, priv_blob) = + match ctx.seal(secret_data, parent_handle, &pcr_selection, TpmAlgId::Sha256) { + Ok(result) => { + println!("✓ Seal succeeded with PCR policy"); + result + } + Err(e) => { + println!("✗ Seal failed: {}", e); + let _ = ctx.flush_context(parent_handle); + return; + } + }; + + println!(" Reading PCR values for verification..."); + match ctx.pcr_read(&pcr_selection) { + Ok(values) => { + for (idx, value) in values { + println!(" PCR[{}] = {}", idx, hex::encode(value)); + } + } + Err(e) => println!(" Warning: failed to read PCRs: {}", e), + } + + println!(" Attempting to unseal data (PCRs must match)..."); + match ctx.unseal( + &pub_blob, + &priv_blob, + parent_handle, + &pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(unsealed) => { + println!("✓ Unseal succeeded: {} bytes", unsealed.len()); + if unsealed == secret_data { + println!("✓ Data matches original!"); + println!(" Content: {:?}", String::from_utf8_lossy(&unsealed)); + } else { + println!("✗ Data mismatch!"); + } + } + Err(e) => { + println!("✗ Unseal failed (PCR mismatch?): {}", e); + } + } + + let _ = ctx.flush_context(parent_handle); + println!(); +} diff --git a/tpm2/src/commands.rs b/tpm2/src/commands.rs new file mode 100644 index 000000000..4eb4b4e4b --- /dev/null +++ b/tpm2/src/commands.rs @@ -0,0 +1,648 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 command implementations +//! +//! This module provides high-level TPM operations. + +use anyhow::{Context, Result}; +use tracing::debug; + +use super::constants::*; +use super::device::*; +use super::marshal::*; +use super::session::*; +use super::types::*; + +/// Pure Rust TPM context +pub struct TpmContext { + device: TpmDevice, +} + +impl TpmContext { + /// Create a new TPM context with the given device path + pub fn new(tcti_path: Option<&str>) -> Result { + let device = match tcti_path { + Some(path) => TpmDevice::open(path)?, + None => TpmDevice::detect()?, + }; + + Ok(Self { device }) + } + + /// Get the device path + pub fn device_path(&self) -> &str { + self.device.path() + } + + // ==================== NV Operations ==================== + + /// Check if an NV index exists + pub fn nv_exists(&mut self, index: u32) -> Result { + let mut cmd = TpmCommand::new(TpmCc::NvReadPublic); + cmd.add_handle(index); + + let response = self.device.execute(&cmd.finalize())?; + + // If successful, the NV index exists + Ok(response.is_success()) + } + + /// Read NV public area to get size + pub fn nv_read_public(&mut self, index: u32) -> Result { + let mut cmd = TpmCommand::new(TpmCc::NvReadPublic); + cmd.add_handle(index); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("NV_ReadPublic failed")?; + + let mut buf = response.data_buffer(); + let nv_public = Tpm2bNvPublic::unmarshal(&mut buf)?; + + Ok(nv_public.nv_public) + } + + /// Read data from an NV index + pub fn nv_read(&mut self, index: u32) -> Result>> { + // First get the NV public to know the size + let nv_public = match self.nv_read_public(index) { + Ok(p) => p, + Err(_) => return Ok(None), // NV index doesn't exist + }; + + let total_size = nv_public.data_size as usize; + let mut result = Vec::with_capacity(total_size); + let mut offset = 0u16; + + // Read in chunks (max ~1024 bytes per read) + const MAX_READ_SIZE: u16 = 1024; + + while (offset as usize) < total_size { + let remaining = total_size - offset as usize; + let read_size = (remaining as u16).min(MAX_READ_SIZE); + + let mut cmd = TpmCommand::with_sessions(TpmCc::NvRead); + // authHandle (owner for owner-readable NV) + cmd.add_handle(tpm_rh::OWNER); + // nvIndex + cmd.add_handle(index); + // Authorization area (null auth) + cmd.add_null_auth_area(); + // size + cmd.add_u16(read_size); + // offset + cmd.add_u16(offset); + + let response = self.device.execute(&cmd.finalize())?; + if !response.is_success() { + // Try with NV index as auth handle instead + let mut cmd = TpmCommand::with_sessions(TpmCc::NvRead); + cmd.add_handle(index); + cmd.add_handle(index); + cmd.add_null_auth_area(); + cmd.add_u16(read_size); + cmd.add_u16(offset); + + let response = self.device.execute(&cmd.finalize())?; + if !response.is_success() { + return Ok(None); + } + + let mut buf = response.skip_parameter_size()?; + let data = buf.get_tpm2b()?; + result.extend_from_slice(&data); + } else { + let mut buf = response.skip_parameter_size()?; + let data = buf.get_tpm2b()?; + result.extend_from_slice(&data); + } + + offset += read_size; + } + + Ok(Some(result)) + } + + /// Write data to an NV index + pub fn nv_write(&mut self, index: u32, data: &[u8]) -> Result { + const MAX_WRITE_SIZE: usize = 1024; + let mut offset = 0u16; + + while (offset as usize) < data.len() { + let remaining = data.len() - offset as usize; + let write_size = remaining.min(MAX_WRITE_SIZE); + let chunk = &data[offset as usize..offset as usize + write_size]; + + let mut cmd = TpmCommand::with_sessions(TpmCc::NvWrite); + // authHandle + cmd.add_handle(tpm_rh::OWNER); + // nvIndex + cmd.add_handle(index); + // Authorization area + cmd.add_null_auth_area(); + // data + cmd.add_tpm2b(chunk); + // offset + cmd.add_u16(offset); + + let response = self.device.execute(&cmd.finalize())?; + response + .ensure_success() + .with_context(|| format!("NV_Write failed at offset {}", offset))?; + + offset += write_size as u16; + } + + debug!("wrote {} bytes to NV index 0x{:08x}", data.len(), index); + Ok(true) + } + + /// Define a new NV index + pub fn nv_define(&mut self, index: u32, size: usize, owner_read_write: bool) -> Result { + let mut attributes = TpmaNv::new(); + if owner_read_write { + attributes = attributes.with_owner_write().with_owner_read(); + } + + let nv_public = TpmsNvPublic::new(index, size as u16, attributes); + + let mut cmd = TpmCommand::with_sessions(TpmCc::NvDefineSpace); + // authHandle (owner) + cmd.add_handle(tpm_rh::OWNER); + // Authorization area + cmd.add_null_auth_area(); + // auth (empty) + cmd.add_tpm2b_empty(); + // publicInfo + cmd.add(&Tpm2bNvPublic { nv_public }); + + let response = self.device.execute(&cmd.finalize())?; + + if response.is_success() { + debug!("defined NV index 0x{:08x} with size {}", index, size); + Ok(true) + } else { + Ok(false) + } + } + + /// Undefine (delete) an NV index + pub fn nv_undefine(&mut self, index: u32) -> Result { + let mut cmd = TpmCommand::with_sessions(TpmCc::NvUndefineSpace); + // authHandle (owner) + cmd.add_handle(tpm_rh::OWNER); + // nvIndex + cmd.add_handle(index); + // Authorization area + cmd.add_null_auth_area(); + + let response = self.device.execute(&cmd.finalize())?; + + if response.is_success() { + debug!("undefined NV index 0x{:08x}", index); + Ok(true) + } else { + Ok(false) + } + } + + // ==================== PCR Operations ==================== + + /// Read PCR values for the given selection + pub fn pcr_read(&mut self, pcr_selection: &TpmlPcrSelection) -> Result)>> { + let mut cmd = TpmCommand::new(TpmCc::PcrRead); + cmd.add(pcr_selection); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("PCR_Read failed")?; + + let mut buf = response.data_buffer(); + let _update_counter = buf.get_u32()?; + let pcr_selection_out = TpmlPcrSelection::unmarshal(&mut buf)?; + let digest_list = TpmlDigest::unmarshal(&mut buf)?; + + // Map digests to PCR indices + let mut result = Vec::new(); + let mut digest_idx = 0; + + for sel in &pcr_selection_out.pcr_selections { + for (byte_idx, &byte) in sel.pcr_select.iter().enumerate() { + for bit in 0..8 { + if byte & (1 << bit) != 0 { + let pcr_idx = (byte_idx * 8 + bit) as u32; + if digest_idx < digest_list.digests.len() { + result.push((pcr_idx, digest_list.digests[digest_idx].buffer.clone())); + digest_idx += 1; + } + } + } + } + } + + Ok(result) + } + + /// Read a single PCR value + pub fn pcr_read_single(&mut self, pcr_idx: u32, hash_alg: TpmAlgId) -> Result> { + let selection = TpmlPcrSelection::single(hash_alg, &[pcr_idx]); + let values = self.pcr_read(&selection)?; + + values + .into_iter() + .find(|(idx, _)| *idx == pcr_idx) + .map(|(_, v)| v) + .ok_or_else(|| anyhow::anyhow!("PCR {} not found in response", pcr_idx)) + } + + /// Extend a PCR with a hash value + pub fn pcr_extend(&mut self, pcr: u32, hash: &[u8], hash_alg: TpmAlgId) -> Result<()> { + let digest_values = TpmlDigestValues::single(TpmtHa { + hash_alg, + digest: hash.to_vec(), + }); + + let mut cmd = TpmCommand::with_sessions(TpmCc::PcrExtend); + // pcrHandle + cmd.add_handle(pcr); + // Authorization area + cmd.add_null_auth_area(); + // digests + cmd.add(&digest_values); + + let response = self.device.execute(&cmd.finalize())?; + response + .ensure_success() + .with_context(|| format!("PCR_Extend failed for PCR {}", pcr))?; + + debug!("extended PCR {}", pcr); + Ok(()) + } + + // ==================== Random Number Generation ==================== + + /// Generate random bytes using the TPM's hardware RNG + pub fn get_random(&mut self, num_bytes: usize) -> Result> { + let mut result = Vec::with_capacity(num_bytes); + + // TPM may return fewer bytes than requested, so loop + while result.len() < num_bytes { + let remaining = num_bytes - result.len(); + let request_size = remaining.min(48) as u16; // TPM typically limits to 48-64 bytes + + let mut cmd = TpmCommand::new(TpmCc::GetRandom); + cmd.add_u16(request_size); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("GetRandom failed")?; + + let mut buf = response.data_buffer(); + let random_bytes = buf.get_tpm2b()?; + result.extend_from_slice(&random_bytes); + } + + result.truncate(num_bytes); + Ok(result) + } + + /// Generate random bytes into a fixed-size array + pub fn get_random_array(&mut self) -> Result<[u8; N]> { + let bytes = self.get_random(N)?; + bytes + .try_into() + .map_err(|_| anyhow::anyhow!("unexpected random bytes length")) + } + + // ==================== Primary Key Operations ==================== + + /// Check if a persistent handle exists + pub fn handle_exists(&mut self, handle: u32) -> Result { + let mut cmd = TpmCommand::new(TpmCc::ReadPublic); + cmd.add_handle(handle); + + let response = self.device.execute(&cmd.finalize())?; + Ok(response.is_success()) + } + + /// Create a primary key in the specified hierarchy + pub fn create_primary( + &mut self, + hierarchy: u32, + template: &TpmtPublic, + ) -> Result<(u32, Vec)> { + let public = Tpm2bPublic::from_template(template); + + let mut cmd = TpmCommand::with_sessions(TpmCc::CreatePrimary); + // primaryHandle (hierarchy) + cmd.add_handle(hierarchy); + // Authorization area + cmd.add_null_auth_area(); + // inSensitive (empty) + cmd.add(&Tpm2bSensitiveCreate::empty()); + // inPublic + cmd.add(&public); + // outsideInfo (empty) + cmd.add_tpm2b_empty(); + // creationPCR (empty) + cmd.add(&TpmlPcrSelection::default()); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("CreatePrimary failed")?; + + // For commands with sessions, the response format is: + // - Handle (4 bytes) - BEFORE parameter size + // - Parameter size (4 bytes) + // - Parameters... + let mut buf = response.data_buffer(); + let handle = buf.get_u32()?; + + // Skip parameter size + let _param_size = buf.get_u32()?; + + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + debug!("created primary key with handle 0x{:08x}", handle); + Ok((handle, out_public.public_area)) + } + + /// Create a primary key from raw public template bytes (for GCP AK) + pub fn create_primary_from_template( + &mut self, + hierarchy: u32, + template_bytes: &[u8], + ) -> Result<(u32, Vec)> { + let mut cmd = TpmCommand::with_sessions(TpmCc::CreatePrimary); + // primaryHandle (hierarchy) + cmd.add_handle(hierarchy); + // Authorization area + cmd.add_null_auth_area(); + // inSensitive (empty) + cmd.add(&Tpm2bSensitiveCreate::empty()); + // inPublic (raw template with size prefix) + cmd.add_tpm2b(template_bytes); + // outsideInfo (empty) + cmd.add_tpm2b_empty(); + // creationPCR (empty) + cmd.add(&TpmlPcrSelection::default()); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("CreatePrimary failed")?; + + // For commands with sessions, the response format is: + // - Handle (4 bytes) - BEFORE parameter size + // - Parameter size (4 bytes) + // - Parameters... + let mut buf = response.data_buffer(); + let handle = buf.get_u32()?; + + // Skip parameter size + let _param_size = buf.get_u32()?; + + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + debug!("created primary key with handle 0x{:08x}", handle); + Ok((handle, out_public.public_area)) + } + + /// Make a key persistent at a given handle + pub fn evict_control(&mut self, object_handle: u32, persistent_handle: u32) -> Result { + let mut cmd = TpmCommand::with_sessions(TpmCc::EvictControl); + // auth (owner) + cmd.add_handle(tpm_rh::OWNER); + // objectHandle + cmd.add_handle(object_handle); + // Authorization area + cmd.add_null_auth_area(); + // persistentHandle + cmd.add_handle(persistent_handle); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("EvictControl failed")?; + + debug!("made key persistent at 0x{:08x}", persistent_handle); + Ok(true) + } + + /// Ensure a persistent primary key exists at the given handle + pub fn ensure_primary_key(&mut self, handle: u32) -> Result { + if self.handle_exists(handle)? { + return Ok(true); + } + + debug!("creating TPM primary key at 0x{:08x}...", handle); + let template = TpmtPublic::rsa_storage_key(); + let (transient, _) = self.create_primary(tpm_rh::OWNER, &template)?; + self.evict_control(transient, handle)?; + + // Flush the transient handle + self.flush_context(transient)?; + + Ok(true) + } + + /// Flush a context (handle) + pub fn flush_context(&mut self, handle: u32) -> Result<()> { + let mut cmd = TpmCommand::new(TpmCc::FlushContext); + cmd.add_handle(handle); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("FlushContext failed")?; + + Ok(()) + } + + // ==================== Seal/Unseal Operations ==================== + + /// Seal data to TPM with PCR policy + pub fn seal( + &mut self, + data: &[u8], + parent_handle: u32, + pcr_selection: &TpmlPcrSelection, + hash_alg: TpmAlgId, + ) -> Result<(Vec, Vec)> { + // Compute policy digest if PCR selection is not empty + let policy_digest = if pcr_selection.pcr_selections.is_empty() { + // No PCR policy - use empty authPolicy (zero length, not zero-filled) + vec![] + } else { + // First, compute the policy digest using a trial session + let trial_session = AuthSession::start_trial(&mut self.device, hash_alg)?; + + // Compute PCR digest + let pcr_digest = compute_pcr_digest(&mut self.device, pcr_selection, hash_alg)?; + + // Apply PCR policy to trial session + trial_session.policy_pcr(&mut self.device, &pcr_digest, pcr_selection)?; + + // Get the policy digest + let digest = trial_session.get_digest(&mut self.device)?; + + // Flush trial session + trial_session.flush(&mut self.device)?; + + digest + }; + + // Create sealed object template + let template = TpmtPublic::sealed_object(Tpm2bDigest::new(policy_digest)); + let public = Tpm2bPublic::from_template(&template); + + // Create the sealed object + let mut cmd = TpmCommand::with_sessions(TpmCc::Create); + // parentHandle + cmd.add_handle(parent_handle); + // Authorization area + cmd.add_null_auth_area(); + // inSensitive (contains the data to seal) + cmd.add(&Tpm2bSensitiveCreate::with_data(data.to_vec())); + // inPublic + cmd.add(&public); + // outsideInfo (empty) + cmd.add_tpm2b_empty(); + // creationPCR (empty) + cmd.add(&TpmlPcrSelection::default()); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("Create (seal) failed")?; + + let mut buf = response.skip_parameter_size()?; + let out_private = Tpm2bPrivate::unmarshal(&mut buf)?; + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + debug!("sealed {} bytes to TPM with PCR policy", data.len()); + + Ok((out_public.public_area, out_private.buffer)) + } + + /// Unseal data from TPM with PCR policy + pub fn unseal( + &mut self, + pub_bytes: &[u8], + priv_bytes: &[u8], + parent_handle: u32, + pcr_selection: &TpmlPcrSelection, + hash_alg: TpmAlgId, + ) -> Result> { + // Load the sealed object + let mut cmd = TpmCommand::with_sessions(TpmCc::Load); + // parentHandle + cmd.add_handle(parent_handle); + // Authorization area + cmd.add_null_auth_area(); + // inPrivate + cmd.add_tpm2b(priv_bytes); + // inPublic + cmd.add_tpm2b(pub_bytes); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("Load failed")?; + + // For commands with sessions, handle comes BEFORE parameter size + let mut buf = response.data_buffer(); + let object_handle = buf.get_u32()?; + let _param_size = buf.get_u32()?; // Skip parameter size + + debug!("loaded sealed object with handle 0x{:08x}", object_handle); + + // Unseal - use policy session if PCR selection is not empty + let response = if pcr_selection.pcr_selections.is_empty() { + // No PCR policy - use null auth + let mut cmd = TpmCommand::with_sessions(TpmCc::Unseal); + cmd.add_handle(object_handle); + cmd.add_null_auth_area(); + self.device.execute(&cmd.finalize())? + } else { + // Start a policy session + let policy_session = AuthSession::start_policy(&mut self.device, hash_alg)?; + + // Compute and apply PCR policy + let pcr_digest = compute_pcr_digest(&mut self.device, pcr_selection, hash_alg)?; + policy_session.policy_pcr(&mut self.device, &pcr_digest, pcr_selection)?; + + // Unseal with policy session + let mut cmd = TpmCommand::with_sessions(TpmCc::Unseal); + cmd.add_handle(object_handle); + cmd.add_policy_auth(policy_session.handle); + + let response = self.device.execute(&cmd.finalize())?; + let _ = policy_session.flush(&mut self.device); + response + }; + + // Clean up object handle + let _ = self.flush_context(object_handle); + + if !response.is_success() { + anyhow::bail!( + "Unseal failed with TPM error: 0x{:08x}", + response.response_code + ); + } + + let mut buf = response.skip_parameter_size()?; + let data = buf.get_tpm2b()?; + + debug!("unsealed {} bytes from TPM", data.len()); + Ok(data) + } + + // ==================== Quote Operations ==================== + + /// Generate a TPM quote + pub fn quote( + &mut self, + sign_handle: u32, + qualifying_data: &[u8], + pcr_selection: &TpmlPcrSelection, + ) -> Result<(Vec, Vec)> { + let mut cmd = TpmCommand::with_sessions(TpmCc::Quote); + // signHandle + cmd.add_handle(sign_handle); + // Authorization area + cmd.add_null_auth_area(); + // qualifyingData + cmd.add_tpm2b(qualifying_data); + // inScheme (NULL - use key's default scheme) + cmd.add(&TpmtSigScheme::null()); + // PCRselect + cmd.add(pcr_selection); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("Quote failed")?; + + let mut buf = response.skip_parameter_size()?; + let quoted = buf.get_tpm2b()?; // TPM2B_ATTEST + let signature = buf.get_remaining(); // TPMT_SIGNATURE + + debug!("generated TPM quote"); + Ok((quoted, signature)) + } + + /// Read public area of a key + pub fn read_public(&mut self, handle: u32) -> Result> { + let mut cmd = TpmCommand::new(TpmCc::ReadPublic); + cmd.add_handle(handle); + + let response = self.device.execute(&cmd.finalize())?; + response.ensure_success().context("ReadPublic failed")?; + + let mut buf = response.data_buffer(); + let out_public = Tpm2bPublic::unmarshal(&mut buf)?; + + Ok(out_public.public_area) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pcr_selection() { + let sel = TpmsPcrSelection::sha256(&[0, 1, 2, 7]); + assert_eq!(sel.hash, TpmAlgId::Sha256); + // PCR 0, 1, 2, 7 = bits 0, 1, 2, 7 = 0b10000111 = 0x87 + assert_eq!(sel.pcr_select[0], 0x87); + } +} diff --git a/tpm2/src/constants.rs b/tpm2/src/constants.rs new file mode 100644 index 000000000..c562fc019 --- /dev/null +++ b/tpm2/src/constants.rs @@ -0,0 +1,380 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 constants and command codes + +/// TPM 2.0 Command Codes (TPM_CC) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum TpmCc { + NvDefineSpace = 0x0000012A, + NvUndefineSpace = 0x00000122, + NvRead = 0x0000014E, + NvWrite = 0x00000137, + NvReadPublic = 0x00000169, + PcrRead = 0x0000017E, + PcrExtend = 0x00000182, + GetRandom = 0x0000017B, + CreatePrimary = 0x00000131, + Create = 0x00000153, + Load = 0x00000157, + Unseal = 0x0000015E, + Quote = 0x00000158, + StartAuthSession = 0x00000176, + PolicyPcr = 0x0000017F, + PolicyGetDigest = 0x00000189, + FlushContext = 0x00000165, + EvictControl = 0x00000120, + ReadPublic = 0x00000173, + GetCapability = 0x0000017A, +} + +impl TpmCc { + pub fn to_u32(self) -> u32 { + self as u32 + } +} + +/// TPM 2.0 Response Codes (TPM_RC) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum TpmRc { + Success = 0x00000000, + // Format 0 errors + Initialize = 0x00000100, + Failure = 0x00000101, + // Format 1 errors (parameter errors) + Value = 0x00000184, + Handle = 0x0000008B, + // NV errors + NvDefined = 0x0000014C, + NvNotDefined = 0x0000014B, + NvLocked = 0x00000148, + NvRange = 0x00000146, + // Auth errors + AuthFail = 0x0000098E, + PolicyFail = 0x0000099D, + // PCR errors + Locality = 0x00000107, +} + +impl TpmRc { + pub fn from_u32(code: u32) -> Self { + match code { + 0x00000000 => TpmRc::Success, + 0x00000100 => TpmRc::Initialize, + 0x00000101 => TpmRc::Failure, + 0x00000184 => TpmRc::Value, + 0x0000008B => TpmRc::Handle, + 0x0000014C => TpmRc::NvDefined, + 0x0000014B => TpmRc::NvNotDefined, + 0x00000148 => TpmRc::NvLocked, + 0x00000146 => TpmRc::NvRange, + 0x0000098E => TpmRc::AuthFail, + 0x0000099D => TpmRc::PolicyFail, + 0x00000107 => TpmRc::Locality, + _ => TpmRc::Failure, // Unknown error + } + } + + pub fn is_success(self) -> bool { + matches!(self, TpmRc::Success) + } +} + +/// TPM 2.0 Algorithm IDs (TPM_ALG_ID) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmAlgId { + Null = 0x0010, + Sha1 = 0x0004, + Sha256 = 0x000B, + Sha384 = 0x000C, + Sha512 = 0x000D, + Rsa = 0x0001, + Ecc = 0x0023, + Aes = 0x0006, + Cfb = 0x0043, + RsaSsa = 0x0014, + RsaPss = 0x0016, + EcDsa = 0x0018, + KeyedHash = 0x0008, + SymCipher = 0x0025, +} + +impl TpmAlgId { + pub fn to_u16(self) -> u16 { + self as u16 + } + + pub fn from_u16(v: u16) -> Option { + match v { + 0x0010 => Some(TpmAlgId::Null), + 0x0004 => Some(TpmAlgId::Sha1), + 0x000B => Some(TpmAlgId::Sha256), + 0x000C => Some(TpmAlgId::Sha384), + 0x000D => Some(TpmAlgId::Sha512), + 0x0001 => Some(TpmAlgId::Rsa), + 0x0023 => Some(TpmAlgId::Ecc), + 0x0006 => Some(TpmAlgId::Aes), + 0x0043 => Some(TpmAlgId::Cfb), + 0x0014 => Some(TpmAlgId::RsaSsa), + 0x0016 => Some(TpmAlgId::RsaPss), + 0x0018 => Some(TpmAlgId::EcDsa), + 0x0008 => Some(TpmAlgId::KeyedHash), + 0x0025 => Some(TpmAlgId::SymCipher), + _ => None, + } + } + + pub fn digest_size(self) -> usize { + match self { + TpmAlgId::Sha1 => 20, + TpmAlgId::Sha256 => 32, + TpmAlgId::Sha384 => 48, + TpmAlgId::Sha512 => 64, + _ => 0, + } + } +} + +/// TPM 2.0 Handle Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum TpmHt { + Pcr = 0x00, + NvIndex = 0x01, + HmacSession = 0x02, + PolicySession = 0x03, + Permanent = 0x40, + Transient = 0x80, + Persistent = 0x81, +} + +/// TPM 2.0 Permanent Handles +pub mod tpm_rh { + pub const OWNER: u32 = 0x40000001; + pub const NULL: u32 = 0x40000007; + pub const ENDORSEMENT: u32 = 0x4000000B; + pub const PLATFORM: u32 = 0x4000000C; + pub const PW: u32 = 0x40000009; // Password authorization +} + +/// TPM 2.0 Session Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum TpmSe { + Hmac = 0x00, + Policy = 0x01, + Trial = 0x03, +} + +/// TPM 2.0 Startup Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmSu { + Clear = 0x0000, + State = 0x0001, +} + +/// TPM 2.0 Capability Types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum TpmCap { + Handles = 0x00000001, + Commands = 0x00000002, + PpCommands = 0x00000003, + AuditCommands = 0x00000004, + Pcrs = 0x00000005, + TpmProperties = 0x00000006, + PcrProperties = 0x00000007, + EccCurves = 0x00000008, + AuthPolicies = 0x00000009, +} + +/// TPM 2.0 Object Attributes +#[derive(Debug, Clone, Copy, Default)] +pub struct TpmaObject(pub u32); + +impl TpmaObject { + pub const FIXED_TPM: u32 = 1 << 1; + pub const ST_CLEAR: u32 = 1 << 2; + pub const FIXED_PARENT: u32 = 1 << 4; + pub const SENSITIVE_DATA_ORIGIN: u32 = 1 << 5; + pub const USER_WITH_AUTH: u32 = 1 << 6; + pub const ADMIN_WITH_POLICY: u32 = 1 << 7; + pub const NO_DA: u32 = 1 << 10; + pub const ENCRYPTED_DUPLICATION: u32 = 1 << 11; + pub const RESTRICTED: u32 = 1 << 16; + pub const DECRYPT: u32 = 1 << 17; + pub const SIGN_ENCRYPT: u32 = 1 << 18; + + pub fn new() -> Self { + Self(0) + } + + pub fn with_fixed_tpm(mut self) -> Self { + self.0 |= Self::FIXED_TPM; + self + } + + pub fn with_fixed_parent(mut self) -> Self { + self.0 |= Self::FIXED_PARENT; + self + } + + pub fn with_sensitive_data_origin(mut self) -> Self { + self.0 |= Self::SENSITIVE_DATA_ORIGIN; + self + } + + pub fn with_user_with_auth(mut self) -> Self { + self.0 |= Self::USER_WITH_AUTH; + self + } + + pub fn with_admin_with_policy(mut self) -> Self { + self.0 |= Self::ADMIN_WITH_POLICY; + self + } + + pub fn with_restricted(mut self) -> Self { + self.0 |= Self::RESTRICTED; + self + } + + pub fn with_decrypt(mut self) -> Self { + self.0 |= Self::DECRYPT; + self + } + + pub fn with_sign_encrypt(mut self) -> Self { + self.0 |= Self::SIGN_ENCRYPT; + self + } +} + +/// TPM 2.0 NV Attributes +#[derive(Debug, Clone, Copy, Default)] +pub struct TpmaNv(pub u32); + +impl TpmaNv { + pub const PP_WRITE: u32 = 1 << 0; + pub const OWNER_WRITE: u32 = 1 << 1; + pub const AUTH_WRITE: u32 = 1 << 2; + pub const POLICY_WRITE: u32 = 1 << 3; + pub const PP_READ: u32 = 1 << 16; + pub const OWNER_READ: u32 = 1 << 17; + pub const AUTH_READ: u32 = 1 << 18; + pub const POLICY_READ: u32 = 1 << 19; + pub const NO_DA: u32 = 1 << 25; + pub const ORDERLY: u32 = 1 << 26; + pub const CLEAR_STCLEAR: u32 = 1 << 27; + pub const READ_LOCKED: u32 = 1 << 28; + pub const WRITTEN: u32 = 1 << 29; + pub const PLATFORM_CREATE: u32 = 1 << 30; + pub const READ_STCLEAR: u32 = 1 << 31; + + pub fn new() -> Self { + Self(0) + } + + pub fn with_owner_write(mut self) -> Self { + self.0 |= Self::OWNER_WRITE; + self + } + + pub fn with_owner_read(mut self) -> Self { + self.0 |= Self::OWNER_READ; + self + } + + pub fn with_auth_write(mut self) -> Self { + self.0 |= Self::AUTH_WRITE; + self + } + + pub fn with_auth_read(mut self) -> Self { + self.0 |= Self::AUTH_READ; + self + } +} + +/// TPM 2.0 Session Attributes +#[derive(Debug, Clone, Copy, Default)] +pub struct TpmaSa(pub u8); + +impl TpmaSa { + pub const CONTINUE_SESSION: u8 = 1 << 0; + pub const AUDIT_EXCLUSIVE: u8 = 1 << 1; + pub const AUDIT_RESET: u8 = 1 << 2; + pub const DECRYPT: u8 = 1 << 5; + pub const ENCRYPT: u8 = 1 << 6; + pub const AUDIT: u8 = 1 << 7; + + pub fn new() -> Self { + Self(0) + } + + pub fn with_continue_session(mut self) -> Self { + self.0 |= Self::CONTINUE_SESSION; + self + } +} + +/// TPM command header tag +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmSt { + NoSessions = 0x8001, + Sessions = 0x8002, + RspCommand = 0x00C4, +} + +impl TpmSt { + pub fn to_u16(self) -> u16 { + self as u16 + } + + pub fn from_u16(v: u16) -> Option { + match v { + 0x8001 => Some(TpmSt::NoSessions), + 0x8002 => Some(TpmSt::Sessions), + 0x00C4 => Some(TpmSt::RspCommand), + _ => None, + } + } +} + +/// ECC Curve IDs +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum TpmEccCurve { + None = 0x0000, + NistP256 = 0x0003, + NistP384 = 0x0004, + NistP521 = 0x0005, +} + +impl TpmEccCurve { + pub fn to_u16(self) -> u16 { + self as u16 + } +} + +/// RSA Key Bits +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum RsaKeyBits { + Rsa1024 = 1024, + Rsa2048 = 2048, + Rsa3072 = 3072, + Rsa4096 = 4096, +} + +impl RsaKeyBits { + pub fn to_u16(self) -> u16 { + self as u16 + } +} diff --git a/tpm2/src/device.rs b/tpm2/src/device.rs new file mode 100644 index 000000000..dbb105f81 --- /dev/null +++ b/tpm2/src/device.rs @@ -0,0 +1,313 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM device communication layer +//! +//! Provides low-level communication with TPM devices via /dev/tpmrm0 or /dev/tpm0. + +use anyhow::{bail, Context, Result}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::path::Path; + +use super::constants::*; +use super::marshal::*; + +/// Maximum TPM command/response size +const TPM_MAX_COMMAND_SIZE: usize = 4096; + +/// TPM device handle +pub struct TpmDevice { + file: File, + path: String, +} + +impl TpmDevice { + /// Open a TPM device + pub fn open(path: &str) -> Result { + // Strip "device:" prefix if present + let device_path = path.strip_prefix("device:").unwrap_or(path); + + let file = OpenOptions::new() + .read(true) + .write(true) + .open(device_path) + .with_context(|| format!("failed to open TPM device: {}", device_path))?; + + Ok(Self { + file, + path: device_path.to_string(), + }) + } + + /// Detect and open the default TPM device + pub fn detect() -> Result { + if Path::new("/dev/tpmrm0").exists() { + Self::open("/dev/tpmrm0") + } else if Path::new("/dev/tpm0").exists() { + Self::open("/dev/tpm0") + } else { + bail!("TPM device not found") + } + } + + /// Get the device path + pub fn path(&self) -> &str { + &self.path + } + + /// Send a command to the TPM and receive the response + pub fn transmit(&mut self, command: &[u8]) -> Result> { + // Write command + self.file + .write_all(command) + .context("failed to write TPM command")?; + + // Read response + let mut response = vec![0u8; TPM_MAX_COMMAND_SIZE]; + let n = self + .file + .read(&mut response) + .context("failed to read TPM response")?; + + response.truncate(n); + Ok(response) + } + + /// Execute a TPM command and parse the response + pub fn execute(&mut self, command: &[u8]) -> Result { + let response_bytes = self.transmit(command)?; + TpmResponse::parse(&response_bytes) + } +} + +/// TPM command builder +pub struct TpmCommand { + buf: CommandBuffer, + has_sessions: bool, +} + +impl TpmCommand { + /// Create a new command without sessions + pub fn new(command_code: TpmCc) -> Self { + let mut buf = CommandBuffer::with_capacity(256); + + // Header: tag (2) + size (4) + command code (4) + buf.put_u16(TpmSt::NoSessions.to_u16()); + buf.put_u32(0); // Size placeholder + buf.put_u32(command_code.to_u32()); + + Self { + buf, + has_sessions: false, + } + } + + /// Create a new command with sessions + pub fn with_sessions(command_code: TpmCc) -> Self { + let mut buf = CommandBuffer::with_capacity(256); + + // Header: tag (2) + size (4) + command code (4) + buf.put_u16(TpmSt::Sessions.to_u16()); + buf.put_u32(0); // Size placeholder + buf.put_u32(command_code.to_u32()); + + Self { + buf, + has_sessions: true, + } + } + + /// Add a handle to the command + pub fn add_handle(&mut self, handle: u32) { + self.buf.put_u32(handle); + } + + /// Add raw bytes to the command + pub fn add_bytes(&mut self, data: &[u8]) { + self.buf.put_bytes(data); + } + + /// Add a u8 value + pub fn add_u8(&mut self, v: u8) { + self.buf.put_u8(v); + } + + /// Add a u16 value + pub fn add_u16(&mut self, v: u16) { + self.buf.put_u16(v); + } + + /// Add a u32 value + pub fn add_u32(&mut self, v: u32) { + self.buf.put_u32(v); + } + + /// Add a TPM2B structure + pub fn add_tpm2b(&mut self, data: &[u8]) { + self.buf.put_tpm2b(data); + } + + /// Add an empty TPM2B structure + pub fn add_tpm2b_empty(&mut self) { + self.buf.put_tpm2b_empty(); + } + + /// Add a marshallable structure + pub fn add(&mut self, value: &T) { + value.marshal(&mut self.buf); + } + + /// Add password authorization session (null auth) + pub fn add_null_auth_area(&mut self) { + // Authorization area size (4 bytes) + // Session handle (4) + nonce (2) + attributes (1) + auth (2) = 9 bytes minimum + let auth_size: u32 = 4 + 2 + 1 + 2; // 9 bytes for null auth + + self.buf.put_u32(auth_size); + self.buf.put_u32(tpm_rh::PW); // Password session handle + self.buf.put_u16(0); // Empty nonce + self.buf.put_u8(0); // Session attributes (continue = 0) + self.buf.put_u16(0); // Empty auth value + } + + /// Add a policy session authorization + pub fn add_policy_auth(&mut self, session_handle: u32) { + let auth_size: u32 = 4 + 2 + 1 + 2; + + self.buf.put_u32(auth_size); + self.buf.put_u32(session_handle); + self.buf.put_u16(0); // Empty nonce + self.buf.put_u8(TpmaSa::CONTINUE_SESSION); // Continue session + self.buf.put_u16(0); // Empty auth value + } + + /// Finalize the command and return the bytes + pub fn finalize(mut self) -> Vec { + // Update the size field + let size = self.buf.len() as u32; + self.buf.update_u32(2, size); + self.buf.into_vec() + } + + /// Get current buffer for inspection + pub fn buffer(&self) -> &CommandBuffer { + &self.buf + } +} + +/// TPM response parser +#[derive(Debug)] +pub struct TpmResponse { + pub tag: TpmSt, + pub response_code: u32, + pub data: Vec, +} + +impl TpmResponse { + /// Parse a TPM response + pub fn parse(response: &[u8]) -> Result { + if response.len() < 10 { + bail!("TPM response too short: {} bytes", response.len()); + } + + let mut buf = ResponseBuffer::new(response); + + let tag_raw = buf.get_u16()?; + let tag = TpmSt::from_u16(tag_raw) + .ok_or_else(|| anyhow::anyhow!("invalid response tag: 0x{:04x}", tag_raw))?; + + let size = buf.get_u32()? as usize; + if response.len() < size { + bail!( + "TPM response size mismatch: expected {}, got {}", + size, + response.len() + ); + } + + let response_code = buf.get_u32()?; + + // Remaining data after header + let data = response[10..size].to_vec(); + + Ok(Self { + tag, + response_code, + data, + }) + } + + /// Check if the response indicates success + pub fn is_success(&self) -> bool { + self.response_code == 0 + } + + /// Get error description + pub fn error_description(&self) -> String { + if self.is_success() { + "success".to_string() + } else { + format!("TPM error: 0x{:08x}", self.response_code) + } + } + + /// Ensure the response is successful + pub fn ensure_success(&self) -> Result<()> { + if self.is_success() { + Ok(()) + } else { + bail!("{}", self.error_description()) + } + } + + /// Get a response buffer for parsing the data + pub fn data_buffer(&self) -> ResponseBuffer<'_> { + ResponseBuffer::new(&self.data) + } + + /// Skip the parameter size field (for commands with sessions) + pub fn skip_parameter_size(&self) -> Result> { + let mut buf = self.data_buffer(); + if self.tag == TpmSt::Sessions { + let _param_size = buf.get_u32()?; + } + Ok(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_command_builder() { + let mut cmd = TpmCommand::new(TpmCc::GetRandom); + cmd.add_u16(32); // Request 32 random bytes + + let bytes = cmd.finalize(); + + // Check header + assert_eq!(&bytes[0..2], &[0x80, 0x01]); // TPM_ST_NO_SESSIONS + assert_eq!(&bytes[6..10], &[0x00, 0x00, 0x01, 0x7B]); // TPM_CC_GetRandom + + // Check size + let size = u32::from_be_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]); + assert_eq!(size as usize, bytes.len()); + } + + #[test] + fn test_response_parse() { + // Minimal success response + let response = vec![ + 0x80, 0x01, // TPM_ST_NO_SESSIONS + 0x00, 0x00, 0x00, 0x0A, // Size = 10 + 0x00, 0x00, 0x00, 0x00, // TPM_RC_SUCCESS + ]; + + let parsed = TpmResponse::parse(&response).unwrap(); + assert!(parsed.is_success()); + assert!(parsed.data.is_empty()); + } +} diff --git a/tpm2/src/lib.rs b/tpm2/src/lib.rs new file mode 100644 index 000000000..ab5db337e --- /dev/null +++ b/tpm2/src/lib.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Pure Rust TPM 2.0 implementation +//! +//! This crate provides TPM 2.0 commands, communicating directly with the TPM +//! device without C library dependencies. +//! +//! ## Features +//! +//! - **Cross-compilation friendly**: Easy to cross-compile for different targets +//! - **Direct device communication**: Talks directly to `/dev/tpmrm0` or `/dev/tpm0` +//! +//! ## Supported Commands +//! +//! - NV operations: `NV_Read`, `NV_Write`, `NV_DefineSpace`, `NV_UndefineSpace` +//! - PCR operations: `PCR_Read`, `PCR_Extend` +//! - Key operations: `CreatePrimary`, `Create`, `Load`, `EvictControl` +//! - Sealing: `Seal`, `Unseal` with PCR policy +//! - Attestation: `Quote` +//! - Random: `GetRandom` +//! - Sessions: Policy sessions for PCR-based authorization +//! +//! ## Example +//! +//! ```no_run +//! use tpm2::TpmContext; +//! +//! let mut ctx = TpmContext::new(None)?; // Auto-detect TPM device +//! let random_bytes = ctx.get_random(32)?; +//! # Ok::<(), anyhow::Error>(()) +//! ``` + +mod commands; +mod constants; +mod device; +mod marshal; +mod session; +mod types; + +pub use commands::TpmContext; +pub use constants::*; +pub use types::*; + +// Re-export device for advanced usage +pub use device::{TpmCommand, TpmDevice, TpmResponse}; +pub use marshal::{CommandBuffer, Marshal, ResponseBuffer, Unmarshal}; +pub use session::AuthSession; diff --git a/tpm2/src/marshal.rs b/tpm2/src/marshal.rs new file mode 100644 index 000000000..e46eedd51 --- /dev/null +++ b/tpm2/src/marshal.rs @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 marshalling/unmarshalling utilities +//! +//! Provides serialization and deserialization for TPM structures. + +use anyhow::{bail, Result}; + +/// Buffer for building TPM commands +#[derive(Debug, Default)] +pub struct CommandBuffer { + data: Vec, +} + +impl CommandBuffer { + pub fn new() -> Self { + Self { data: Vec::new() } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + data: Vec::with_capacity(capacity), + } + } + + pub fn put_u8(&mut self, v: u8) { + self.data.push(v); + } + + pub fn put_u16(&mut self, v: u16) { + self.data.extend_from_slice(&v.to_be_bytes()); + } + + pub fn put_u32(&mut self, v: u32) { + self.data.extend_from_slice(&v.to_be_bytes()); + } + + pub fn put_u64(&mut self, v: u64) { + self.data.extend_from_slice(&v.to_be_bytes()); + } + + pub fn put_bytes(&mut self, bytes: &[u8]) { + self.data.extend_from_slice(bytes); + } + + /// Put a TPM2B structure (2-byte size prefix + data) + pub fn put_tpm2b(&mut self, data: &[u8]) { + self.put_u16(data.len() as u16); + self.put_bytes(data); + } + + /// Put an empty TPM2B structure + pub fn put_tpm2b_empty(&mut self) { + self.put_u16(0); + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + pub fn into_vec(self) -> Vec { + self.data + } + + /// Update a u32 at a specific position (for size fields) + pub fn update_u32(&mut self, pos: usize, v: u32) { + self.data[pos..pos + 4].copy_from_slice(&v.to_be_bytes()); + } +} + +/// Buffer for parsing TPM responses +#[derive(Debug)] +pub struct ResponseBuffer<'a> { + data: &'a [u8], + pos: usize, +} + +impl<'a> ResponseBuffer<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + pub fn remaining(&self) -> usize { + self.data.len().saturating_sub(self.pos) + } + + pub fn position(&self) -> usize { + self.pos + } + + pub fn get_u8(&mut self) -> Result { + if self.pos >= self.data.len() { + bail!("buffer underflow reading u8"); + } + let v = self.data[self.pos]; + self.pos += 1; + Ok(v) + } + + pub fn get_u16(&mut self) -> Result { + if self.pos + 2 > self.data.len() { + bail!("buffer underflow reading u16"); + } + let v = u16::from_be_bytes([self.data[self.pos], self.data[self.pos + 1]]); + self.pos += 2; + Ok(v) + } + + pub fn get_u32(&mut self) -> Result { + if self.pos + 4 > self.data.len() { + bail!("buffer underflow reading u32"); + } + let v = u32::from_be_bytes([ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + ]); + self.pos += 4; + Ok(v) + } + + pub fn get_u64(&mut self) -> Result { + if self.pos + 8 > self.data.len() { + bail!("buffer underflow reading u64"); + } + let v = u64::from_be_bytes([ + self.data[self.pos], + self.data[self.pos + 1], + self.data[self.pos + 2], + self.data[self.pos + 3], + self.data[self.pos + 4], + self.data[self.pos + 5], + self.data[self.pos + 6], + self.data[self.pos + 7], + ]); + self.pos += 8; + Ok(v) + } + + pub fn get_bytes(&mut self, len: usize) -> Result> { + if self.pos + len > self.data.len() { + bail!( + "buffer underflow reading {} bytes (remaining: {})", + len, + self.remaining() + ); + } + let v = self.data[self.pos..self.pos + len].to_vec(); + self.pos += len; + Ok(v) + } + + /// Get a TPM2B structure (2-byte size prefix + data) + pub fn get_tpm2b(&mut self) -> Result> { + let size = self.get_u16()? as usize; + self.get_bytes(size) + } + + /// Get remaining bytes + pub fn get_remaining(&mut self) -> Vec { + let v = self.data[self.pos..].to_vec(); + self.pos = self.data.len(); + v + } + + /// Skip bytes + pub fn skip(&mut self, len: usize) -> Result<()> { + if self.pos + len > self.data.len() { + bail!("buffer underflow skipping {} bytes", len); + } + self.pos += len; + Ok(()) + } + + /// Peek at bytes without advancing position + pub fn peek_bytes(&self, len: usize) -> Result<&[u8]> { + if self.pos + len > self.data.len() { + bail!("buffer underflow peeking {} bytes", len); + } + Ok(&self.data[self.pos..self.pos + len]) + } +} + +/// Trait for types that can be marshalled to TPM format +pub trait Marshal { + fn marshal(&self, buf: &mut CommandBuffer); + + fn to_bytes(&self) -> Vec { + let mut buf = CommandBuffer::new(); + self.marshal(&mut buf); + buf.into_vec() + } +} + +/// Trait for types that can be unmarshalled from TPM format +pub trait Unmarshal: Sized { + fn unmarshal(buf: &mut ResponseBuffer) -> Result; + + fn from_bytes(data: &[u8]) -> Result { + let mut buf = ResponseBuffer::new(data); + Self::unmarshal(&mut buf) + } +} + +// Implement Marshal for primitive types +impl Marshal for u8 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u8(*self); + } +} + +impl Marshal for u16 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(*self); + } +} + +impl Marshal for u32 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(*self); + } +} + +impl Marshal for u64 { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u64(*self); + } +} + +// Implement Unmarshal for primitive types +impl Unmarshal for u8 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u8() + } +} + +impl Unmarshal for u16 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u16() + } +} + +impl Unmarshal for u32 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u32() + } +} + +impl Unmarshal for u64 { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + buf.get_u64() + } +} diff --git a/tpm2/src/session.rs b/tpm2/src/session.rs new file mode 100644 index 000000000..b706990a7 --- /dev/null +++ b/tpm2/src/session.rs @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 session management + +use anyhow::{Context, Result}; + +use super::constants::*; +use super::device::*; +use super::marshal::*; +use super::types::*; + +/// Authorization session handle +#[derive(Debug, Clone, Copy)] +pub struct AuthSession { + pub handle: u32, + pub session_type: TpmSe, + pub hash_alg: TpmAlgId, +} + +impl AuthSession { + /// Start a new authorization session + pub fn start(device: &mut TpmDevice, session_type: TpmSe, hash_alg: TpmAlgId) -> Result { + // TPM2_StartAuthSession command + let mut cmd = TpmCommand::new(TpmCc::StartAuthSession); + + const ZERO_NONCE: [u8; 16] = [0u8; 16]; + + // tpmKey (TPM_RH_NULL for unbound session) + cmd.add_handle(tpm_rh::NULL); + // bind (TPM_RH_NULL for unbound session) + cmd.add_handle(tpm_rh::NULL); + // nonceCaller (16-byte nonce as required by TPM spec) + cmd.add_tpm2b(&ZERO_NONCE); + // encryptedSalt (empty - no salt) + cmd.add_tpm2b_empty(); + // sessionType + cmd.add_u8(session_type as u8); + // symmetric (AES-128-CFB, matches TPM default expectation) + cmd.add(&TpmtSymDef::aes_128_cfb()); + // authHash + cmd.add_u16(hash_alg.to_u16()); + + let cmd_bytes = cmd.finalize(); + tracing::debug!("StartAuthSession command: {} bytes", cmd_bytes.len()); + let response = device.execute(&cmd_bytes)?; + if !response.is_success() { + anyhow::bail!( + "StartAuthSession failed with TPM error: 0x{:08x}", + response.response_code + ); + } + + let mut buf = response.data_buffer(); + let handle = buf.get_u32()?; + let _nonce_tpm = buf.get_tpm2b()?; // nonceTPM + + Ok(Self { + handle, + session_type, + hash_alg, + }) + } + + /// Start a policy session + pub fn start_policy(device: &mut TpmDevice, hash_alg: TpmAlgId) -> Result { + Self::start(device, TpmSe::Policy, hash_alg) + } + + /// Start a trial policy session (for computing policy digest) + pub fn start_trial(device: &mut TpmDevice, hash_alg: TpmAlgId) -> Result { + Self::start(device, TpmSe::Trial, hash_alg) + } + + /// Apply PCR policy to this session + pub fn policy_pcr( + &self, + device: &mut TpmDevice, + pcr_digest: &[u8], + pcr_selection: &TpmlPcrSelection, + ) -> Result<()> { + let mut cmd = TpmCommand::new(TpmCc::PolicyPcr); + + // policySession + cmd.add_handle(self.handle); + // pcrDigest + cmd.add_tpm2b(pcr_digest); + // pcrs + cmd.add(pcr_selection); + + let response = device.execute(&cmd.finalize())?; + response.ensure_success().context("PolicyPCR failed")?; + + Ok(()) + } + + /// Get the current policy digest + pub fn get_digest(&self, device: &mut TpmDevice) -> Result> { + let mut cmd = TpmCommand::new(TpmCc::PolicyGetDigest); + cmd.add_handle(self.handle); + + let response = device.execute(&cmd.finalize())?; + response + .ensure_success() + .context("PolicyGetDigest failed")?; + + let mut buf = response.data_buffer(); + let digest = buf.get_tpm2b()?; + + Ok(digest) + } + + /// Flush (close) this session + pub fn flush(self, device: &mut TpmDevice) -> Result<()> { + let mut cmd = TpmCommand::new(TpmCc::FlushContext); + cmd.add_handle(self.handle); + + let response = device.execute(&cmd.finalize())?; + response.ensure_success().context("FlushContext failed")?; + + Ok(()) + } +} + +/// Compute the PCR digest for a given PCR selection +pub fn compute_pcr_digest( + device: &mut TpmDevice, + pcr_selection: &TpmlPcrSelection, + hash_alg: TpmAlgId, +) -> Result> { + use sha2::{Digest, Sha256, Sha384, Sha512}; + + // Read PCR values + let pcr_values = read_pcr_values(device, pcr_selection)?; + + // Concatenate all PCR values + let mut concat = Vec::new(); + for value in &pcr_values { + concat.extend_from_slice(value); + } + + // Hash the concatenated values + let digest = match hash_alg { + TpmAlgId::Sha256 => { + let mut hasher = Sha256::new(); + hasher.update(&concat); + hasher.finalize().to_vec() + } + TpmAlgId::Sha384 => { + let mut hasher = Sha384::new(); + hasher.update(&concat); + hasher.finalize().to_vec() + } + TpmAlgId::Sha512 => { + let mut hasher = Sha512::new(); + hasher.update(&concat); + hasher.finalize().to_vec() + } + _ => anyhow::bail!("unsupported hash algorithm for PCR digest"), + }; + + Ok(digest) +} + +/// Read PCR values for a selection +fn read_pcr_values( + device: &mut TpmDevice, + pcr_selection: &TpmlPcrSelection, +) -> Result>> { + let mut cmd = TpmCommand::new(TpmCc::PcrRead); + cmd.add(pcr_selection); + + let response = device.execute(&cmd.finalize())?; + response.ensure_success().context("PCR_Read failed")?; + + let mut buf = response.data_buffer(); + let _update_counter = buf.get_u32()?; + let _pcr_selection_out = TpmlPcrSelection::unmarshal(&mut buf)?; + let digest_list = TpmlDigest::unmarshal(&mut buf)?; + + Ok(digest_list.digests.into_iter().map(|d| d.buffer).collect()) +} diff --git a/tpm2/src/types.rs b/tpm2/src/types.rs new file mode 100644 index 000000000..65d9c709f --- /dev/null +++ b/tpm2/src/types.rs @@ -0,0 +1,876 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! TPM 2.0 data types + +use anyhow::{bail, Result}; + +use super::constants::*; +use super::marshal::*; + +/// TPM2B_DIGEST - Variable length digest +#[derive(Debug, Clone, Default)] +pub struct Tpm2bDigest { + pub buffer: Vec, +} + +impl Tpm2bDigest { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bDigest { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bDigest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_DATA - Variable length data +#[derive(Debug, Clone, Default)] +pub struct Tpm2bData { + pub buffer: Vec, +} + +impl Tpm2bData { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bData { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bData { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_SENSITIVE_DATA - Sensitive data for sealing +#[derive(Debug, Clone, Default)] +pub struct Tpm2bSensitiveData { + pub buffer: Vec, +} + +impl Tpm2bSensitiveData { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bSensitiveData { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +/// TPM2B_AUTH - Authorization value +#[derive(Debug, Clone, Default)] +pub struct Tpm2bAuth { + pub buffer: Vec, +} + +impl Tpm2bAuth { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } + + pub fn empty() -> Self { + Self { buffer: Vec::new() } + } +} + +impl Marshal for Tpm2bAuth { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bAuth { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_NONCE - Nonce value +pub type Tpm2bNonce = Tpm2bDigest; + +/// TPM2B_MAX_NV_BUFFER - NV buffer +#[derive(Debug, Clone, Default)] +pub struct Tpm2bMaxNvBuffer { + pub buffer: Vec, +} + +impl Tpm2bMaxNvBuffer { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } +} + +impl Marshal for Tpm2bMaxNvBuffer { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bMaxNvBuffer { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPMS_PCR_SELECTION - PCR selection for a single hash algorithm +#[derive(Debug, Clone)] +pub struct TpmsPcrSelection { + pub hash: TpmAlgId, + pub pcr_select: Vec, // Bitmap of selected PCRs +} + +impl TpmsPcrSelection { + pub fn new(hash: TpmAlgId, pcrs: &[u32]) -> Self { + // Calculate required size (at least 3 bytes for PCR 0-23) + let max_pcr = pcrs.iter().max().copied().unwrap_or(0); + let size = ((max_pcr / 8) + 1).max(3) as usize; + let mut pcr_select = vec![0u8; size]; + + for &pcr in pcrs { + let byte_idx = (pcr / 8) as usize; + let bit_idx = pcr % 8; + if byte_idx < pcr_select.len() { + pcr_select[byte_idx] |= 1 << bit_idx; + } + } + + Self { hash, pcr_select } + } + + pub fn sha256(pcrs: &[u32]) -> Self { + Self::new(TpmAlgId::Sha256, pcrs) + } +} + +impl Marshal for TpmsPcrSelection { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.hash.to_u16()); + buf.put_u8(self.pcr_select.len() as u8); + buf.put_bytes(&self.pcr_select); + } +} + +impl Unmarshal for TpmsPcrSelection { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let hash_alg = buf.get_u16()?; + let hash = TpmAlgId::from_u16(hash_alg) + .ok_or_else(|| anyhow::anyhow!("unknown hash algorithm: 0x{:04x}", hash_alg))?; + let size = buf.get_u8()? as usize; + let pcr_select = buf.get_bytes(size)?; + Ok(Self { hash, pcr_select }) + } +} + +/// TPML_PCR_SELECTION - List of PCR selections +#[derive(Debug, Clone, Default)] +pub struct TpmlPcrSelection { + pub pcr_selections: Vec, +} + +impl TpmlPcrSelection { + pub fn new(selections: Vec) -> Self { + Self { + pcr_selections: selections, + } + } + + pub fn single(hash: TpmAlgId, pcrs: &[u32]) -> Self { + Self { + pcr_selections: vec![TpmsPcrSelection::new(hash, pcrs)], + } + } +} + +impl Marshal for TpmlPcrSelection { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(self.pcr_selections.len() as u32); + for sel in &self.pcr_selections { + sel.marshal(buf); + } + } +} + +impl Unmarshal for TpmlPcrSelection { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let count = buf.get_u32()? as usize; + let mut pcr_selections = Vec::with_capacity(count); + for _ in 0..count { + pcr_selections.push(TpmsPcrSelection::unmarshal(buf)?); + } + Ok(Self { pcr_selections }) + } +} + +/// TPML_DIGEST - List of digests +#[derive(Debug, Clone, Default)] +pub struct TpmlDigest { + pub digests: Vec, +} + +impl Unmarshal for TpmlDigest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let count = buf.get_u32()? as usize; + let mut digests = Vec::with_capacity(count); + for _ in 0..count { + digests.push(Tpm2bDigest::unmarshal(buf)?); + } + Ok(Self { digests }) + } +} + +/// TPMS_NV_PUBLIC - NV index public area +#[derive(Debug, Clone)] +pub struct TpmsNvPublic { + pub nv_index: u32, + pub name_alg: TpmAlgId, + pub attributes: TpmaNv, + pub auth_policy: Tpm2bDigest, + pub data_size: u16, +} + +impl TpmsNvPublic { + pub fn new(nv_index: u32, data_size: u16, attributes: TpmaNv) -> Self { + Self { + nv_index, + name_alg: TpmAlgId::Sha256, + attributes, + auth_policy: Tpm2bDigest::empty(), + data_size, + } + } +} + +impl Marshal for TpmsNvPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(self.nv_index); + buf.put_u16(self.name_alg.to_u16()); + buf.put_u32(self.attributes.0); + self.auth_policy.marshal(buf); + buf.put_u16(self.data_size); + } +} + +impl Unmarshal for TpmsNvPublic { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let nv_index = buf.get_u32()?; + let name_alg_raw = buf.get_u16()?; + let name_alg = TpmAlgId::from_u16(name_alg_raw) + .ok_or_else(|| anyhow::anyhow!("unknown algorithm: 0x{:04x}", name_alg_raw))?; + let attributes = TpmaNv(buf.get_u32()?); + let auth_policy = Tpm2bDigest::unmarshal(buf)?; + let data_size = buf.get_u16()?; + Ok(Self { + nv_index, + name_alg, + attributes, + auth_policy, + data_size, + }) + } +} + +/// TPM2B_NV_PUBLIC - NV public with size prefix +#[derive(Debug, Clone)] +pub struct Tpm2bNvPublic { + pub nv_public: TpmsNvPublic, +} + +impl Marshal for Tpm2bNvPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + let mut inner = CommandBuffer::new(); + self.nv_public.marshal(&mut inner); + buf.put_tpm2b(inner.as_bytes()); + } +} + +impl Unmarshal for Tpm2bNvPublic { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let size = buf.get_u16()? as usize; + if size == 0 { + bail!("empty NV public"); + } + let data = buf.get_bytes(size)?; + let mut inner = ResponseBuffer::new(&data); + let nv_public = TpmsNvPublic::unmarshal(&mut inner)?; + Ok(Self { nv_public }) + } +} + +/// TPMT_SYM_DEF - Symmetric algorithm definition +#[derive(Debug, Clone, Copy)] +pub struct TpmtSymDef { + pub algorithm: TpmAlgId, + pub key_bits: u16, + pub mode: TpmAlgId, +} + +impl TpmtSymDef { + pub fn null() -> Self { + Self { + algorithm: TpmAlgId::Null, + key_bits: 0, + mode: TpmAlgId::Null, + } + } + + pub fn aes_128_cfb() -> Self { + Self { + algorithm: TpmAlgId::Aes, + key_bits: 128, + mode: TpmAlgId::Cfb, + } + } +} + +impl Marshal for TpmtSymDef { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.algorithm.to_u16()); + if self.algorithm != TpmAlgId::Null { + buf.put_u16(self.key_bits); + buf.put_u16(self.mode.to_u16()); + } + } +} + +impl Unmarshal for TpmtSymDef { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let alg = buf.get_u16()?; + let algorithm = TpmAlgId::from_u16(alg) + .ok_or_else(|| anyhow::anyhow!("unknown algorithm: 0x{:04x}", alg))?; + if algorithm == TpmAlgId::Null { + Ok(Self::null()) + } else { + let key_bits = buf.get_u16()?; + let mode_raw = buf.get_u16()?; + let mode = TpmAlgId::from_u16(mode_raw) + .ok_or_else(|| anyhow::anyhow!("unknown mode: 0x{:04x}", mode_raw))?; + Ok(Self { + algorithm, + key_bits, + mode, + }) + } + } +} + +/// TPMT_SYM_DEF_OBJECT - Symmetric definition for objects +pub type TpmtSymDefObject = TpmtSymDef; + +/// TPMS_SCHEME_HASH - Hash scheme +#[derive(Debug, Clone, Copy)] +pub struct TpmsSchemeHash { + pub hash_alg: TpmAlgId, +} + +/// TPMT_RSA_SCHEME - RSA signature scheme +#[derive(Debug, Clone, Copy)] +pub struct TpmtRsaScheme { + pub scheme: TpmAlgId, + pub hash_alg: Option, +} + +impl TpmtRsaScheme { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + hash_alg: None, + } + } + + pub fn rsassa(hash: TpmAlgId) -> Self { + Self { + scheme: TpmAlgId::RsaSsa, + hash_alg: Some(hash), + } + } +} + +impl Marshal for TpmtRsaScheme { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + if let Some(hash) = self.hash_alg { + buf.put_u16(hash.to_u16()); + } + } +} + +/// TPMT_ECC_SCHEME - ECC signature scheme +#[derive(Debug, Clone, Copy)] +pub struct TpmtEccScheme { + pub scheme: TpmAlgId, + pub hash_alg: Option, +} + +impl TpmtEccScheme { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + hash_alg: None, + } + } + + pub fn ecdsa(hash: TpmAlgId) -> Self { + Self { + scheme: TpmAlgId::EcDsa, + hash_alg: Some(hash), + } + } +} + +impl Marshal for TpmtEccScheme { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + if let Some(hash) = self.hash_alg { + buf.put_u16(hash.to_u16()); + } + } +} + +/// TPMT_SIG_SCHEME - Signature scheme (for Quote) +#[derive(Debug, Clone, Copy)] +pub struct TpmtSigScheme { + pub scheme: TpmAlgId, + pub hash_alg: Option, +} + +impl TpmtSigScheme { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + hash_alg: None, + } + } +} + +impl Marshal for TpmtSigScheme { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + if let Some(hash) = self.hash_alg { + buf.put_u16(hash.to_u16()); + } + } +} + +/// TPMS_RSA_PARMS - RSA key parameters +#[derive(Debug, Clone)] +pub struct TpmsRsaParms { + pub symmetric: TpmtSymDefObject, + pub scheme: TpmtRsaScheme, + pub key_bits: u16, + pub exponent: u32, +} + +impl TpmsRsaParms { + pub fn storage_key() -> Self { + Self { + symmetric: TpmtSymDef::aes_128_cfb(), + scheme: TpmtRsaScheme::null(), + key_bits: 2048, + exponent: 0, // Default exponent (65537) + } + } +} + +impl Marshal for TpmsRsaParms { + fn marshal(&self, buf: &mut CommandBuffer) { + self.symmetric.marshal(buf); + self.scheme.marshal(buf); + buf.put_u16(self.key_bits); + buf.put_u32(self.exponent); + } +} + +/// TPMS_ECC_PARMS - ECC key parameters +#[derive(Debug, Clone)] +pub struct TpmsEccParms { + pub symmetric: TpmtSymDefObject, + pub scheme: TpmtEccScheme, + pub curve_id: TpmEccCurve, + pub kdf: TpmAlgId, +} + +impl Marshal for TpmsEccParms { + fn marshal(&self, buf: &mut CommandBuffer) { + self.symmetric.marshal(buf); + self.scheme.marshal(buf); + buf.put_u16(self.curve_id.to_u16()); + buf.put_u16(self.kdf.to_u16()); // KDF scheme (usually NULL) + } +} + +/// TPMS_KEYEDHASH_PARMS - Keyed hash parameters (for sealed data) +#[derive(Debug, Clone, Copy)] +pub struct TpmsKeyedHashParms { + pub scheme: TpmAlgId, +} + +impl TpmsKeyedHashParms { + pub fn null() -> Self { + Self { + scheme: TpmAlgId::Null, + } + } +} + +impl Marshal for TpmsKeyedHashParms { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.scheme.to_u16()); + } +} + +/// TPMT_PUBLIC - Public area template +#[derive(Debug, Clone)] +pub struct TpmtPublic { + pub type_alg: TpmAlgId, + pub name_alg: TpmAlgId, + pub object_attributes: TpmaObject, + pub auth_policy: Tpm2bDigest, + pub parameters: TpmtPublicParms, + pub unique: TpmtPublicUnique, +} + +/// TPMU_PUBLIC_PARMS - Public parameters union +#[derive(Debug, Clone)] +pub enum TpmtPublicParms { + Rsa(TpmsRsaParms), + Ecc(TpmsEccParms), + KeyedHash(TpmsKeyedHashParms), +} + +impl Marshal for TpmtPublicParms { + fn marshal(&self, buf: &mut CommandBuffer) { + match self { + TpmtPublicParms::Rsa(p) => p.marshal(buf), + TpmtPublicParms::Ecc(p) => p.marshal(buf), + TpmtPublicParms::KeyedHash(p) => p.marshal(buf), + } + } +} + +/// TPMU_PUBLIC_ID - Unique identifier union +#[derive(Debug, Clone)] +pub enum TpmtPublicUnique { + Rsa(Vec), // TPM2B_PUBLIC_KEY_RSA + Ecc(Vec, Vec), // TPMS_ECC_POINT (x, y) + KeyedHash(Vec), // TPM2B_DIGEST +} + +impl Marshal for TpmtPublicUnique { + fn marshal(&self, buf: &mut CommandBuffer) { + match self { + TpmtPublicUnique::Rsa(n) => buf.put_tpm2b(n), + TpmtPublicUnique::Ecc(x, y) => { + buf.put_tpm2b(x); + buf.put_tpm2b(y); + } + TpmtPublicUnique::KeyedHash(d) => buf.put_tpm2b(d), + } + } +} + +impl TpmtPublic { + /// Create an RSA storage key template (SRK) + pub fn rsa_storage_key() -> Self { + Self { + type_alg: TpmAlgId::Rsa, + name_alg: TpmAlgId::Sha256, + object_attributes: TpmaObject::new() + .with_fixed_tpm() + .with_fixed_parent() + .with_sensitive_data_origin() + .with_user_with_auth() + .with_restricted() + .with_decrypt(), + auth_policy: Tpm2bDigest::empty(), + parameters: TpmtPublicParms::Rsa(TpmsRsaParms::storage_key()), + unique: TpmtPublicUnique::Rsa(Vec::new()), + } + } + + /// Create a sealed data object template + pub fn sealed_object(policy_digest: Tpm2bDigest) -> Self { + // If policy_digest is empty, use userWithAuth; otherwise use adminWithPolicy + let object_attributes = if policy_digest.buffer.is_empty() { + TpmaObject::new() + .with_fixed_tpm() + .with_fixed_parent() + .with_user_with_auth() + } else { + TpmaObject::new() + .with_fixed_tpm() + .with_fixed_parent() + .with_admin_with_policy() + }; + + Self { + type_alg: TpmAlgId::KeyedHash, + name_alg: TpmAlgId::Sha256, + object_attributes, + auth_policy: policy_digest, + parameters: TpmtPublicParms::KeyedHash(TpmsKeyedHashParms::null()), + unique: TpmtPublicUnique::KeyedHash(Vec::new()), + } + } +} + +impl Marshal for TpmtPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.type_alg.to_u16()); + buf.put_u16(self.name_alg.to_u16()); + buf.put_u32(self.object_attributes.0); + self.auth_policy.marshal(buf); + self.parameters.marshal(buf); + self.unique.marshal(buf); + } +} + +/// TPM2B_PUBLIC - Public area with size prefix +#[derive(Debug, Clone)] +pub struct Tpm2bPublic { + pub public_area: Vec, // Raw marshalled TPMT_PUBLIC +} + +impl Tpm2bPublic { + pub fn from_template(template: &TpmtPublic) -> Self { + Self { + public_area: template.to_bytes(), + } + } +} + +impl Marshal for Tpm2bPublic { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.public_area); + } +} + +impl Unmarshal for Tpm2bPublic { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let public_area = buf.get_tpm2b()?; + Ok(Self { public_area }) + } +} + +/// TPM2B_PRIVATE - Private area +#[derive(Debug, Clone)] +pub struct Tpm2bPrivate { + pub buffer: Vec, +} + +impl Tpm2bPrivate { + pub fn new(data: Vec) -> Self { + Self { buffer: data } + } +} + +impl Marshal for Tpm2bPrivate { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_tpm2b(&self.buffer); + } +} + +impl Unmarshal for Tpm2bPrivate { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + Ok(Self { + buffer: buf.get_tpm2b()?, + }) + } +} + +/// TPM2B_SENSITIVE_CREATE - Sensitive data for object creation +#[derive(Debug, Clone, Default)] +pub struct Tpm2bSensitiveCreate { + pub user_auth: Tpm2bAuth, + pub data: Tpm2bSensitiveData, +} + +impl Tpm2bSensitiveCreate { + pub fn with_data(data: Vec) -> Self { + Self { + user_auth: Tpm2bAuth::empty(), + data: Tpm2bSensitiveData::new(data), + } + } + + pub fn empty() -> Self { + Self::default() + } +} + +impl Marshal for Tpm2bSensitiveCreate { + fn marshal(&self, buf: &mut CommandBuffer) { + // First marshal the inner structure + let mut inner = CommandBuffer::new(); + self.user_auth.marshal(&mut inner); + self.data.marshal(&mut inner); + // Then wrap with size + buf.put_tpm2b(inner.as_bytes()); + } +} + +/// TPMS_ATTEST - Attestation structure (returned by Quote) +#[derive(Debug, Clone)] +pub struct TpmsAttest { + pub raw: Vec, // Keep raw bytes for signature verification +} + +impl Unmarshal for TpmsAttest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + // For Quote, we need the raw bytes for verification + // The structure is variable length, so we capture everything + let raw = buf.get_remaining(); + Ok(Self { raw }) + } +} + +/// TPM2B_ATTEST - Attestation data with size prefix +#[derive(Debug, Clone)] +pub struct Tpm2bAttest { + pub attestation_data: Vec, +} + +impl Unmarshal for Tpm2bAttest { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let attestation_data = buf.get_tpm2b()?; + Ok(Self { attestation_data }) + } +} + +/// TPMT_SIGNATURE - Signature structure +#[derive(Debug, Clone)] +pub struct TpmtSignature { + pub raw: Vec, // Keep raw bytes for verification +} + +impl Unmarshal for TpmtSignature { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + // Signature format depends on algorithm, capture remaining bytes + let raw = buf.get_remaining(); + Ok(Self { raw }) + } +} + +/// TPMT_TK_CREATION - Creation ticket +#[derive(Debug, Clone)] +pub struct TpmtTkCreation { + pub tag: u16, + pub hierarchy: u32, + pub digest: Tpm2bDigest, +} + +impl Unmarshal for TpmtTkCreation { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let tag = buf.get_u16()?; + let hierarchy = buf.get_u32()?; + let digest = Tpm2bDigest::unmarshal(buf)?; + Ok(Self { + tag, + hierarchy, + digest, + }) + } +} + +/// TPM2B_CREATION_DATA - Creation data +#[derive(Debug, Clone)] +pub struct Tpm2bCreationData { + pub data: Vec, +} + +impl Unmarshal for Tpm2bCreationData { + fn unmarshal(buf: &mut ResponseBuffer) -> Result { + let data = buf.get_tpm2b()?; + Ok(Self { data }) + } +} + +/// TPMS_SENSITIVE_CREATE - Inner sensitive create structure +#[derive(Debug, Clone, Default)] +pub struct TpmsSensitiveCreate { + pub user_auth: Tpm2bAuth, + pub data: Tpm2bSensitiveData, +} + +/// TPMT_HA - Hash value with algorithm +#[derive(Debug, Clone)] +pub struct TpmtHa { + pub hash_alg: TpmAlgId, + pub digest: Vec, +} + +impl TpmtHa { + pub fn sha256(digest: Vec) -> Self { + Self { + hash_alg: TpmAlgId::Sha256, + digest, + } + } +} + +impl Marshal for TpmtHa { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u16(self.hash_alg.to_u16()); + buf.put_bytes(&self.digest); + } +} + +/// TPML_DIGEST_VALUES - List of digest values for PCR extend +#[derive(Debug, Clone)] +pub struct TpmlDigestValues { + pub digests: Vec, +} + +impl TpmlDigestValues { + pub fn single(digest: TpmtHa) -> Self { + Self { + digests: vec![digest], + } + } +} + +impl Marshal for TpmlDigestValues { + fn marshal(&self, buf: &mut CommandBuffer) { + buf.put_u32(self.digests.len() as u32); + for d in &self.digests { + d.marshal(buf); + } + } +} From 39e1cd65ad9c6bed62c0441c4fc2f40d169f65f8 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 23 Dec 2025 00:03:00 +0000 Subject: [PATCH 060/264] tpm: Add some tests --- tpm2/src/bin/tpm2-test.rs | 127 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/tpm2/src/bin/tpm2-test.rs b/tpm2/src/bin/tpm2-test.rs index 94f69f821..bd9ea309f 100644 --- a/tpm2/src/bin/tpm2-test.rs +++ b/tpm2/src/bin/tpm2-test.rs @@ -25,7 +25,7 @@ //! all - Run all tests use std::env; -use tpm2::{tpm_rh, TpmAlgId, TpmContext, TpmlPcrSelection, TpmtPublic}; +use tpm2::{tpm_rh, ResponseBuffer, TpmAlgId, TpmContext, TpmlPcrSelection, TpmtPublic, Unmarshal}; fn main() { let args: Vec = env::args().collect(); @@ -300,6 +300,17 @@ fn test_quote_rsa() { println!("✓ Generated quote:"); println!(" Quoted size: {} bytes", quoted.len()); println!(" Signature size: {} bytes", signature.len()); + + match verify_quote_pcr_digest(&mut ctx, "ed, &pcr_selection) + { + Ok(()) => println!( + "✓ Quote PCR digest matches current PCR values" + ), + Err(e) => println!( + "✗ Quote PCR digest verification failed: {}", + e + ), + } } Err(e) => { println!("✗ Quote failed: {}", e); @@ -371,6 +382,17 @@ fn test_quote_ecc() { println!("✓ Generated ECC quote:"); println!(" Quoted size: {} bytes", quoted.len()); println!(" Signature size: {} bytes", signature.len()); + + match verify_quote_pcr_digest(&mut ctx, "ed, &pcr_selection) + { + Ok(()) => println!( + "✓ Quote PCR digest matches current PCR values" + ), + Err(e) => println!( + "✗ Quote PCR digest verification failed: {}", + e + ), + } } Err(e) => { println!("✗ Quote failed: {}", e); @@ -751,9 +773,83 @@ fn test_seal_unseal() { println!(); } +fn parse_quote_attestation(quoted: &[u8]) -> anyhow::Result<(TpmlPcrSelection, Vec)> { + let mut buf = ResponseBuffer::new(quoted); + + let magic = buf.get_u32()?; + let attest_type = buf.get_u16()?; + if magic != 0xff544347 { + anyhow::bail!("unexpected TPMS_ATTEST.magic: 0x{:08x}", magic); + } + if attest_type != 0x8018 { + anyhow::bail!("unexpected TPMS_ATTEST.type: 0x{:04x}", attest_type); + } + let _qualified_signer = buf.get_tpm2b()?; + let _extra_data = buf.get_tpm2b()?; + + let _clock = buf.get_u64()?; + let _reset_count = buf.get_u32()?; + let _restart_count = buf.get_u32()?; + let _safe = buf.get_u8()?; + + let _firmware_version = buf.get_u64()?; + + let pcr_select = TpmlPcrSelection::unmarshal(&mut buf)?; + let pcr_digest = buf.get_tpm2b()?; + + Ok((pcr_select, pcr_digest)) +} + +fn verify_quote_pcr_digest( + ctx: &mut TpmContext, + quoted: &[u8], + requested_selection: &TpmlPcrSelection, +) -> anyhow::Result<()> { + use sha2::{Digest, Sha256, Sha384, Sha512}; + + let (attested_selection, attested_digest) = parse_quote_attestation(quoted)?; + + if attested_selection.pcr_selections.len() != requested_selection.pcr_selections.len() { + anyhow::bail!("quote returned unexpected PCR selection count"); + } + for (a, r) in attested_selection + .pcr_selections + .iter() + .zip(requested_selection.pcr_selections.iter()) + { + if a.hash.to_u16() != r.hash.to_u16() || a.pcr_select != r.pcr_select { + anyhow::bail!("quote returned PCR selection different from request"); + } + } + + if requested_selection.pcr_selections.len() != 1 { + anyhow::bail!("quote PCR verification only supports a single PCR bank selection"); + } + let hash_alg = requested_selection.pcr_selections[0].hash; + + let mut values = ctx.pcr_read(requested_selection)?; + values.sort_by_key(|(idx, _)| *idx); + let mut concat = Vec::new(); + for (_, v) in values { + concat.extend_from_slice(&v); + } + + let computed = match hash_alg { + TpmAlgId::Sha256 => Sha256::digest(&concat).to_vec(), + TpmAlgId::Sha384 => Sha384::digest(&concat).to_vec(), + TpmAlgId::Sha512 => Sha512::digest(&concat).to_vec(), + _ => anyhow::bail!("unsupported hash algorithm for quote PCR digest verification"), + }; + if computed != attested_digest { + anyhow::bail!("pcrDigest mismatch"); + } + + Ok(()) +} + fn test_seal_unseal_with_pcr() { println!("--- Test: Seal/Unseal Operations with PCR Policy ---"); - println!(" This seals data bound to PCR[0,7] (SHA256)"); + println!(" This seals data bound to PCR[23] (SHA256)"); let mut ctx = match TpmContext::new(None) { Ok(ctx) => ctx, @@ -777,7 +873,7 @@ fn test_seal_unseal_with_pcr() { }; let secret_data = b"PCR protected secret data!"; - let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[0, 7]); + let pcr_selection = TpmlPcrSelection::single(TpmAlgId::Sha256, &[23]); println!( " Sealing {} bytes of data with PCR policy...", secret_data.len() @@ -807,7 +903,7 @@ fn test_seal_unseal_with_pcr() { } println!(" Attempting to unseal data (PCRs must match)..."); - match ctx.unseal( + let unsealed_ok = match ctx.unseal( &pub_blob, &priv_blob, parent_handle, @@ -822,9 +918,32 @@ fn test_seal_unseal_with_pcr() { } else { println!("✗ Data mismatch!"); } + true } Err(e) => { println!("✗ Unseal failed (PCR mismatch?): {}", e); + false + } + }; + + if unsealed_ok { + println!(" Extending PCR 23 to ensure unseal fails in a different PCR environment..."); + let extend_value = [0xA5u8; 32]; + match ctx.pcr_extend(23, &extend_value, TpmAlgId::Sha256) { + Ok(()) => println!("✓ PCR_Extend succeeded"), + Err(e) => println!("✗ PCR_Extend failed: {}", e), + } + + println!(" Attempting to unseal again (must FAIL after PCR change)..."); + match ctx.unseal( + &pub_blob, + &priv_blob, + parent_handle, + &pcr_selection, + TpmAlgId::Sha256, + ) { + Ok(_) => println!("✗ Unseal unexpectedly succeeded after PCR change!"), + Err(_) => println!("✓ Unseal failed after PCR change (expected)"), } } From 585f10e45b885debc3a519b4036952197f61f40b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 23 Dec 2025 00:04:11 +0000 Subject: [PATCH 061/264] Remove unused field --- tpm2/src/device.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tpm2/src/device.rs b/tpm2/src/device.rs index dbb105f81..f57a7d1ff 100644 --- a/tpm2/src/device.rs +++ b/tpm2/src/device.rs @@ -85,7 +85,6 @@ impl TpmDevice { /// TPM command builder pub struct TpmCommand { buf: CommandBuffer, - has_sessions: bool, } impl TpmCommand { @@ -98,10 +97,7 @@ impl TpmCommand { buf.put_u32(0); // Size placeholder buf.put_u32(command_code.to_u32()); - Self { - buf, - has_sessions: false, - } + Self { buf } } /// Create a new command with sessions @@ -113,10 +109,7 @@ impl TpmCommand { buf.put_u32(0); // Size placeholder buf.put_u32(command_code.to_u32()); - Self { - buf, - has_sessions: true, - } + Self { buf } } /// Add a handle to the command From 687af3cfed832707e4c1776c8a862b1e5ace39f3 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 23 Dec 2025 06:48:53 +0000 Subject: [PATCH 062/264] tpm: Use scale codec to seal data --- Cargo.lock | 1 + tpm-attest/Cargo.toml | 3 +-- tpm-attest/src/esapi.rs | 7 +++++- tpm-attest/src/lib.rs | 51 ++++++++++++++++++--------------------- tpm2/src/bin/tpm2-test.rs | 2 +- 5 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7991ea67d..e03f83879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7628,6 +7628,7 @@ dependencies = [ "dstack-types", "fs-err", "hex", + "parity-scale-codec", "serde", "serde-human-bytes", "serde_json", diff --git a/tpm-attest/Cargo.toml b/tpm-attest/Cargo.toml index add86c812..931b5b631 100644 --- a/tpm-attest/Cargo.toml +++ b/tpm-attest/Cargo.toml @@ -21,6 +21,5 @@ tracing.workspace = true fs-err.workspace = true tpm-types.workspace = true dstack-types.workspace = true - -# Pure Rust TPM 2.0 implementation - no C library dependencies tpm2.workspace = true +scale.workspace = true diff --git a/tpm-attest/src/esapi.rs b/tpm-attest/src/esapi.rs index cfe7cdf98..7b5c41850 100644 --- a/tpm-attest/src/esapi.rs +++ b/tpm-attest/src/esapi.rs @@ -4,7 +4,7 @@ use crate::{PcrSelection, PcrValue}; use anyhow::{bail, Result}; -use tpm2::{TpmAlgId, TpmContext as RawTpmContext, TpmlPcrSelection}; +use tpm2::{TpmAlgId, TpmContext as RawTpmContext, TpmlPcrSelection, TpmsNvPublic}; pub struct EsapiContext { context: RawTpmContext, @@ -24,6 +24,11 @@ impl EsapiContext { self.context.nv_exists(index) } + /// Read NV public area (to determine the defined size, attributes, etc.) + pub fn nv_read_public(&mut self, index: u32) -> Result { + self.context.nv_read_public(index) + } + /// Read data from an NV index pub fn nv_read(&mut self, index: u32) -> Result>> { self.context.nv_read(index) diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs index 0b00a7582..10e179fdc 100644 --- a/tpm-attest/src/lib.rs +++ b/tpm-attest/src/lib.rs @@ -12,6 +12,7 @@ //! For quote verification, see the tpm-qvl crate. use anyhow::{bail, Context, Result}; +use scale::{Decode, Encode}; use std::path::Path; use tracing::{debug, warn}; @@ -56,9 +57,7 @@ impl SealedBlob { } pub fn from_parts(pub_data: &[u8], priv_data: &[u8]) -> Self { - let mut data = Vec::with_capacity(pub_data.len() + priv_data.len()); - data.extend_from_slice(pub_data); - data.extend_from_slice(priv_data); + let data = (pub_data, priv_data).encode(); Self { data } } @@ -66,22 +65,7 @@ impl SealedBlob { if self.data.len() < 4 { bail!("sealed blob too small"); } - - let pub_size = u16::from_be_bytes([self.data[0], self.data[1]]) as usize; - if self.data.len() < 2 + pub_size + 2 { - bail!("sealed blob truncated at pub"); - } - - let priv_offset = 2 + pub_size; - let priv_size = - u16::from_be_bytes([self.data[priv_offset], self.data[priv_offset + 1]]) as usize; - if self.data.len() < priv_offset + 2 + priv_size { - bail!("sealed blob truncated at priv"); - } - - let pub_data = self.data[..2 + pub_size].to_vec(); - let priv_data = self.data[priv_offset..priv_offset + 2 + priv_size].to_vec(); - + let (pub_data, priv_data) = Decode::decode(&mut &self.data[..])?; Ok((pub_data, priv_data)) } } @@ -196,8 +180,15 @@ impl TpmContext { let sealed_blob = SealedBlob::from_parts(&pub_bytes, &priv_bytes); - if !ctx.nv_exists(nv_index)? { - ctx.nv_define(nv_index, sealed_blob.data.len(), true)?; + let needed_size = sealed_blob.data.len(); + if ctx.nv_exists(nv_index)? { + let nv_public = ctx.nv_read_public(nv_index)?; + if (nv_public.data_size as usize) < needed_size { + ctx.nv_undefine(nv_index)?; + ctx.nv_define(nv_index, needed_size, true)?; + } + } else { + ctx.nv_define(nv_index, needed_size, true)?; } ctx.nv_write(nv_index, &sealed_blob.data)?; @@ -220,7 +211,14 @@ impl TpmContext { }; let sealed_blob = SealedBlob::new(sealed_data); - let (pub_bytes, priv_bytes) = sealed_blob.split()?; + let (pub_bytes, priv_bytes) = match sealed_blob.split() { + Ok(v) => v, + Err(e) => { + warn!("sealed blob in NV index 0x{nv_index:08x} is invalid ({e}); regenerating"); + let _ = ctx.nv_undefine(nv_index); + return Ok(None); + } + }; let data = ctx.unseal(&pub_bytes, &priv_bytes, parent_handle, pcr_selection)?; @@ -314,13 +312,10 @@ mod tests { #[test] fn test_sealed_blob_split() { - let pub_data = vec![0x00, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05]; - let priv_data = vec![0x00, 0x03, 0xAA, 0xBB, 0xCC]; - let mut blob_data = Vec::new(); - blob_data.extend_from_slice(&pub_data); - blob_data.extend_from_slice(&priv_data); + let pub_data = vec![0x01, 0x02, 0x03, 0x04, 0x05]; + let priv_data = vec![0xAA, 0xBB, 0xCC]; - let blob = SealedBlob::new(blob_data); + let blob = SealedBlob::from_parts(&pub_data, &priv_data); let (pub_part, priv_part) = blob.split().unwrap(); assert_eq!(pub_part, pub_data); diff --git a/tpm2/src/bin/tpm2-test.rs b/tpm2/src/bin/tpm2-test.rs index bd9ea309f..6afb2d827 100644 --- a/tpm2/src/bin/tpm2-test.rs +++ b/tpm2/src/bin/tpm2-test.rs @@ -59,7 +59,7 @@ fn main() { } _ => { eprintln!("Unknown command: {}", command); - eprintln!("Available commands: info, random, pcr-read, pcr-extend, nv-test, nv-full, primary, evict, seal, seal-pcr, quote, quote-ecc, all"); + eprintln!("Available commands: info, random, pcr-read, pcr-extend, nv-test, nv-full, primary, evict, seal, seal-pcr, seal-nv, quote, quote-ecc, all"); std::process::exit(1); } } From 805b5547371971faf098b74e7def711be52b7536 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Tue, 23 Dec 2025 08:20:08 +0000 Subject: [PATCH 063/264] Remove mod-tdx-guest --- LICENSES/GPL-2.0-only.txt | 117 -------------------- LICENSES/Linux-syscall-note.txt | 12 --- REUSE.toml | 5 - mod-tdx-guest/Kconfig | 11 -- mod-tdx-guest/Makefile | 22 ---- mod-tdx-guest/mod.c | 184 -------------------------------- mod-tdx-guest/tdcall.S | 162 ---------------------------- mod-tdx-guest/tdx-guest.h | 53 --------- mod-tdx-guest/tdx.h | 101 ------------------ 9 files changed, 667 deletions(-) delete mode 100644 LICENSES/GPL-2.0-only.txt delete mode 100644 LICENSES/Linux-syscall-note.txt delete mode 100644 mod-tdx-guest/Kconfig delete mode 100644 mod-tdx-guest/Makefile delete mode 100644 mod-tdx-guest/mod.c delete mode 100644 mod-tdx-guest/tdcall.S delete mode 100644 mod-tdx-guest/tdx-guest.h delete mode 100644 mod-tdx-guest/tdx.h diff --git a/LICENSES/GPL-2.0-only.txt b/LICENSES/GPL-2.0-only.txt deleted file mode 100644 index 17cb28643..000000000 --- a/LICENSES/GPL-2.0-only.txt +++ /dev/null @@ -1,117 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 2, June 1991 - -Copyright (C) 1989, 1991 Free Software Foundation, Inc. -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. - -To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. - -Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. - -Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - -0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. - -1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. - -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. - -2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. - - c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. - -3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. - -If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. - -4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. - -5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. - -6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. - -7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. - -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. - -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - -8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. - -9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. - -10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - -NO WARRANTY - -11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author - - This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. - -signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice diff --git a/LICENSES/Linux-syscall-note.txt b/LICENSES/Linux-syscall-note.txt deleted file mode 100644 index fcd056364..000000000 --- a/LICENSES/Linux-syscall-note.txt +++ /dev/null @@ -1,12 +0,0 @@ - NOTE! This copyright does *not* cover user programs that use kernel - services by normal system calls - this is merely considered normal use - of the kernel, and does *not* fall under the heading of "derived work". - Also note that the GPL below is copyrighted by the Free Software - Foundation, but the instance of code that it refers to (the Linux - kernel) is copyrighted by me and others who actually wrote it. - - Also note that the only valid version of the GPL as far as the kernel - is concerned is _this_ particular version of the license (ie v2, not - v2.2 or v3.x or whatever), unless explicitly otherwise stated. - - Linus Torvalds diff --git a/REUSE.toml b/REUSE.toml index 35055f15a..11596d72f 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -106,11 +106,6 @@ path = "kms/auth-eth/lib/forge-std/**" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "Apache-2.0" -[[annotations]] -path = "mod-tdx-guest/Kconfig" -SPDX-FileCopyrightText = "© 2022 Intel Corporation" -SPDX-License-Identifier = "GPL-2.0-only" - # Generated files [[annotations]] diff --git a/mod-tdx-guest/Kconfig b/mod-tdx-guest/Kconfig deleted file mode 100644 index 22dd59e19..000000000 --- a/mod-tdx-guest/Kconfig +++ /dev/null @@ -1,11 +0,0 @@ -config TDX_GUEST_DRIVER - tristate "TDX Guest driver" - depends on INTEL_TDX_GUEST - select TSM_REPORTS - help - The driver provides userspace interface to communicate with - the TDX module to request the TDX guest details like attestation - report. - - To compile this driver as module, choose M here. The module will - be called tdx-guest. diff --git a/mod-tdx-guest/Makefile b/mod-tdx-guest/Makefile deleted file mode 100644 index a9a17e211..000000000 --- a/mod-tdx-guest/Makefile +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# SPDX-FileCopyrightText: © 2022 Intel Corporation -# SPDX-FileCopyrightText: © 2024 Phala Network - -SRC := $(shell pwd) -KERNEL_SRC ?= /lib/modules/$(shell uname -r)/build -INSTALL_MOD_PATH := $(shell pwd)/dist/ - -obj-m += tdx-guest.o -tdx-guest-objs := tdcall.o mod.o - -all: - $(MAKE) -C $(KERNEL_SRC) M=$(SRC) - -modules_install: - $(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install - -clean: - $(MAKE) -C $(KERNEL_SRC) M=$(SRC) clean - -install: - make -C $(KDIR) M=$(PWD) modules_install INSTALL_MOD_PATH=$(INSTALL_MOD_PATH) diff --git a/mod-tdx-guest/mod.c b/mod-tdx-guest/mod.c deleted file mode 100644 index 45e04d316..000000000 --- a/mod-tdx-guest/mod.c +++ /dev/null @@ -1,184 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 Intel Corporation -// SPDX-FileCopyrightText: © 2024 Phala Network -// SPDX-License-Identifier: GPL-2.0-only -/* - * TDX guest user interface driver - * - * Copyright (C) 2022 Intel Corporation - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "tdx-guest.h" -#include "tdx.h" - -#define TDCALL_RETURN_CODE(a) ((a) >> 32) -#define TDCALL_INVALID_OPERAND 0xc0000100 - -#define TDREPORT_SUBTYPE_0 0 - -static int tdx_mcall_get_report0(u8 *reportdata, u8 *tdreport) -{ - struct tdx_module_args args = { - .rcx = virt_to_phys(tdreport), - .rdx = virt_to_phys(reportdata), - .r8 = TDREPORT_SUBTYPE_0, - }; - u64 ret; - - ret = __tdcall(TDG_MR_REPORT, &args); - if (ret) { - if (TDCALL_RETURN_CODE(ret) == TDCALL_INVALID_OPERAND) - return -EINVAL; - return -EIO; - } - - return 0; -} - -static long tdx_get_report0(struct tdx_report_req __user *req) -{ - u8 *reportdata, *tdreport; - long ret; - - reportdata = kmalloc(TDX_REPORTDATA_LEN, GFP_KERNEL); - if (!reportdata) - return -ENOMEM; - - tdreport = kzalloc(TDX_REPORT_LEN, GFP_KERNEL); - if (!tdreport) - { - ret = -ENOMEM; - goto out; - } - - if (copy_from_user(reportdata, req->reportdata, TDX_REPORTDATA_LEN)) - { - ret = -EFAULT; - goto out; - } - - /* Generate TDREPORT0 using "TDG.MR.REPORT" TDCALL */ - ret = tdx_mcall_get_report0(reportdata, tdreport); - if (ret) - goto out; - - if (copy_to_user(req->tdreport, tdreport, TDX_REPORT_LEN)) - ret = -EFAULT; - -out: - kfree(reportdata); - kfree(tdreport); - - return ret; -} - -static long tdx_extend_rtmr(struct tdx_extend_rtmr_req __user *req) -{ - u8 *data; - u8 index; - long ret; - - data = kmalloc(TDX_EXTEND_RTMR_DATA_LEN, GFP_KERNEL); - if (!data) - return -ENOMEM; - - if (copy_from_user(data, req->data, TDX_EXTEND_RTMR_DATA_LEN)) - { - ret = -EFAULT; - goto out; - } - - if (copy_from_user(&index, (u8 __user *)&req->index, 1)) - { - ret = -EFAULT; - goto out; - } - - { - struct tdx_module_args args = { - .rcx = virt_to_phys(data), - .rdx = index, - }; - - ret = __tdcall(TDG_MR_RTMR_EXTEND, &args); - } -out: - kfree(data); - return ret; -} - -static long tdx_guest_ioctl(struct file *file, unsigned int cmd, - unsigned long arg) -{ - switch (cmd) - { - case TDX_CMD_GET_REPORT0: - return tdx_get_report0((struct tdx_report_req __user *)arg); - case TDX_CMD_EXTEND_RTMR: - case TDX_CMD_EXTEND_RTMR2: - return tdx_extend_rtmr((struct tdx_extend_rtmr_req __user *)arg); - default: - return -ENOTTY; - } -} - -static const struct file_operations tdx_guest_fops = { - .owner = THIS_MODULE, - .unlocked_ioctl = tdx_guest_ioctl, - .llseek = noop_llseek, -}; - -static struct miscdevice tdx_misc_dev = { - .name = KBUILD_MODNAME, - .minor = MISC_DYNAMIC_MINOR, - .fops = &tdx_guest_fops, -}; - -static const struct x86_cpu_id tdx_guest_ids[] = { - X86_MATCH_FEATURE(X86_FEATURE_TDX_GUEST, NULL), - {} -}; -MODULE_DEVICE_TABLE(x86cpu, tdx_guest_ids); - -static int __init tdx_guest_init(void) -{ - int ret; - - if (!x86_match_cpu(tdx_guest_ids)) - return -ENODEV; - - ret = misc_register(&tdx_misc_dev); - if (ret) - return ret; - - - return 0; - - misc_deregister(&tdx_misc_dev); - - return ret; -} -module_init(tdx_guest_init); - -static void __exit tdx_guest_exit(void) -{ - misc_deregister(&tdx_misc_dev); -} -module_exit(tdx_guest_exit); - -MODULE_AUTHOR("Kuppuswamy Sathyanarayanan , kvinwang"); -MODULE_DESCRIPTION("TDX Guest Driver"); -MODULE_LICENSE("GPL"); diff --git a/mod-tdx-guest/tdcall.S b/mod-tdx-guest/tdcall.S deleted file mode 100644 index a98528695..000000000 --- a/mod-tdx-guest/tdcall.S +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (c) 2023, Intel Inc - * SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note - */ -#include -#include - -#include -#include - -#include -#include - -/* - * TDCALL and SEAMCALL are supported in Binutils >= 2.36. - */ -#define tdcall .byte 0x66,0x0f,0x01,0xcc -#define seamcall .byte 0x66,0x0f,0x01,0xcf - -/* - * TDX_MODULE_CALL - common helper macro for both - * TDCALL and SEAMCALL instructions. - * - * TDCALL - used by TDX guests to make requests to the - * TDX module and hypercalls to the VMM. - * SEAMCALL - used by TDX hosts to make requests to the - * TDX module. - * - *------------------------------------------------------------------------- - * TDCALL/SEAMCALL ABI: - *------------------------------------------------------------------------- - * Input Registers: - * - * RAX - TDCALL/SEAMCALL Leaf number. - * RCX,RDX,RDI,RSI,RBX,R8-R15 - TDCALL/SEAMCALL Leaf specific input registers. - * - * Output Registers: - * - * RAX - TDCALL/SEAMCALL instruction error code. - * RCX,RDX,RDI,RSI,RBX,R8-R15 - TDCALL/SEAMCALL Leaf specific output registers. - * - *------------------------------------------------------------------------- - * - * So while the common core (RAX,RCX,RDX,R8-R11) fits nicely in the - * callee-clobbered registers and even leaves RDI,RSI free to act as a - * base pointer, some leafs (e.g., VP.ENTER) make a giant mess of things. - * - * For simplicity, assume that anything that needs the callee-saved regs - * also tramples on RDI,RSI. This isn't strictly true, see for example - * TDH.EXPORT.MEM. - */ -.macro TDX_MODULE_CALL host:req ret=0 - FRAME_BEGIN - - /* Move Leaf ID to RAX */ - mov %rdi, %rax - - /* Move other input regs from 'struct tdx_module_args' */ - movq TDX_MODULE_rcx(%rsi), %rcx - movq TDX_MODULE_rdx(%rsi), %rdx - movq TDX_MODULE_r8(%rsi), %r8 - movq TDX_MODULE_r9(%rsi), %r9 - movq TDX_MODULE_r10(%rsi), %r10 - movq TDX_MODULE_r11(%rsi), %r11 - -.if \host -.Lseamcall\@: - seamcall - /* - * SEAMCALL instruction is essentially a VMExit from VMX root - * mode to SEAM VMX root mode. VMfailInvalid (CF=1) indicates - * that the targeted SEAM firmware is not loaded or disabled, - * or P-SEAMLDR is busy with another SEAMCALL. %rax is not - * changed in this case. - * - * Set %rax to TDX_SEAMCALL_VMFAILINVALID for VMfailInvalid. - * This value will never be used as actual SEAMCALL error code as - * it is from the Reserved status code class. - */ - jc .Lseamcall_vmfailinvalid\@ -.else - tdcall -.endif - -.if \ret - /* Copy output registers to the structure */ - movq %rcx, TDX_MODULE_rcx(%rsi) - movq %rdx, TDX_MODULE_rdx(%rsi) - movq %r8, TDX_MODULE_r8(%rsi) - movq %r9, TDX_MODULE_r9(%rsi) - movq %r10, TDX_MODULE_r10(%rsi) - movq %r11, TDX_MODULE_r11(%rsi) -.endif /* \ret */ - -.if \host -.Lout\@: -.endif - - FRAME_END - RET - -.if \host -.Lseamcall_vmfailinvalid\@: - mov $TDX_SEAMCALL_VMFAILINVALID, %rax - jmp .Lseamcall_fail\@ - -.Lseamcall_trap\@: - /* - * SEAMCALL caused #GP or #UD. By reaching here RAX contains - * the trap number. Convert the trap number to the TDX error - * code by setting TDX_SW_ERROR to the high 32-bits of RAX. - * - * Note cannot OR TDX_SW_ERROR directly to RAX as OR instruction - * only accepts 32-bit immediate at most. - */ - movq $TDX_SW_ERROR, %rdi - orq %rdi, %rax - -.Lseamcall_fail\@: - jmp .Lout\@ - - _ASM_EXTABLE_FAULT(.Lseamcall\@, .Lseamcall_trap\@) -.endif /* \host */ - -.endm - -.section .noinstr.text, "ax" - -/* - * __tdcall() - Used by TDX guests to request services from the TDX - * module (does not include VMM services) using TDCALL instruction. - * - * __tdcall() function ABI: - * - * @fn (RDI) - TDCALL Leaf ID, moved to RAX - * @args (RSI) - struct tdx_module_args for input - * - * Only RCX/RDX/R8-R11 are used as input registers. - * - * Return status of TDCALL via RAX. - */ -SYM_FUNC_START(__tdcall) - TDX_MODULE_CALL host=0 -SYM_FUNC_END(__tdcall) - -/* - * __tdcall_ret() - Used by TDX guests to request services from the TDX - * module (does not include VMM services) using TDCALL instruction, with - * saving output registers to the 'struct tdx_module_args' used as input. - * - * __tdcall_ret() function ABI: - * - * @fn (RDI) - TDCALL Leaf ID, moved to RAX - * @args (RSI) - struct tdx_module_args for input and output - * - * Only RCX/RDX/R8-R11 are used as input/output registers. - * - * Return status of TDCALL via RAX. - */ -SYM_FUNC_START(__tdcall_ret) - TDX_MODULE_CALL host=0 ret=1 -SYM_FUNC_END(__tdcall_ret) diff --git a/mod-tdx-guest/tdx-guest.h b/mod-tdx-guest/tdx-guest.h deleted file mode 100644 index 05b2cb07a..000000000 --- a/mod-tdx-guest/tdx-guest.h +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 Intel Corporation -// SPDX-FileCopyrightText: © 2024 Phala Network -// SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note -/* - * Userspace interface for TDX guest driver - * - * Copyright (C) 2022 Intel Corporation - */ - -#ifndef _UAPI_LINUX_TDX_GUEST_H_ -#define _UAPI_LINUX_TDX_GUEST_H_ - -#include -#include - -/* Length of the REPORTDATA used in TDG.MR.REPORT TDCALL */ -#define TDX_REPORTDATA_LEN 64 - -/* Length of TDREPORT used in TDG.MR.REPORT TDCALL */ -#define TDX_REPORT_LEN 1024 - -/** - * struct tdx_report_req - Request struct for TDX_CMD_GET_REPORT0 IOCTL. - * - * @reportdata: User buffer with REPORTDATA to be included into TDREPORT. - * Typically it can be some nonce provided by attestation - * service, so the generated TDREPORT can be uniquely verified. - * @tdreport: User buffer to store TDREPORT output from TDCALL[TDG.MR.REPORT]. - */ -struct tdx_report_req { - __u8 reportdata[TDX_REPORTDATA_LEN]; - __u8 tdreport[TDX_REPORT_LEN]; -}; - -/* - * TDX_CMD_GET_REPORT0 - Get TDREPORT0 (a.k.a. TDREPORT subtype 0) using - * TDCALL[TDG.MR.REPORT] - * - * Return 0 on success, -EIO on TDCALL execution failure, and - * standard errno on other general error cases. - */ -#define TDX_CMD_GET_REPORT0 _IOWR('T', 1, struct tdx_report_req) - -#define TDX_CMD_EXTEND_RTMR _IOR('T', 3, struct tdx_extend_rtmr_req) -#define TDX_CMD_EXTEND_RTMR2 _IOW('T', 3, struct tdx_extend_rtmr_req) -#define TDX_EXTEND_RTMR_DATA_LEN 48 -struct tdx_extend_rtmr_req -{ - u8 data[TDX_EXTEND_RTMR_DATA_LEN]; - u8 index; -}; - -#endif /* _UAPI_LINUX_TDX_GUEST_H_ */ diff --git a/mod-tdx-guest/tdx.h b/mod-tdx-guest/tdx.h deleted file mode 100644 index 05e8b97ea..000000000 --- a/mod-tdx-guest/tdx.h +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: © 2022 Intel Corporation -// SPDX-FileCopyrightText: © 2024 Phala Network -// SPDX-License-Identifier: GPL-2.0-only WITH Linux-syscall-note -#ifndef _ASM_X86_SHARED_TDX_H -#define _ASM_X86_SHARED_TDX_H - -#include -#include - -#define TDX_HYPERCALL_STANDARD 0 - -#define TDX_CPUID_LEAF_ID 0x21 -#define TDX_IDENT "IntelTDX " - -/* TDX module Call Leaf IDs */ -#define TDG_VP_VMCALL 0 -#define TDG_VP_INFO 1 -#define TDG_MR_RTMR_EXTEND 2 -#define TDG_VP_VEINFO_GET 3 -#define TDG_MR_REPORT 4 -#define TDG_MEM_PAGE_ACCEPT 6 -#define TDG_VM_WR 8 - -/* TDCS fields. To be used by TDG.VM.WR and TDG.VM.RD module calls */ -#define TDCS_NOTIFY_ENABLES 0x9100000000000010 - -/* TDX hypercall Leaf IDs */ -#define TDVMCALL_MAP_GPA 0x10001 -#define TDVMCALL_GET_QUOTE 0x10002 -#define TDVMCALL_REPORT_FATAL_ERROR 0x10003 - -#define TDVMCALL_STATUS_RETRY 1 - -/* - * Bitmasks of exposed registers (with VMM). - */ -#define TDX_RDX BIT(2) -#define TDX_RBX BIT(3) -#define TDX_RSI BIT(6) -#define TDX_RDI BIT(7) -#define TDX_R8 BIT(8) -#define TDX_R9 BIT(9) -#define TDX_R10 BIT(10) -#define TDX_R11 BIT(11) -#define TDX_R12 BIT(12) -#define TDX_R13 BIT(13) -#define TDX_R14 BIT(14) -#define TDX_R15 BIT(15) - -/* - * These registers are clobbered to hold arguments for each - * TDVMCALL. They are safe to expose to the VMM. - * Each bit in this mask represents a register ID. Bit field - * details can be found in TDX GHCI specification, section - * titled "TDCALL [TDG.VP.VMCALL] leaf". - */ -#define TDVMCALL_EXPOSE_REGS_MASK \ - (TDX_RDX | TDX_RBX | TDX_RSI | TDX_RDI | TDX_R8 | TDX_R9 | \ - TDX_R10 | TDX_R11 | TDX_R12 | TDX_R13 | TDX_R14 | TDX_R15) - -/* TDX supported page sizes from the TDX module ABI. */ -#define TDX_PS_4K 0 -#define TDX_PS_2M 1 -#define TDX_PS_1G 2 -#define TDX_PS_NR (TDX_PS_1G + 1) - -#ifndef __ASSEMBLY__ - -#include - -/* - * Used in __tdcall*() to gather the input/output registers' values of the - * TDCALL instruction when requesting services from the TDX module. This is a - * software only structure and not part of the TDX module/VMM ABI - */ -struct tdx_module_args { - /* callee-clobbered */ - u64 rcx; - u64 rdx; - u64 r8; - u64 r9; - /* extra callee-clobbered */ - u64 r10; - u64 r11; - /* callee-saved + rdi/rsi */ - u64 r12; - u64 r13; - u64 r14; - u64 r15; - u64 rbx; - u64 rdi; - u64 rsi; -}; - -/* Used to communicate with the TDX module */ -u64 __tdcall(u64 fn, struct tdx_module_args *args); -u64 __tdcall_ret(u64 fn, struct tdx_module_args *args); -u64 __tdcall_saved_ret(u64 fn, struct tdx_module_args *args); - -#endif /* !__ASSEMBLY__ */ -#endif /* _ASM_X86_SHARED_TDX_H */ From b4a94a821ae804ec9e07b7135a1a8c8f2519458e Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 24 Dec 2025 00:39:28 +0000 Subject: [PATCH 064/264] optional host_api_url --- dstack-types/src/lib.rs | 2 +- dstack-util/src/host_api.rs | 27 ++++++++++++++------------- guest-agent/src/guest_api_service.rs | 5 ++++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 282172caa..00eca611e 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -134,7 +134,7 @@ pub struct SysConfig { pub gateway_urls: Vec, pub pccs_url: Option, pub docker_registry: Option, - pub host_api_url: String, + pub host_api_url: Option, // JSON serialized VmConfig pub vm_config: String, } diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index 5577e4cec..95eb59d73 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -22,34 +22,31 @@ pub(crate) struct KeyProvision { } pub(crate) struct HostApi { - client: DefaultClient, + client: Option, pccs_url: Option, } impl Default for HostApi { fn default() -> Self { - Self::new("".into(), None) + Self::new(None, None) } } impl HostApi { - pub fn new(base_url: String, pccs_url: Option) -> Self { + pub fn new(base_url: Option, pccs_url: Option) -> Self { Self { - client: new_client(base_url), + client: base_url.map(new_client), pccs_url, } } pub fn load_or_default(url: Option) -> Result { let api = match url { - Some(url) => Self::new(url, None), + Some(url) => Self::new(Some(url), None), None => { let local_config: SysConfig = deserialize_json_file(format!("{HOST_SHARED_DIR}/{SYS_CONFIG}"))?; - Self::new( - local_config.host_api_url.clone(), - local_config.pccs_url.clone(), - ) + Self::new(local_config.host_api_url, local_config.pccs_url) } }; Ok(api) @@ -63,7 +60,10 @@ impl HostApi { return Ok(()); } } - self.client + let Some(client) = &self.client else { + return Ok(()); + }; + client .notify(Notification { event: event.to_string(), payload: payload.to_string(), @@ -83,9 +83,10 @@ impl HostApi { let mut report_data = [0u8; 64]; report_data[..PUBLICKEYBYTES].copy_from_slice(pk.as_bytes()); let quote = tdx_attest::get_quote(&report_data).context("Failed to get quote")?; - - let provision = self - .client + let Some(client) = &self.client else { + return Err(anyhow!("Host API client not initialized")); + }; + let provision = client .get_sealing_key(host_api::GetSealingKeyRequest { quote: quote.to_vec(), }) diff --git a/guest-agent/src/guest_api_service.rs b/guest-agent/src/guest_api_service.rs index 1376cb25f..2f905778e 100644 --- a/guest-agent/src/guest_api_service.rs +++ b/guest-agent/src/guest_api_service.rs @@ -208,7 +208,10 @@ pub async fn notify_host(event: &str, payload: &str) -> Result<()> { let local_config: SysConfig = serde_json::from_str(&fs::read_to_string(format!( "{HOST_SHARED_DIR}/{SYS_CONFIG}" ))?)?; - let nc = host_api::client::new_client(local_config.host_api_url); + let Some(host_api_url) = local_config.host_api_url else { + anyhow::bail!("host_api_url not configured"); + }; + let nc = host_api::client::new_client(host_api_url); nc.notify(Notification { event: event.to_string(), payload: payload.to_string(), From 8d2e2c5a8ea6a2f307e19d0eb48af3c97b741260 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 24 Dec 2025 03:39:12 +0000 Subject: [PATCH 065/264] verifier: Allow missing cpus and mem in vm_config for GCPP --- dstack-types/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 00eca611e..a2dc10477 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -144,7 +144,9 @@ pub struct VmConfig { pub spec_version: u32, #[serde(with = "hex_bytes")] pub os_image_hash: Vec, + #[serde(default)] pub cpu_count: u32, + #[serde(default)] pub memory_size: u64, // https://github.com/intel-staging/qemu-tdx/issues/1 #[serde(default, skip_serializing_if = "Option::is_none")] From f1a49f01a74c03c2034587dcf624bfc9c918ef20 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 24 Dec 2025 04:07:55 +0000 Subject: [PATCH 066/264] Update path of user-config in docs --- docs/security-guide/cvm-boundaries.md | 2 +- docs/vmm-cli-user-guide.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/security-guide/cvm-boundaries.md b/docs/security-guide/cvm-boundaries.md index eb0d71b1a..b658ffae1 100644 --- a/docs/security-guide/cvm-boundaries.md +++ b/docs/security-guide/cvm-boundaries.md @@ -122,7 +122,7 @@ dstack uses encrypted environment variables to allow app developers to securely 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. ### .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. +This is an optional application-specific configuration file that applications inside the CVM can access. dstack OS simply stores it at /dstack/.host-shared/.user-config without any measurement or additional processing. Application developers should perform integrity checks on user_config at the application layer if necessary. diff --git a/docs/vmm-cli-user-guide.md b/docs/vmm-cli-user-guide.md index 242d024b2..5befa43aa 100644 --- a/docs/vmm-cli-user-guide.md +++ b/docs/vmm-cli-user-guide.md @@ -250,7 +250,7 @@ Deploy your application with the compose file: - `--gpu`: GPU assignments - `--ppcie`: Enable PPCIE mode (attach ALL available GPUs and NVSwitches) - `--env-file`: Environment variables file -- `--user-config`: Path to user config file (will be placed at `/dstack/.user-config` in the CVM) +- `--user-config`: Path to user config file (will be placed at `/dstack/.host-shared/.user-config` in the CVM) - `--kms-url`: KMS server URL - `--gateway-url`: Gateway server URL - `--stopped`: Create VM in stopped state (requires dstack-vmm >= 0.5.4) From 448e606cb970231d56a4dfed211ef37f827b5e55 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 24 Dec 2025 09:20:12 +0000 Subject: [PATCH 067/264] Update simulator configs --- guest-agent/dstack.toml | 3 --- guest-agent/src/config.rs | 2 -- guest-agent/src/rpc_service.rs | 24 +++++++++++++----------- sdk/go/dstack/client_test.go | 2 +- sdk/run-tests.sh | 1 + sdk/simulator/app-compose.json | 16 +--------------- sdk/simulator/appkeys.json | 20 ++++++++++---------- sdk/simulator/attestation.bin | Bin 39090 -> 39090 bytes sdk/simulator/dstack.toml | 2 -- sdk/simulator/eventlog.json | 1 - sdk/simulator/quote.hex | 1 - 11 files changed, 26 insertions(+), 46 deletions(-) delete mode 100644 sdk/simulator/eventlog.json delete mode 100644 sdk/simulator/quote.hex diff --git a/guest-agent/dstack.toml b/guest-agent/dstack.toml index 7b6e52846..63b71bc9f 100644 --- a/guest-agent/dstack.toml +++ b/guest-agent/dstack.toml @@ -18,10 +18,7 @@ data_disks = ["/"] [default.core.simulator] enabled = false -quote_file = "quote.hex" attestation_file = "attestation.bin" -event_log_file = "eventlog.json" -sys_config_file = "sys-config.json" [internal-v0] address = "unix:/var/run/tappd.sock" diff --git a/guest-agent/src/config.rs b/guest-agent/src/config.rs index c13438f65..6276a4daa 100644 --- a/guest-agent/src/config.rs +++ b/guest-agent/src/config.rs @@ -71,7 +71,5 @@ where #[derive(Debug, Clone, Deserialize)] pub struct Simulator { pub enabled: bool, - pub quote_file: String, - pub event_log_file: String, pub attestation_file: String, } diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 275d5216f..762258382 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -447,18 +447,20 @@ fn simulate_quote( report_data: [u8; 64], vm_config: &str, ) -> Result { - let quote_file = - fs::read_to_string(&config.simulator.quote_file).context("Failed to read quote file")?; - let mut quote = hex::decode(quote_file.trim()).context("Failed to decode quote")?; - let event_log = fs::read_to_string(&config.simulator.event_log_file) - .context("Failed to read event log file")?; - if quote.len() < 632 { - return Err(anyhow::anyhow!("Quote is too short")); - } - quote[568..632].copy_from_slice(&report_data); + let attestation_bytes = fs::read(&config.simulator.attestation_file) + .context("Failed to read simulator attestation file")?; + let VersionedAttestation::V0 { attestation } = + VersionedAttestation::from_scale(&attestation_bytes) + .context("Failed to decode simulator attestation")?; + let Some(mut quote) = attestation.tdx_quote else { + return Err(anyhow::anyhow!("Quote not found")); + }; + + quote.quote[568..632].copy_from_slice(&report_data); Ok(GetQuoteResponse { - quote, - event_log, + quote: quote.quote, + event_log: serde_json::to_string("e.event_log) + .context("Failed to serialize event log")?, report_data: report_data.to_vec(), vm_config: vm_config.to_string(), }) diff --git a/sdk/go/dstack/client_test.go b/sdk/go/dstack/client_test.go index a6b3ae375..477cd0314 100644 --- a/sdk/go/dstack/client_test.go +++ b/sdk/go/dstack/client_test.go @@ -449,7 +449,7 @@ func TestInfo(t *testing.T) { } func TestGetKeySignatureVerification(t *testing.T) { - expectedAppPubkey, _ := hex.DecodeString("02b85cceca0c02d878f0ebcda72a97469a472416eb6faf3c4807642132f9786810") + expectedAppPubkey, _ := hex.DecodeString("02818494263695e8839122dbd88e281d7380622999df4e60a14befa0f2d096fc7c") expectedKmsPubkey, _ := hex.DecodeString("02cad3a8bb11c5c0858fb3e402048b5137457039d577986daade678ed4b4ab1b9b") client := dstack.NewDstackClient() diff --git a/sdk/run-tests.sh b/sdk/run-tests.sh index 74a64475c..00ebcd7d7 100755 --- a/sdk/run-tests.sh +++ b/sdk/run-tests.sh @@ -25,6 +25,7 @@ cargo test -p dstack-sdk-types --test no_std_test --no-default-features popd pushd go/ +go clean -testcache go test -v ./dstack DSTACK_SIMULATOR_ENDPOINT=$TAPPD_SIMULATOR_ENDPOINT go test -v ./tappd popd diff --git a/sdk/simulator/app-compose.json b/sdk/simulator/app-compose.json index c5f5ff8c0..bcbba37d3 100644 --- a/sdk/simulator/app-compose.json +++ b/sdk/simulator/app-compose.json @@ -1,15 +1 @@ -{ - "manifest_version": 2, - "name": "kvin-nb", - "runner": "docker-compose", - "docker_compose_file": "services:\n jupyter:\n image: quay.io/jupyter/base-notebook\n user: root\n environment:\n - GRANT_SUDO=yes\n ports:\n - \"8888:8888\"\n volumes:\n - /:/host/\n - /var/run/tappd.sock:/var/run/tappd.sock\n - /var/run/dstack.sock:/var/run/dstack.sock\n logging:\n driver: journald\n options:\n tag: jupyter-notebook\n", - "docker_config": {}, - "kms_enabled": true, - "tproxy_enabled": true, - "public_logs": true, - "public_sysinfo": true, - "public_tcbinfo": false, - "local_key_provider_enabled": false, - "allowed_envs": [], - "no_instance_id": false -} +{"manifest_version":2,"name":"guest-agent","runner":"docker-compose","docker_compose_file":"services:\n dstack-agent:\n image: ubuntu\n user: root\n network_mode: host\n volumes:\n - /:/host/\n - /var/run/tappd.sock:/var/run/tappd.sock\n - /var/run/dstack.sock:/var/run/dstack.sock\n entrypoint: |\n bash -c '\n apt-get update && apt-get install -y socat\n socat TCP-LISTEN:2000,fork UNIX-CONNECT:/var/run/tappd.sock &\n socat TCP-LISTEN:3000,fork UNIX-CONNECT:/var/run/dstack.sock &\n tail -f /dev/null\n '\n dstack-verifier:\n image: dstacktee/dstack-verifier:0.5.4\n ports:\n - \"8080:8080\"\n restart: unless-stopped","gateway_enabled":true,"public_logs":true,"public_sysinfo":true,"public_tcbinfo":true,"key_provider_id":"","allowed_envs":[],"no_instance_id":false,"secure_time":false,"key_provider":"kms","kms_enabled":true,"storage_fs":"ext4","pre_launch_script":"docker run --rm --privileged --pid=host --net=host -v /:/host \\\n -e SSH_GITHUB_USER=\"kvinwang\" \\\n kvin/dstack-openssh-installer:latest"} \ No newline at end of file diff --git a/sdk/simulator/appkeys.json b/sdk/simulator/appkeys.json index aa5d44c10..1e67f019d 100644 --- a/sdk/simulator/appkeys.json +++ b/sdk/simulator/appkeys.json @@ -1,13 +1,13 @@ { - "disk_crypt_key": "1122e1f340c19407adc5ec531ac98d72bcf702bf7858f6fa49b5be79b61e4d5b", - "env_crypt_key": "ca1a3895d9d613287fc14034d0ec60abb5089896e7c8fd7c2f02bd91fa0076aa", - "k256_key": "e0e5d254fb944dcc370a2e5288b336a1e809871545a73ee645368957fefa31f9", - "k256_signature": "2f431c7956869a4fe3e028c5f9518a935e2d01e81a3628f8b1d178fc2fac7b6d2405ace433624e5568e23c4ed291dbaf60dac79b756837c0fe745154ebfdc0a601", - "gateway_app_id": "any", - "ca_cert": "-----BEGIN CERTIFICATE-----\nMIIBmTCCAUCgAwIBAgIUU7801+krCs2OpIdne3t6OWrJ2fMwCgYIKoZIzj0EAwIw\nKTEPMA0GA1UECgwGRHN0YWNrMRYwFAYDVQQDDA1Ec3RhY2sgS01TIENBMB4XDTc1\nMDEwMTAwMDAwMFoXDTM1MDMxNzA5NDQ0MlowKTEPMA0GA1UECgwGRHN0YWNrMRYw\nFAYDVQQDDA1Ec3RhY2sgS01TIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\nGbJFfdm4qmRG2YDxNv/3gS7NbHd0DusOKLENVsDAACiltuWdzqMH1YO9H3B2npwR\nbfK8+xdYqV2GE+feHISCwKNGMEQwDwYDVR0PAQH/BAUDAweAADAdBgNVHQ4EFgQU\nevjJ+VZPvDxHJ2ejjeIaUYMMcEcwEgYDVR0TAQH/BAgwBgEB/wIBATAKBggqhkjO\nPQQDAgNHADBEAiAhQHQNbmyvx9BDBXRjW1eCkPCpFs/2Vt/nvbi+M69FPAIgQ13F\n3pmxicxyFeVW2iOjrbG1cxLdT9Kh+9ICF9zn8kA=\n-----END CERTIFICATE-----\n", - "key_provider": { - "None": { - "key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1PYCFKYfDmUfv5fk\nstppasf4mPGqnz0fEoLEnGx8CnKhRANCAAQZskV92biqZEbZgPE2//eBLs1sd3QO\n6w4osQ1WwMAAKKW25Z3OowfVg70fcHaenBFt8rz7F1ipXYYT594chILA\n-----END PRIVATE KEY-----\n" + "disk_crypt_key": "2cbc10ccbed084b91af2ceff8400e6082402367f18a2c6248bac17d2fc951607", + "env_crypt_key": "4f3cf0a19a0444674c8e51222afd395b8df9fad2ba3cd7956f640a4b3c046db6", + "k256_key": "d6e88992cdeeee35fe70b5db61ab66cdb191fb9b6ec9313757ef162dd7214d5d", + "k256_signature": "9e618603e72d01fedb82deff6daf2d62a572becf0059eec3f89c1ab40e1f2e594d2a283f843f34e8f39e4cc49a612496ce67223a12ac923f8efe330346dfc6c500", + "gateway_app_id": "any", + "ca_cert": "-----BEGIN CERTIFICATE-----\nMIIBmzCCAUCgAwIBAgIUU7801+krCs2OpIdne3t6OWrJ2fMwCgYIKoZIzj0EAwIw\nKTEPMA0GA1UECgwGRHN0YWNrMRYwFAYDVQQDDA1Ec3RhY2sgS01TIENBMB4XDTc1\nMDEwMTAwMDAwMFoXDTM1MTIxOTAyNTEzOVowKTEPMA0GA1UECgwGRHN0YWNrMRYw\nFAYDVQQDDA1Ec3RhY2sgS01TIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\nGbJFfdm4qmRG2YDxNv/3gS7NbHd0DusOKLENVsDAACiltuWdzqMH1YO9H3B2npwR\nbfK8+xdYqV2GE+feHISCwKNGMEQwDwYDVR0PAQH/BAUDAweGADAdBgNVHQ4EFgQU\nevjJ+VZPvDxHJ2ejjeIaUYMMcEcwEgYDVR0TAQH/BAgwBgEB/wIBATAKBggqhkjO\nPQQDAgNJADBGAiEAvAYUOGbU5QC23zzQtJqm7/hGzVK5SlI0P7yGDii+/4ACIQCN\nbKkagb0uncr6sUKlhKrpHhID+WWTvqJj0TrkvbVdCg==\n-----END CERTIFICATE-----\n", + "key_provider": { + "None": { + "key": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg1PYCFKYfDmUfv5fk\nstppasf4mPGqnz0fEoLEnGx8CnKhRANCAAQZskV92biqZEbZgPE2//eBLs1sd3QO\n6w4osQ1WwMAAKKW25Z3OowfVg70fcHaenBFt8rz7F1ipXYYT594chILA\n-----END PRIVATE KEY-----\n" + } } - } } \ No newline at end of file diff --git a/sdk/simulator/attestation.bin b/sdk/simulator/attestation.bin index e757b3da4d3275485806bba237e737c717809c87..2f7a9c35efad571c2e964cf40f8b48d87d345d51 100644 GIT binary patch delta 2607 zcmZWpcTm&W7Eb6S6j=dLT0kuHRFa5|ED%Z(2uWyynuH{j#1LwTU_eDcSOI_VEZ7&+ zN0TZ~sjdnEX`&!DT#<+@O$0$B!YXfP-XHIqIdjkV-P7jYnNw4(R8y@~RRKPDhI=uo zjQ!i*F57MoQc-r%xqFZLXB3B=BjpM&YwIJp>atLoCh=YC>FEw(e^pqlpwv!)2!Ey&#re|+qlhXIdk;#z*?RfK@ zE&FH6R?6p=`(G?xK3ocGO7M*+dCWbzxJ!$)XWdS*EbIB866J8PDRh`G8SE137$| z0y|W1o4H8H^dN)}-31;H&Zdwho{411|1>~Htu2u3D2C9e9Dgx9iikqSNWvL-CmvDg zf?}`)v^alnuD=@}kx20o(ntw34BUe##G}30y!hB~cUCmrgAq(gz(L?)Vk8BRbB`1w z*hF+RA~-?-Y*l}5LW&3$x;Xj=3nU2vFKA*h}_4@QtXE>c9Jaz!Evn&br` z#=8sP33M?dKERzTAO$iB?p}OaEIWV&uWf^RiKHs5ILmtRk_GqIVU8Z46BZAAF7 zS;G^Qrex$xxVu*4>_y0BMpN)zjK zf4h>dX%v3{ao?+W8Qk{h6U9pHln_{zeu3+y0Q$RAr2lG6X@0dNDx(}v*b;l~Qzk?O z_Wa_D(sdspZp}Js=~#2e0zN6RqADJEk!lLKrg_7Mc(N;0|9n=Ly{zCbQy<-w$povd zh0oHQr*(%rI0lci{$OdL_b6x~3(QS!S|!5T)--4EF16}R9M`#O*ltKI_W{U<{F}#! zksvRjd+2kJYftfq%fYHhg2S;*(756gMYTxYHPD}+eg%2$49XZmZtXcCyKjJEDG+c#^}>Ta3KhWQGPftHyJ;Sh}1!0 zcK?1mH|YCQqN4s~$9i<}v)k;eKy@Y_rk?3d={o$VF@$JRlsdQanx`DmbitGR{mX|q z?+5j*4**Ek?)A6PE#pU^D!jcFnN8}KPVvdvQ^3Bgt42r!0)<681smxi;TQ}8iNLH0 z7(oBzfkP-X8jZuE5jZ#sgK|P*;Yc(*2)jpqJjSGy&8_a*ZUjBfIPq`$6d;1 zTIb^DPI;VXTqP)TC$x5cdKf;~M~IT)H(!#3-Uz1JoZ2wv46$EsfA1OO+~{VrF8oZF z!@lt^f!ObPuC3{x`_^6Mtam#_XX+n3b7BiFwbDQN&6Y^H9_qm`s|W*M_<)Av4%<6n zdgOQU4jCR-hDoMXO^RK{)kBVEzOSSwFSoRp8}^iE44lE}ox#SCq}u#h#8N%(a-sc7 zCt~b^@dXrpn7>{1Bx--{kmkRG2aA)7s*e?9}xYd#qJ`j;{NrrcP?B2`#jyZ@9_ z+3l25f|iEfc<1(dmy6Rc|H!OQd~eo%{DjboNlFC)#L=I-J+*lKdr#AEw}Zaw$MuA} zZtLvW+5P6C+zc5YnkV!y}#+jC}-KO@FRi3(8lfDYj)8 iNt8)Bl-Aj%U+dboa;8ECB8o9Dkn8&^*|p>Xj>_MF&SUKW delta 2607 zcmZWpc{J2(AD^+!% zAL}1>S~^KcsV5(t5XR>sur=Te-Ph8%Zql)y#e4Nz5T^|oWiC_9BE*dvAUJSS=_Dvrg# z*)u~uy-o05Yyygr#BoA%JeY0{B#Ma(UC8zEjPVde;=`jHJb1_$N9REILo8)ipHLHm zIoXRBz$XexCcIFN7hnV#)Mf;c66qn1LbMAzK8PNMjCP`<=?R|7flRU+pangm5zXh| zi2+`2TnDZ@+RmG5hmXVa$kIG0O>QTHkMVI}as?rF9AyV8pX3}G5pNfs0L&^U`H-E0 zXtqAY07{rG6NzR-*t&DWaY&a4Vl0;6&2s13+WKGw-a>vzB%SNXb*I`f%tPp&hohM+ zb8{X}In;$IjHV_yqTC29q$7rX$Q(-y4WlEYh#u0`U+6I+J!V%hT)TyQoAaXn)`enR z24y)mrq<#yS@-xNG17F$@JyxB=NIYugu;kD8evavfC>~T4vug)TX!Z11P1wmKp@Xa zdCl+cJNqxzZ>wws`ak*bPd-|-)&G221ywp@DE9(nRVDX5PyA7n_R~5`%isPGe*h6{ z+_rnEN3GEbRQ<&I$pP%utt~t+GfEoE>XN3_Nea-G60Jo@k7MeiC+L!k8Y1F0K^zDQ z-DqCFrUE#p9<^5LExfnl*4GJ{IMEZ`-Cz>$UM(G~T-)O@$fWss^ttJzeKbQ&IJ!1$ zS?Ql|SWE|{%fU`l8#iXSl>k$zu3P_aFIAL<3}bDJ_vNp>hDN8hI~CfLr9G)8+CB!(1^oG$HfU|qC?%%WtllKPgL9Iw7cBA1q@r&^6$5%0jR4Xm4gLb9%UH5qOiP1CZ05kom{`&Z)ICSq($43V_mA7|^ZZ zyk`;YI2W}v6SiSZ?eDR9#r$NNmj~7m4AMlbNn6qnfUR`?R%n}A?uoG=ASGvtSM+$! zErP^?DjL=lnrK>c*~RPk#Fd6mabxX~w?NC$+8AYV=?kVUHdd z1Oxp7g6qn56qJawade*k>5&%;ni42Fck`M|cIkvJR*jlymS zSYW&47!O4vVNf_Mj)W%>i6{bzfWjeJNOdKMaVG7#l^j?Jx)I2OA@X)01&&Sgy!3TX zqaw(wbokF3UCz%{46cluLkvVe7@`3fI$% z$iz6H6d04BaEErVb9JgR&EJ)ai9?9=$ZIc7M>}0x1A^dCCT=Rikz4_DsJ#0LyU@ z#dLNJ=|QsU*fg8n7oU5gSupmsiIaueh*`vGi2Jb$5ks%iqAKZ0_l1uQ#QE~bEjV*U z3(DiBZ5f%n4nZzYsED;Bu7*6spl7L1^yPbqYx$wmDj{bY-+y!eO<{VqUvjtYqYstl*h1pqOS%_)BRk+lC(0{IvJ$}D# z*xk{BXCIzaHgC3c0y{HOo@PC|{~ zv+3IW%^g9np4R2PuPV+MoXsq76OCuN;)jmUh97wUV&6cLn6BQ`_jm15s?B

CHBE z`IA&QWva`R;XarG4oz} zG*2W?v3_?BsyAaB2BMhUlv#exCQfQN?-$8F2|iYA0yTwKT;k2Iz+47`@BVx3 zuO-CjRv@BK08g&j?Dyz^U$f@*EV3*W2I^c|=tg%wO^mxkf8U7>;}rtN@IS{%5!q`~ zenjVwo7zvT&g_+iHY!D5IAL@_)_b`_@(iE@rD3Of|GK>syvg8RlsQ+xTJzEuFtO5DrJh1Q62PxxBu)==;13zz{ diff --git a/sdk/simulator/dstack.toml b/sdk/simulator/dstack.toml index a8c5b4bd8..ecf4a8e40 100644 --- a/sdk/simulator/dstack.toml +++ b/sdk/simulator/dstack.toml @@ -17,8 +17,6 @@ sys_config_file = "sys-config.json" [default.core.simulator] enabled = true -quote_file = "quote.hex" -event_log_file = "eventlog.json" attestation_file = "attestation.bin" [internal-v0] diff --git a/sdk/simulator/eventlog.json b/sdk/simulator/eventlog.json deleted file mode 100644 index 7036e3ece..000000000 --- a/sdk/simulator/eventlog.json +++ /dev/null @@ -1 +0,0 @@ -[{"imr":0,"event_type":2147483659,"digest":"0e35f1b315ba6c912cf791e5c79dd9d3a2b8704516aa27d4e5aa78fb09ede04aef2bbd02ac7a8734c48562b9c26ba35d","event":"","event_payload":"095464785461626c65000100000000000000af96bb93f2b9b84e9462e0ba745642360090800000000000"},{"imr":0,"event_type":2147483658,"digest":"344bc51c980ba621aaa00da3ed7436f7d6e549197dfe699515dfa2c6583d95e6412af21c097d473155875ffd561d6790","event":"","event_payload":"2946762858585858585858582d585858582d585858582d585858582d58585858585858585858585829000000c0ff000000000040080000000000"},{"imr":0,"event_type":2147483649,"digest":"9dc3a1f80bcec915391dcda5ffbb15e7419f77eab462bbf72b42166fb70d50325e37b36f93537a863769bcf9bedae6fb","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c0a00000000000000000000000000000053006500630075007200650042006f006f007400"},{"imr":0,"event_type":2147483649,"digest":"6f2e3cbc14f9def86980f5f66fd85e99d63e69a73014ed8a5633ce56eca5b64b692108c56110e22acadcef58c3250f1b","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c0200000000000000000000000000000050004b00"},{"imr":0,"event_type":2147483649,"digest":"d607c0efb41c0d757d69bca0615c3a9ac0b1db06c557d992e906c6b7dee40e0e031640c7bfd7bcd35844ef9edeadc6f9","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c030000000000000000000000000000004b0045004b00"},{"imr":0,"event_type":2147483649,"digest":"08a74f8963b337acb6c93682f934496373679dd26af1089cb4eaf0c30cf260a12e814856385ab8843e56a9acea19e127","event":"","event_payload":"cbb219d73a3d9645a3bcdad00e67656f0200000000000000000000000000000064006200"},{"imr":0,"event_type":2147483649,"digest":"18cc6e01f0c6ea99aa23f8a280423e94ad81d96d0aeb5180504fc0f7a40cb3619dd39bd6a95ec1680a86ed6ab0f9828d","event":"","event_payload":"cbb219d73a3d9645a3bcdad00e67656f03000000000000000000000000000000640062007800"},{"imr":0,"event_type":4,"digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0","event":"","event_payload":"00000000"},{"imr":0,"event_type":10,"digest":"68cd79315e70aecd4afe7c1b23a5ed7b3b8e51a477e1739f111b3156def86bbc56ebf239dcd4591bc7a9fff90023f481","event":"","event_payload":"414350492044415441"},{"imr":0,"event_type":10,"digest":"6bc203b3843388cc4918459c3f5c6d1300a796fb594781b7ecfaa3ae7456975f095bfcc1156c9f2d25e8b8bc1b520f66","event":"","event_payload":"414350492044415441"},{"imr":0,"event_type":10,"digest":"ec9e8622a100c399d71062a945f95d8e4cdb7294e8b1c6d17a6a8d37b5084444000a78b007ef533f290243421256d25c","event":"","event_payload":"414350492044415441"},{"imr":1,"event_type":2147483651,"digest":"0db5964580e727672734da95797318d8455ab74b3e3d66fbb1aaa4ddd01a3f8555f4889e57c19a15c165594e31678dc0","event":"","event_payload":"18a0447b0000000000b4b2000000000000000000000000002a000000000000000403140072f728144ab61e44b8c39ebdd7f893c7040412006b00650072006e0065006c0000007fff0400"},{"imr":0,"event_type":2147483650,"digest":"1dd6f7b457ad880d840d41c961283bab688e94e4b59359ea45686581e90feccea3c624b1226113f824f315eb60ae0a7c","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c0900000000000000020000000000000042006f006f0074004f0072006400650072000000"},{"imr":0,"event_type":2147483650,"digest":"23ada07f5261f12f34a0bd8e46760962d6b4d576a416f1fea1c64bc656b1d28eacf7047ae6e967c58fd2a98bfa74c298","event":"","event_payload":"61dfe48bca93d211aa0d00e098032b8c08000000000000003e0000000000000042006f006f0074003000300030003000090100002c0055006900410070007000000004071400c9bdb87cebf8344faaea3ee4af6516a10406140021aa2c4614760345836e8ab6f46623317fff0400"},{"imr":1,"event_type":2147483655,"digest":"77a0dab2312b4e1e57a84d865a21e5b2ee8d677a21012ada819d0a98988078d3d740f6346bfe0abaa938ca20439a8d71","event":"","event_payload":"43616c6c696e6720454649204170706c69636174696f6e2066726f6d20426f6f74204f7074696f6e"},{"imr":1,"event_type":4,"digest":"394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0","event":"","event_payload":"00000000"},{"imr":2,"event_type":6,"digest":"ad49ca7e80258d7580c5c580cd21ada7ecbf418dde5197d6f8c835493ceb6edec0f8954b733bd9b889f96f33e5f9cb05","event":"","event_payload":"ed223b8f1a0000004c4f414445445f494d4147453a3a4c6f61644f7074696f6e7300"},{"imr":2,"event_type":6,"digest":"e0cdb72fbba75a0f4d396c0b80a4336db049b383a9730467160dec0b7059cb22aca87639dcc655d935d6c6356b3108ad","event":"","event_payload":"ec223b8f0d0000004c696e757820696e6974726400"},{"imr":1,"event_type":2147483655,"digest":"214b0bef1379756011344877743fdc2a5382bac6e70362d624ccf3f654407c1b4badf7d8f9295dd3dabdef65b27677e0","event":"","event_payload":"4578697420426f6f7420536572766963657320496e766f636174696f6e"},{"imr":1,"event_type":2147483655,"digest":"0a2e01c85deae718a530ad8c6d20a84009babe6c8989269e950d8cf440c6e997695e64d455c4174a652cd080f6230b74","event":"","event_payload":"4578697420426f6f742053657276696365732052657475726e656420776974682053756363657373"},{"imr":3,"event_type":134217729,"digest":"f9974020ef507068183313d0ca808e0d1ca9b2d1ad0c61f5784e7157c362c06536f5ddacdad4451693f48fcc72fff624","event":"system-preparing","event_payload":""},{"imr":3,"event_type":134217729,"digest":"b01c7a2e6a406ae9cd5aa81451e4614e112b8f404df12e6ef506962c1a5279a94dc58da0923c4b7db89e26da9e538302","event":"app-id","event_payload":"ea549f02e1a25fabd1cb788380e033ec5461b2ff"},{"imr":3,"event_type":134217729,"digest":"9c1fecc259af1e8494484a391bdef460cb74d677c76dd114b1e9e7fac343da4e773b2b0eb8df7a6fc0dd8ba5edbb30e1","event":"compose-hash","event_payload":"ea549f02e1a25fabd1cb788380e033ec5461b2ffe4328d753642cf035452e48b"},{"imr":3,"event_type":134217729,"digest":"a8dc2d07060d74dfba7b4942411bcf93ae198da42d172860f0c6dcb9207198a2c857a4b0e57bb019d68be072074a2d01","event":"instance-id","event_payload":"59df8036b824b0aac54f8998b9e1fb2a0cfc5d3a"},{"imr":3,"event_type":134217729,"digest":"98bd7e6bd3952720b65027fd494834045d06b4a714bf737a06b874638b3ea00ff402f7f583e3e3b05e921c8570433ac6","event":"boot-mr-done","event_payload":""},{"imr":3,"event_type":134217729,"digest":"cc0ae424f1335f3059359f712f72f0aebee7a01fba2e4d527f3ea9299bac808a3ea1f8ae2982875fb3c9697fd6f4a5f2","event":"key-provider","event_payload":"7b226e616d65223a226b6d73222c226964223a223330353933303133303630373261383634386365336430323031303630383261383634386365336430333031303730333432303030343139623234353764643962386161363434366439383066313336666666373831326563643663373737343065656230653238623130643536633063303030323861356236653539646365613330376435383362643166373037363965396331313664663262636662313735386139356438363133653764653163383438326330227d"},{"imr":3,"event_type":134217729,"digest":"1a76b2a80a0be71eae59f80945d876351a7a3fb8e9fd1ff1cede5734aa84ea11fd72b4edfbb6f04e5a85edd114c751bd","event":"system-ready","event_payload":""}] diff --git a/sdk/simulator/quote.hex b/sdk/simulator/quote.hex deleted file mode 100644 index 33a3fd92f..000000000 --- a/sdk/simulator/quote.hex +++ /dev/null @@ -1 +0,0 @@ -040002008100000000000000939a7233f79c4ca9940a0db3957f060783fbfe61525f55581315cd9dc950f44700000000060102000000000000000000000000005b38e33a6487958b72c3c12a938eaa5e3fd4510c51aeeab58c7d5ecee41d7c436489d6c8e4f92f160b7cad34207b00c100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000e702060000000000c68518a0ebb42136c12b2275164f8c72f25fa9a34392228687ed6e9caeb9c0f1dbd895e9cf475121c029dc47e70e91fd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085e0855a6384fa1c8a6ab36d0dcbfaa11a5753e5a070c08218ae5fe872fcb86967fd2449c29e22e59dc9fec998cb65476ba8f87f35d0641e8abca07e75e3882abdc9f19d7cc8f6e3fe04435bd5f694d4e3cf008b60d7c7233896e8d1f23c34a703b1c4afcac07d00d8e853163aff3ba3f9af68ddfbdbeafab70210a8dc601b409c28873d74fb6dbe7dc33a8da7c096216d1a3da994b6611ee602f25f07b41671ece90cd2898689f1ad4448fdf1155e3668736cca4499659caae2d8044070de5700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cc1000004f8ed43bde5c1c75f4dcc530d5015ab0514879a8b9dc2663e6c462ac2a0a31face0b334f64976b2aadc4ec0acf00601d5f5738cbf61c12fdcc25dab524a9eac84996a9e56e40ac6c0b019709537f16d751c03e8c0d905d79f224ff06ddc4102860a8770107748c011cdbfcccc857e418735b699ac89dc2ed4da11d5125cb925e0600461000000202191b03ff0006000000000000000000000000000000000000000000000000000000000000000000000000000000001500000000000000e700000000000000e5a3a7b5d830c2953b98534c6c59a3a34fdc34e933f7f5898f0a85cf08846bca0000000000000000000000000000000000000000000000000000000000000000dc9e2a7c6f948f17474e34a7fc43ed030f7c1563f1babddf6340c82e0e54a8c500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000503bbfe5befa55a13e21747c3859f0b618a050312a0340e980187eea232356d60000000000000000000000000000000000000000000000000000000000000000784b1126be37912aaa4189f677ac8821e36366bb526c1b9ffc42c9ad0c332804423f05b854f20d4c511dbcaee26c5911e9b47d28b0f791b9c3d993554034b1382000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f05005e0e00002d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d49494538444343424a65674177494241674956414c5235544954392b396e73423142545a3173725851346c627752424d416f4743437147534d343942414d430a4d484178496a416742674e5642414d4d47556c756447567349464e4857434251513073675547786864475a76636d306751304578476a415942674e5642416f4d0a45556c756447567349454e76636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155450a4341774351304578437a414a42674e5642415954416c56544d423458445449304d4467774d6a45784d54557a4e316f5844544d784d4467774d6a45784d54557a0a4e316f77634445694d434147413155454177775a535735305a5777675530645949464244537942445a584a3061575a70593246305a5445614d426747413155450a43677752535735305a577767513239796347397959585270623234784644415342674e564241634d43314e68626e526849454e7359584a684d517377435159440a5651514944414a445154454c4d416b474131554542684d4356564d775754415442676371686b6a4f5051494242676771686b6a4f50514d4242774e43414154590a77777155344778504a6a596f6a4d4752686136327970346a425164355744764b776d54366c6c314147786a59363870694a50676950686462387a544766374b620a314f79643153464f4d5a70594c795054427a59646f3449444444434341776777487759445652306a42426777466f41556c5739647a62306234656c4153636e550a3944504f4156634c336c5177617759445652306642475177596a42676f46366758495a616148523063484d364c79396863476b7564484a316333526c5a484e6c0a636e5a705932567a4c6d6c75644756734c6d4e766253397a5a3367765932567964476c6d61574e6864476c76626939324e4339775932746a636d772f593245390a6347786864475a76636d306d5a57356a62325270626d63395a4756794d423047413155644467515742425146303476507654474b7762416c356f54765664664d0a2b356a6e7554414f42674e56485138424166384542414d434273417744415944565230544151482f4241497741444343416a6b4743537147534962345451454e0a4151534341696f776767496d4d42344743697147534962345451454e41514545454e3564416f7135634b356e383277396f793165346e34776767466a42676f710a686b69472b453042445145434d494942557a415142677371686b69472b4530424451454341514942416a415142677371686b69472b45304244514543416749420a416a415142677371686b69472b4530424451454341774942416a415142677371686b69472b4530424451454342414942416a415142677371686b69472b4530420a4451454342514942417a415142677371686b69472b45304244514543426749424154415142677371686b69472b453042445145434277494241444151426773710a686b69472b4530424451454343414942417a415142677371686b69472b45304244514543435149424144415142677371686b69472b45304244514543436749420a4144415142677371686b69472b45304244514543437749424144415142677371686b69472b45304244514543444149424144415142677371686b69472b4530420a44514543445149424144415142677371686b69472b45304244514543446749424144415142677371686b69472b453042445145434477494241444151426773710a686b69472b45304244514543454149424144415142677371686b69472b4530424451454345514942437a416642677371686b69472b45304244514543456751510a4167494341674d4241414d4141414141414141414144415142676f71686b69472b45304244514544424149414144415542676f71686b69472b453042445145450a4241617777473841414141774477594b4b6f5a496876684e4151304242516f424154416542676f71686b69472b453042445145474242424a316472685349736d0a682b2f46793074746a6a762f4d45514743697147534962345451454e415163774e6a415142677371686b69472b45304244514548415145422f7a4151426773710a686b69472b45304244514548416745422f7a415142677371686b69472b45304244514548417745422f7a414b42676771686b6a4f5051514441674e48414442450a41694270455738754f726b537469486b4c4b6e6a426855416f637a39545733366a4e2f303765416844503635617749674d2f31474c58745a70446436706150760a535a386d4e7472543830305635346b465944474f7a4f78504374383d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436c6a4343416a32674177494241674956414a567658633239472b487051456e4a3150517a7a674658433935554d416f4743437147534d343942414d430a4d476778476a415942674e5642414d4d45556c756447567349464e48574342536232393049454e424d526f77474159445651514b4442464a626e526c624342440a62334a7762334a6864476c76626a45554d424947413155454277774c553246756447456751327868636d4578437a414a42674e564241674d416b4e424d5173770a435159445651514745774a56557a4165467730784f4441314d6a45784d4455774d5442614677307a4d7a41314d6a45784d4455774d5442614d484178496a41670a42674e5642414d4d47556c756447567349464e4857434251513073675547786864475a76636d306751304578476a415942674e5642416f4d45556c75644756730a49454e76636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b474131554543417743513045780a437a414a42674e5642415954416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a304441516344516741454e53422f377432316c58534f0a3243757a7078773734654a423732457944476757357258437478327456544c7136684b6b367a2b5569525a436e71523770734f766771466553786c6d546c4a6c0a65546d693257597a33714f42757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f536347724442530a42674e5648523845537a424a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b633256790a646d6c6a5a584d75615735305a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e5648513445466751556c5739640a7a62306234656c4153636e553944504f4156634c336c517744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159420a4166384341514177436759494b6f5a497a6a30454177494452774177524149675873566b6930772b6936565947573355462f32327561586530594a446a3155650a6e412b546a44316169356343494359623153416d4435786b66545670766f34556f79695359787244574c6d5552344349394e4b7966504e2b0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949436a7a4343416a53674177494241674955496d554d316c71644e496e7a6737535655723951477a6b6e42717777436759494b6f5a497a6a3045417749770a614445614d4267474131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e760a636e4276636d4630615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a0a42674e5642415954416c56544d423458445445344d4455794d5445774e4455784d466f58445451354d54497a4d54497a4e546b314f566f77614445614d4267470a4131554541777752535735305a5777675530645949464a766233516751304578476a415942674e5642416f4d45556c756447567349454e76636e4276636d46300a615739754d5251774567594456515148444174545957353059534244624746795954454c4d416b47413155454341774351304578437a414a42674e56424159540a416c56544d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a3044415163445167414543366e45774d4449595a4f6a2f69505773437a61454b69370a314f694f534c52466857476a626e42564a66566e6b59347533496a6b4459594c304d784f346d717379596a6c42616c54565978465032734a424b357a6c4b4f420a757a43427544416642674e5648534d4547444157674251695a517a575770303069664f44744a5653763141624f5363477244425342674e5648523845537a424a0a4d45656752614244686b466f64485277637a6f764c324e6c636e52705a6d6c6a5958526c63793530636e567a6447566b63325679646d6c6a5a584d75615735300a5a577775593239744c306c756447567355306459556d397664454e424c6d526c636a416442674e564851344546675155496d554d316c71644e496e7a673753560a55723951477a6b6e4271777744675944565230504151482f42415144416745474d42494741315564457745422f7751494d4159424166384341514577436759490a4b6f5a497a6a3045417749445351417752674968414f572f35516b522b533943695344634e6f6f774c7550524c735747662f59693747535839344267775477670a41694541344a306c72486f4d732b586f356f2f7358364f39515778485241765a55474f6452513763767152586171493d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 From 5f05fbbfb787bed782e71a72336279581f0fde4a Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 24 Dec 2025 10:44:14 +0000 Subject: [PATCH 068/264] Fix unit tests --- REUSE.toml | 5 ++++ guest-agent/fixtures/attestation.bin | Bin 0 -> 39090 bytes guest-agent/src/rpc_service.rs | 38 ++++++++++++--------------- 3 files changed, 22 insertions(+), 21 deletions(-) create mode 100644 guest-agent/fixtures/attestation.bin diff --git a/REUSE.toml b/REUSE.toml index 11596d72f..228ffbd63 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -167,3 +167,8 @@ SPDX-License-Identifier = "CC0-1.0" path = "verifier/builder/shared/*.txt" SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = "guest-agent/fixtures/*" +SPDX-FileCopyrightText = "NONE" +SPDX-License-Identifier = "CC0-1.0" diff --git a/guest-agent/fixtures/attestation.bin b/guest-agent/fixtures/attestation.bin new file mode 100644 index 0000000000000000000000000000000000000000..2f7a9c35efad571c2e964cf40f8b48d87d345d51 GIT binary patch literal 39090 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 (AppState, tempfile::NamedTempFile, tempfile::NamedTempFile) { - let mut dummy_quote_file = tempfile::NamedTempFile::new().unwrap(); - let dummy_event_log_file = tempfile::NamedTempFile::new().unwrap(); + async fn setup_test_state() -> (AppState, tempfile::NamedTempFile) { + let mut temp_attestation_file = tempfile::NamedTempFile::new().unwrap(); - let dummy_quote = vec![b'0'; 10020]; - dummy_quote_file.write_all(&dummy_quote).unwrap(); - dummy_quote_file.flush().unwrap(); + let attestation = include_bytes!("../fixtures/attestation.bin"); + temp_attestation_file.write_all(attestation).unwrap(); + temp_attestation_file.flush().unwrap(); let dummy_simulator = Simulator { enabled: true, - quote_file: dummy_quote_file.path().to_str().unwrap().to_string(), - event_log_file: dummy_event_log_file.path().to_str().unwrap().to_string(), - attestation_file: String::new(), + attestation_file: temp_attestation_file.path().to_str().unwrap().to_string(), }; let dummy_appcompose = AppCompose { @@ -882,14 +879,13 @@ pNs85uhOZE8z2jr8Pg== AppState { inner: Arc::new(inner), }, - dummy_quote_file, - dummy_event_log_file, + temp_attestation_file, ) } #[tokio::test] async fn test_verify_ed25519_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -916,7 +912,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_verify_secp256k1_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -943,7 +939,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_ed25519_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -973,7 +969,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_secp256k1_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -1005,7 +1001,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_secp256k1_prehashed_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -1042,7 +1038,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_secp256k1_prehashed_invalid_length_fails() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state: state.clone(), }; @@ -1065,7 +1061,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_sign_unsupported_algorithm_fails() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = InternalRpcHandler { state }; let request = SignRequest { algorithm: "rsa".to_string(), // Unsupported algorithm @@ -1079,7 +1075,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_get_attestation_for_app_key_ed25519_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = ExternalRpcHandler::new(state.clone()); let request = GetAttestationForAppKeyRequest { algorithm: "ed25519".to_string(), @@ -1094,7 +1090,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_get_attestation_for_app_key_secp256k1_success() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = ExternalRpcHandler::new(state.clone()); let request = GetAttestationForAppKeyRequest { algorithm: "secp256k1".to_string(), @@ -1109,7 +1105,7 @@ pNs85uhOZE8z2jr8Pg== #[tokio::test] async fn test_get_attestation_for_app_key_unsupported_algorithm_fails() { - let (state, _quote_file, _log_file) = setup_test_state().await; + let (state, _guard) = setup_test_state().await; let handler = ExternalRpcHandler::new(state); let request = GetAttestationForAppKeyRequest { algorithm: "ecdsa".to_string(), // Unsupported algorithm From fb232eef6a2b82e1056604e07e15c6a3cc6b2e50 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 24 Dec 2025 14:19:51 +0000 Subject: [PATCH 069/264] Add nitro TPM support (step 1) --- dstack-attest/src/attestation.rs | 70 ++++++++++++-- dstack-types/src/lib.rs | 13 ++- dstack-util/src/host_api.rs | 4 +- dstack-util/src/main.rs | 102 ++++++++++++++++++++ tpm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem | 14 +++ tpm-qvl/src/lib.rs | 2 + verifier/src/verification.rs | 5 +- vmm/src/app.rs | 12 ++- 8 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 tpm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index a031efe16..15f8ff5e0 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -12,7 +12,7 @@ use dcap_qvl::{ quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15}, verify::VerifiedReport as TdxVerifiedReport, }; -use dstack_types::Platform; +use dstack_types::{Platform, VmConfig}; use ez_hash::{sha256, Hasher, Sha256, Sha384}; use or_panic::ResultOrPanic; use scale::{Decode, Encode}; @@ -82,6 +82,7 @@ impl AttestationMode { } bail!("Unsupported platform: GCP(-tdx)"); } + Platform::AwsNitroEnclave => Ok(Self::DstackNitro), } } @@ -344,9 +345,13 @@ struct Mrs { } impl Attestation { - fn decode_mr_tpm(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { - let os_image_hash = self.find_event_payload("os-image-hash").unwrap_or_default(); - let mr_system = sha256([&os_image_hash, mr_key_provider]); + fn decode_mr_gcp_tpm( + &self, + boottime_mr: bool, + mr_key_provider: &[u8], + os_image_hash: &[u8], + ) -> Result { + let mr_system = sha256([os_image_hash, mr_key_provider]); let tpm_quote = self.tpm_quote.as_ref().context("TPM quote not found")?; let pcr0 = tpm_quote .pcr_values @@ -367,6 +372,32 @@ impl Attestation { }) } + fn decode_mr_nitro_tpm( + &self, + _boottime_mr: bool, + mr_key_provider: &[u8], + os_image_hash: &[u8], + ) -> Result { + let mr_system = sha256([os_image_hash, mr_key_provider]); + let tpm_quote = self.tpm_quote.as_ref().context("TPM quote not found")?; + let pcr0 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 0) + .context("PCR 0 not found")?; + let pcr2 = tpm_quote + .pcr_values + .iter() + .find(|p| p.index == 2) + .context("PCR 2 not found")?; + let todo = "Maybe more pcrs"; + let mr_aggregated = sha256([&pcr0.value[..], &pcr2.value]); + Ok(Mrs { + mr_system, + mr_aggregated, + }) + } + fn decode_mr_tdx(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { let quote = self.decode_tdx_quote()?; let rtmr3 = self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); @@ -406,6 +437,15 @@ impl Attestation { }) } + fn decode_os_image_hash(&self) -> Result> { + if self.config.is_empty() { + return Ok(vec![]); + } + let vm_config: VmConfig = + serde_json::from_str(&self.config).context("Failed to parse vm config")?; + Ok(vm_config.os_image_hash) + } + /// Decode the app info from the event log pub fn decode_app_info(&self, boottime_mr: bool) -> Result { let key_provider_info = if boottime_mr { @@ -418,20 +458,27 @@ impl Attestation { } else { sha256(&key_provider_info) }; + let os_image_hash = self + .decode_os_image_hash() + .context("Failed to decode os image hash")?; let mrs = match self.mode { AttestationMode::DstackTdx => self.decode_mr_tdx(boottime_mr, &mr_key_provider)?, - AttestationMode::GcpTdx => self.decode_mr_tpm(boottime_mr, &mr_key_provider)?, - AttestationMode::DstackNitro => bail!("Nitro attestation is not supported"), + AttestationMode::GcpTdx => { + self.decode_mr_gcp_tpm(boottime_mr, &mr_key_provider, &os_image_hash)? + } + AttestationMode::DstackNitro => { + self.decode_mr_nitro_tpm(boottime_mr, &mr_key_provider, &os_image_hash)? + } }; Ok(AppInfo { app_id: self.find_event_payload("app-id").unwrap_or_default(), compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), instance_id: self.find_event_payload("instance-id").unwrap_or_default(), - os_image_hash: self.find_event_payload("os-image-hash").unwrap_or_default(), device_id: sha256(self.report.get_ppid()).to_vec(), mr_system: mrs.mr_system, mr_aggregated: mrs.mr_aggregated, key_provider_info, + os_image_hash, }) } } @@ -659,7 +706,14 @@ impl Attestation { } } AttestationMode::DstackNitro => { - bail!("Nitro not supported"); + let Some(tpm_report) = &tpm_report else { + bail!("TPM report is missing in dstack-nitro mode"); + }; + // TPM quote the TDX quote + let qualifying_data = sha256(self.report_data); + if qualifying_data != tpm_report.attest.qualified_data[..] { + bail!("tpm qualified_data mismatch"); + } } } let report = DstackVerifiedReport { diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index a2dc10477..e9023405b 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: Apache-2.0 +use std::path::Path; + use scale::{Decode, Encode}; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; @@ -141,8 +143,7 @@ pub struct SysConfig { #[derive(Deserialize, Serialize, Debug, Clone)] pub struct VmConfig { - pub spec_version: u32, - #[serde(with = "hex_bytes")] + #[serde(with = "hex_bytes", default)] pub os_image_hash: Vec, #[serde(default)] pub cpu_count: u32, @@ -272,11 +273,18 @@ pub enum Platform { Dstack, /// Google Cloud Platform Gcp, + /// AWS Nitro Enclave + AwsNitroEnclave, } impl Platform { /// Detect platform from system DMI information pub fn detect() -> Option { + // Nitro Enclave: NSM device exists only inside enclave + if Path::new("/dev/nsm").exists() { + return Some(Self::AwsNitroEnclave); + } + if let Ok(board_name) = std::fs::read_to_string("/sys/class/dmi/id/product_name") { match board_name.trim() { "dstack" | "qemu" => return Some(Self::Dstack), @@ -297,6 +305,7 @@ impl Platform { match self { Self::Dstack => "dstack", Self::Gcp => "gcp", + Self::AwsNitroEnclave => "aws-nitro-enclave", } } } diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index 95eb59d73..88aeae4d4 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -55,8 +55,8 @@ impl HostApi { pub async fn notify(&self, event: &str, payload: &str) -> Result<()> { match Platform::detect_or_dstack() { Platform::Dstack => {} - Platform::Gcp => { - // Skip notify on GCP as no host dstack-vmm there. + Platform::Gcp | Platform::AwsNitroEnclave => { + // Skip notify on unsupported platforms return Ok(()); } } diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index 40840ecd4..6a0cc65ee 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -87,6 +87,8 @@ enum Commands { AttestJson(AttestJsonArgs), /// Strip attestation for certificate embedding AttestStrip(AttestStripArgs), + /// Get app keys from a KMS server + GetKeys(GetKeysArgs), } #[derive(Parser)] @@ -331,6 +333,22 @@ struct AttestStripArgs { output: Option, } +#[derive(Parser)] +/// Get app keys from a KMS server +struct GetKeysArgs { + /// KMS server URL (e.g., https://kms.example.com) + #[arg(short, long)] + kms_url: String, + + /// PCCS URL for quote verification (optional) + #[arg(short, long)] + pccs_url: Option, + + /// Output file path (default: stdout as JSON) + #[arg(short, long)] + output: Option, +} + fn pad64(data: &[u8]) -> Result<[u8; 64]> { if data.len() > 64 { anyhow::bail!("report_data must be at most 64 bytes"); @@ -495,6 +513,87 @@ fn cmd_attest_strip(args: AttestStripArgs) -> Result<()> { Ok(()) } +async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { + use dstack_kms_rpc::kms_client::KmsClient; + use ra_rpc::client::{RaClient, RaClientConfig}; + use ra_tls::cert::generate_ra_cert; + + let kms_url = if args.kms_url.ends_with("/prpc") { + args.kms_url.clone() + } else { + format!("{}/prpc", args.kms_url.trim_end_matches('/')) + }; + + // Step 1: Get temporary CA certificate + eprintln!("Connecting to KMS: {kms_url}"); + let tmp_ca = { + let client = RaClient::new(kms_url.clone(), true)?; + let kms_client = KmsClient::new(client); + kms_client + .get_temp_ca_cert() + .await + .context("Failed to get temp CA cert")? + }; + + // Step 2: Generate RA-TLS client certificate + let cert_pair = generate_ra_cert(tmp_ca.temp_ca_cert.clone(), tmp_ca.temp_ca_key.clone()) + .context("Failed to generate RA cert")?; + + // Step 3: Create authenticated client and request app keys + let ra_client = RaClientConfig::builder() + .tls_no_check(false) + .tls_built_in_root_certs(false) + .remote_uri(kms_url.clone()) + .tls_client_cert(cert_pair.cert_pem) + .tls_client_key(cert_pair.key_pem) + .tls_ca_cert(tmp_ca.ca_cert.clone()) + .maybe_pccs_url(args.pccs_url.clone()) + .build() + .into_client() + .context("Failed to create RA client")?; + + let kms_client = KmsClient::new(ra_client); + let response = kms_client + .get_app_key(dstack_kms_rpc::GetAppKeyRequest { + api_version: 1, + vm_config: "".to_string(), + }) + .await + .context("Failed to get app key")?; + + // Step 4: Build AppKeys structure + let (_, ca_pem) = x509_parser::pem::parse_x509_pem(tmp_ca.ca_cert.as_bytes()) + .context("Failed to parse CA cert")?; + let x509 = ca_pem.parse_x509().context("Failed to parse CA cert")?; + let root_pubkey = x509.public_key().raw.to_vec(); + + let keys = utils::AppKeys { + ca_cert: tmp_ca.ca_cert, + disk_crypt_key: response.disk_crypt_key, + env_crypt_key: response.env_crypt_key, + k256_key: response.k256_key, + k256_signature: response.k256_signature, + gateway_app_id: response.gateway_app_id, + key_provider: KeyProvider::Kms { + url: kms_url, + pubkey: root_pubkey, + tmp_ca_key: tmp_ca.temp_ca_key, + tmp_ca_cert: tmp_ca.temp_ca_cert, + }, + }; + + // Step 5: Output result + let json = serde_json::to_string_pretty(&keys).context("Failed to serialize app keys")?; + if let Some(output_path) = args.output { + fs::write(&output_path, &json).context("Failed to write app keys")?; + eprintln!("App keys written to: {}", output_path.display()); + } else { + println!("{json}"); + } + + Ok(()) +} + fn cmd_quote() -> Result<()> { let mut report_data = [0; 64]; io::stdin() @@ -1162,6 +1261,9 @@ async fn main() -> Result<()> { Commands::AttestStrip(args) => { cmd_attest_strip(args)?; } + Commands::GetKeys(args) => { + cmd_get_keys(args).await?; + } } Ok(()) diff --git a/tpm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem b/tpm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem new file mode 100644 index 000000000..03fd45442 --- /dev/null +++ b/tpm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD +VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4 +MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL +DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG +BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb +48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE +h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF +R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC +MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW +rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N +IwLz3/Y= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/tpm-qvl/src/lib.rs b/tpm-qvl/src/lib.rs index 7e8392897..f7e81cb3b 100644 --- a/tpm-qvl/src/lib.rs +++ b/tpm-qvl/src/lib.rs @@ -22,11 +22,13 @@ use serde::{Deserialize, Serialize}; /// Subject: CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US /// Valid: 2022-07-08 to 2122-07-08 (100 years) pub const GCP_ROOT_CA: &str = include_str!("../certs/gcp-root-ca.pem"); +pub const AWS_NITRO_ENCLAVES_ROOT_G1: &str = include_str!("../certs/AWS_NitroEnclaves_Root-G1.pem"); /// Get TPM root CA certificate for the given platform pub fn get_root_ca(platform: Platform) -> Result<&'static str> { match platform { Platform::Gcp => Ok(GCP_ROOT_CA), + Platform::AwsNitroEnclave => Ok(AWS_NITRO_ENCLAVES_ROOT_G1), Platform::Dstack => bail!("dstack platform does not use TPM attestation"), } } diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 2ef335421..20d0ba977 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -497,7 +497,10 @@ impl CvmVerifier { self.verify_os_image_hash_for_dstack_tdx(&vm_config, attestation, debug, details) .await?; } - AttestationMode::DstackNitro => bail!("Nitro not supported"), + AttestationMode::DstackNitro => { + let todo = "Implement it"; + bail!("Nitro not supported"); + } } Ok(vm_config) } diff --git a/vmm/src/app.rs b/vmm/src/app.rs index 1533e6e45..c176ce94f 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -805,22 +805,21 @@ pub(crate) fn make_sys_config(cfg: &Config, manifest: &Manifest) -> Result dstack_types::VmConfig { +fn make_vm_config(cfg: &Config, manifest: &Manifest, image: &Image) -> Result { let os_image_hash = image .digest .as_ref() .and_then(|d| hex::decode(d).ok()) .unwrap_or_default(); let gpus = manifest.gpus.clone().unwrap_or_default(); - dstack_types::VmConfig { - spec_version: 1, + let mut config = serde_json::to_value(dstack_types::VmConfig { os_image_hash, cpu_count: manifest.vcpu, memory_size: manifest.memory as u64 * 1024 * 1024, @@ -834,7 +833,10 @@ fn make_vm_config(cfg: &Config, manifest: &Manifest, image: &Image) -> dstack_ty host_share_mode: cfg.cvm.host_share_mode.clone(), hotplug_off: cfg.cvm.qemu_hotplug_off, image: Some(manifest.image.clone()), - } + })?; + // For backward compatibility + config["spec_version"] = serde_json::Value::from(1); + Ok(config) } fn paginate(items: Vec, page: u32, page_size: u32) -> impl Iterator { From a153bc0afa87cda536bd4180de0a1012e6b47c00 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 25 Dec 2025 02:56:32 +0000 Subject: [PATCH 070/264] Add nsm-attest --- Cargo.lock | 52 +++++++ Cargo.toml | 2 + dstack-attest/Cargo.toml | 1 + nsm-attest/Cargo.toml | 21 +++ nsm-attest/src/lib.rs | 197 +++++++++++++++++++++++++ nsm-attest/src/types.rs | 78 ++++++++++ nsm-attest/tests/attestation_test.rs | 89 +++++++++++ nsm-attest/tests/nitro_attestation.bin | Bin 0 -> 4682 bytes 8 files changed, 440 insertions(+) create mode 100644 nsm-attest/Cargo.toml create mode 100644 nsm-attest/src/lib.rs create mode 100644 nsm-attest/src/types.rs create mode 100644 nsm-attest/tests/attestation_test.rs create mode 100644 nsm-attest/tests/nitro_attestation.bin diff --git a/Cargo.lock b/Cargo.lock index e03f83879..e1ec07386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1361,6 +1361,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cipher" version = "0.3.0" @@ -2137,6 +2164,7 @@ dependencies = [ "fs-err", "hex", "hex_fmt", + "nsm-attest", "or-panic", "parity-scale-codec", "serde", @@ -3228,6 +3256,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash_hasher" version = "2.0.4" @@ -4564,6 +4603,19 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +[[package]] +name = "nsm-attest" +version = "0.5.5" +dependencies = [ + "anyhow", + "ciborium", + "hex", + "nix 0.29.0", + "serde", + "serde_bytes", + "tracing", +] + [[package]] name = "ntapi" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index 6072b6c4d..c9eee846e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "ra-tls", "tdx-attest", "tpm-attest", + "nsm-attest", "tpm2", "tpm-types", "tpm-qvl", @@ -73,6 +74,7 @@ supervisor = { path = "supervisor" } supervisor-client = { path = "supervisor/client" } tdx-attest = { path = "tdx-attest" } tpm-attest = { path = "tpm-attest" } +nsm-attest = { path = "nsm-attest" } tpm2 = { path = "tpm2" } tpm-types = { path = "tpm-types" } dstack-attest = { path = "dstack-attest" } diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml index 7ccfca47d..e66d2903d 100644 --- a/dstack-attest/Cargo.toml +++ b/dstack-attest/Cargo.toml @@ -27,6 +27,7 @@ sha2.workspace = true sha3.workspace = true tdx-attest.workspace = true tpm-attest.workspace = true +nsm-attest.workspace = true tpm-qvl.workspace = true tpm-types.workspace = true diff --git a/nsm-attest/Cargo.toml b/nsm-attest/Cargo.toml new file mode 100644 index 000000000..1e8ca1726 --- /dev/null +++ b/nsm-attest/Cargo.toml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nsm-attest" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "AWS Nitro Enclave NSM attestation library" + +[dependencies] +anyhow.workspace = true +serde.workspace = true +tracing.workspace = true +aws-nitro-enclaves-nsm-api = "0.4" +ciborium = "0.2" + +[dev-dependencies] +hex.workspace = true diff --git a/nsm-attest/src/lib.rs b/nsm-attest/src/lib.rs new file mode 100644 index 000000000..faf176459 --- /dev/null +++ b/nsm-attest/src/lib.rs @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! AWS Nitro Enclave NSM (Nitro Security Module) Attestation Library +//! +//! This crate wraps the official `aws-nitro-enclaves-nsm-api` crate and provides +//! additional utilities for attestation document parsing. +//! +//! The NSM device is available at `/dev/nsm` inside a Nitro Enclave and +//! provides attestation, PCR operations, and entropy generation. + +use anyhow::{bail, Result}; +use aws_nitro_enclaves_nsm_api::api::{Request, Response}; +use aws_nitro_enclaves_nsm_api::driver; +use std::path::Path; + +mod types; + +pub use types::*; + +/// NSM device path +pub const NSM_DEVICE_PATH: &str = "/dev/nsm"; + +/// Check if running inside a Nitro Enclave +pub fn is_nitro_enclave() -> bool { + Path::new(NSM_DEVICE_PATH).exists() +} + +/// NSM Context for interacting with the Nitro Security Module +#[derive(Debug)] +pub struct NsmContext { + fd: i32, +} + +impl NsmContext { + /// Open the NSM device + pub fn new() -> Result { + let fd = driver::nsm_init(); + if fd < 0 { + bail!("Failed to open NSM device"); + } + Ok(Self { fd }) + } + + /// Get attestation document from NSM + /// + /// # Arguments + /// * `user_data` - Optional user data to include in attestation (max 512 bytes) + /// * `nonce` - Optional nonce for freshness (max 512 bytes) + /// * `public_key` - Optional public key to include (max 1024 bytes) + pub fn get_attestation_doc( + &self, + user_data: Option<&[u8]>, + nonce: Option<&[u8]>, + public_key: Option<&[u8]>, + ) -> Result> { + let request = Request::Attestation { + user_data: user_data.map(|d| d.to_vec().into()), + nonce: nonce.map(|d| d.to_vec().into()), + public_key: public_key.map(|d| d.to_vec().into()), + }; + + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::Attestation { document } => Ok(document), + Response::Error(err) => bail!("NSM attestation failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } + + /// Describe the NSM module + pub fn describe(&self) -> Result { + let request = Request::DescribeNSM; + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::DescribeNSM { + version_major, + version_minor, + version_patch, + module_id, + max_pcrs, + locked_pcrs, + digest, + } => Ok(NsmDescription { + version_major, + version_minor, + version_patch, + module_id, + max_pcrs, + locked_pcrs: locked_pcrs.into_iter().collect(), + digest: format!("{:?}", digest), + }), + Response::Error(err) => bail!("NSM describe failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } + + /// Get random bytes from NSM + pub fn get_random(&self) -> Result> { + let request = Request::GetRandom; + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::GetRandom { random } => Ok(random), + Response::Error(err) => bail!("NSM get_random failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } + + /// Extend a PCR with data + /// + /// # Arguments + /// * `index` - PCR index (0-15 for user PCRs) + /// * `data` - Data to extend into PCR (will be hashed) + pub fn pcr_extend(&self, index: u16, data: &[u8]) -> Result> { + let request = Request::ExtendPCR { + index, + data: data.to_vec(), + }; + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::ExtendPCR { data } => Ok(data), + Response::Error(err) => bail!("NSM PCR extend failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } + + /// Lock a PCR (prevent further extensions) + pub fn pcr_lock(&self, index: u16) -> Result<()> { + let request = Request::LockPCR { index }; + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::LockPCR => Ok(()), + Response::Error(err) => bail!("NSM PCR lock failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } + + /// Lock multiple PCRs + pub fn pcr_lock_range(&self, range: u16) -> Result<()> { + let request = Request::LockPCRs { range }; + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::LockPCRs => Ok(()), + Response::Error(err) => bail!("NSM PCR lock range failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } + + /// Describe a specific PCR + pub fn describe_pcr(&self, index: u16) -> Result { + let request = Request::DescribePCR { index }; + let response = driver::nsm_process_request(self.fd, request); + + match response { + Response::DescribePCR { lock, data } => Ok(PcrInfo { lock, data }), + Response::Error(err) => bail!("NSM describe PCR failed: {:?}", err), + _ => bail!("Unexpected NSM response"), + } + } +} + +impl Drop for NsmContext { + fn drop(&mut self) { + driver::nsm_exit(self.fd); + } +} + +/// PCR information +#[derive(Debug, Clone)] +pub struct PcrInfo { + /// Whether the PCR is locked + pub lock: bool, + /// Current PCR value + pub data: Vec, +} + +/// Create an attestation document with report data +/// +/// This is a convenience function that creates an attestation document +/// with the given report data as user_data. +pub fn get_attestation(report_data: &[u8]) -> Result> { + let ctx = NsmContext::new()?; + ctx.get_attestation_doc(Some(report_data), None, None) +} + +/// Get random bytes from NSM +pub fn get_random() -> Result> { + let ctx = NsmContext::new()?; + ctx.get_random() +} diff --git a/nsm-attest/src/types.rs b/nsm-attest/src/types.rs new file mode 100644 index 000000000..e33ab9be2 --- /dev/null +++ b/nsm-attest/src/types.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! NSM types for attestation document parsing + +use anyhow::Context; +use serde::Deserialize; + +/// NSM Description +#[derive(Debug, Clone)] +pub struct NsmDescription { + /// Version major + pub version_major: u16, + /// Version minor + pub version_minor: u16, + /// Version patch + pub version_patch: u16, + /// Module ID + pub module_id: String, + /// Maximum number of PCRs + pub max_pcrs: u16, + /// Locked PCRs bitmap + pub locked_pcrs: Vec, + /// Digest algorithm + pub digest: String, +} + +/// Attestation document structure (COSE Sign1) +/// +/// The attestation document is a COSE Sign1 structure containing: +/// - Protected header with algorithm +/// - Unprotected header (empty) +/// - Payload (CBOR-encoded attestation claims) +/// - Signature +#[derive(Debug, Clone, Deserialize)] +pub struct AttestationDocument { + /// Module ID + pub module_id: String, + /// Digest algorithm used + pub digest: String, + /// Timestamp (milliseconds since epoch) + pub timestamp: u64, + /// PCR values + pub pcrs: std::collections::BTreeMap>, + /// Certificate (DER-encoded) + pub certificate: Vec, + /// CA bundle (list of DER-encoded certificates) + pub cabundle: Vec>, + /// Optional public key + #[serde(default)] + pub public_key: Option>, + /// Optional user data + #[serde(default)] + pub user_data: Option>, + /// Optional nonce + #[serde(default)] + pub nonce: Option>, +} + +impl AttestationDocument { + /// Parse attestation document from COSE Sign1 bytes + pub fn from_cose(data: &[u8]) -> anyhow::Result { + // COSE Sign1 structure is a CBOR array: [protected, unprotected, payload, signature] + let (_protected, _unprotected, payload, _signature): ( + Vec, + std::collections::BTreeMap, + Vec, + Vec, + ) = ciborium::from_reader(data).context("Failed to parse COSE Sign1")?; + + // Parse the payload + let doc: AttestationDocument = ciborium::from_reader(&payload[..]) + .map_err(|e| anyhow::anyhow!("Failed to parse attestation payload: {}", e))?; + + Ok(doc) + } +} diff --git a/nsm-attest/tests/attestation_test.rs b/nsm-attest/tests/attestation_test.rs new file mode 100644 index 000000000..2b3569afc --- /dev/null +++ b/nsm-attest/tests/attestation_test.rs @@ -0,0 +1,89 @@ +// Test for NSM attestation document parsing +use nsm_attest::AttestationDocument; + +// Real attestation captured from Nitro Enclave +const ATTESTATION_BIN: &[u8] = include_bytes!("nitro_attestation.bin"); + +#[test] +fn test_parse_versioned_attestation_and_extract_nsm_quote() { + // The attestation.bin is a VersionedAttestation (SCALE encoded) + // Format: version (1 byte) + SCALE-encoded Attestation + // Attestation contains: quote (AttestationQuote), runtime_events, report_data, config + + // For DstackNitroEnclave, the quote contains nsm_quote which is the COSE Sign1 document + // Let's find the COSE Sign1 marker (0x8444 = CBOR array tag for COSE_Sign1) + + let data = ATTESTATION_BIN; + println!("Total attestation length: {} bytes", data.len()); + + // Find COSE Sign1 structure (starts with 0x84 0x44 for protected header) + let mut cose_start = None; + for i in 0..data.len().saturating_sub(2) { + if data[i] == 0x84 && data[i + 1] == 0x44 { + cose_start = Some(i); + break; + } + } + + let cose_start = cose_start.expect("Should find COSE Sign1 marker"); + println!("COSE Sign1 starts at offset: {}", cose_start); + + // The COSE Sign1 structure length is encoded before it in SCALE + // For now, let's try parsing from the marker to the end + let cose_data = &data[cose_start..]; + + // Try to parse the attestation document + let result = AttestationDocument::from_cose(cose_data); + match result { + Ok(doc) => { + println!("Successfully parsed attestation document!"); + println!("Module ID: {}", doc.module_id); + println!("Digest: {}", doc.digest); + println!("Timestamp: {}", doc.timestamp); + println!("PCR count: {}", doc.pcrs.len()); + + // Verify expected values + assert!(!doc.module_id.is_empty(), "Module ID should not be empty"); + assert_eq!(doc.digest, "SHA384", "Digest should be SHA384"); + assert!(doc.pcrs.contains_key(&0), "Should have PCR0"); + assert!(doc.pcrs.contains_key(&1), "Should have PCR1"); + assert!(doc.pcrs.contains_key(&2), "Should have PCR2"); + + // Print all PCR values + for idx in 0..16u16 { + if let Some(value) = doc.pcrs.get(&idx) { + let is_zero = value.iter().all(|&b| b == 0); + if is_zero { + println!("PCR{}: ALL ZEROS (len={})", idx, value.len()); + } else { + println!("PCR{}: {:02x?} (len={})", idx, value, value.len()); + } + } + } + } + Err(e) => { + panic!("Failed to parse attestation document: {}", e); + } + } +} + +#[test] +fn test_attestation_document_structure() { + // Verify the COSE Sign1 structure is present + let data = ATTESTATION_BIN; + + // COSE Sign1 is a CBOR array with 4 elements + // The marker 0x84 indicates a 4-element array + let has_cose_marker = data.windows(2).any(|w| w[0] == 0x84 && w[1] == 0x44); + assert!( + has_cose_marker, + "Should contain COSE Sign1 marker (0x84 0x44)" + ); + + // Verify module_id string is present + let module_id_marker = b"module_id"; + let has_module_id = data + .windows(module_id_marker.len()) + .any(|w| w == module_id_marker); + assert!(has_module_id, "Should contain module_id field"); +} diff --git a/nsm-attest/tests/nitro_attestation.bin b/nsm-attest/tests/nitro_attestation.bin new file mode 100644 index 0000000000000000000000000000000000000000..676305f1b723ae1d111cfb5891918749050fc2d8 GIT binary patch literal 4682 zcmd6qc~lek7RQrG*f)X7A_Z$u#3lHfWF`xW0zt%w)heO^wQ-V}EJ6q+0YRTYC|IZz z7YYj6ibbuURuFs^9^h8+Aqqw7F5-?AK?SO^Sl&d`LdEw^PW#^Job&tR-prl3ncwGo z@BLArO8;cP9aJ78FT`4KM4?pS(cy$hfya(lFaeLnCAfS(A>p&wEF3|YM1%yud@+h* z9G)0w5jY~2;tCm|)<}Z_eAqlhp;0JF3sXiqQ7F`{!Miga;E|FjbrD4fmL7((1Y?qv zar_Hcl5)Sah)rlkPv95?_iegE$}nd zZhp2ew{7RN-eRFf-5STvg^1Qb0lx)7{JHT$>5 zr#}1?y`iEHPA7ZJ0h?V{!VG<%rqmU{jn1%>};z1I$id@t@8bpPl z5jth9JKGkh16T1OuN#;e8c$psQ13;j(SSAsHUlPxv|zfSEfgFC+X8EyVQg-?7*op? z5i*S`0v-V@bxtF5qaXs)Xu=8D3Rvhorsjq|N^BK*i#hpJ(qU#!^I04O^8r_9(agax zg~20Q%_K0jh6#IME7cwm3Qd$sS1iJ@7(xwP>@8R*%pw;XM%hT{8wY}_ zL1vXzd6CGB-4U6yZbnRAf^D0Zq$ZL0=j8Od)Hw~w+fsT8mwA7?{6=DG-O@%E#+HiW zQ$KCoUtj;dxqa?TH{%!|#Mt-dTtrzi>N#U(}qGk`EXie#cqvcK;4%~G1KV5pK<@ut)8G_kEclN(9 z<^&uEw#;UDieNqrSUi}`;sKPT2pKSum*$`Os&{OCAGEM_xACz~dE~9!D;ui(d8|$9 zh_t}|r%{*F`~p9ke;3(V{PgPWqZPtYti#1IMwZuYF67R5FvdyRvY=s2W0Q$UWgFRL zZ!EZcZ3K19odyZj-9+m5S#6v+8GKkhqbuJRcmu09TW`ibK>0 zqO|w=`WuGMD}o#owq5zSQ{9gqomo!^v|tycu+8&s_~2pt-*Yzxy$2~juX?7mEIf2~#iPssY4I^QY-H9> zr{=Ss`P%b9dluNdfyNjZk}Qx-0*u!ATt;T~$X2lld&=(H?RDW{Om zDVP}HbAUv`gnCa}EEP&qLr0OlkHpxx?<=}6x^4ZE?d)Aa zi>4ln?^zk1>1`B{v%0qAYOP6idX72wiS}v_R%=;4<<6xzg&yJ5? zB)6Xg-Egd3SA*VuVM)p8FtDm*hGtG2xmkLS{A#g7$NHN7P`*`9%C~D4-Wexa5g9EGS4c!*MBGzFw3>(#;g|*!dh1S7f8-(} zR1p&5X}@rJ(WBVFhscW6kH71(aOsTq9vemt9-o$~ofj9o_`sZ|X8O}P_Aql@XM@#> zgE2ftS}QEB$iCUF==F3I$W4q)Gw%|mnf3m^C?fh@SqBa3@U;8`Wq@&ODqP%^r|3KO zN3whwJD<9mp;C(!N=!zG Date: Thu, 25 Dec 2025 03:03:05 +0000 Subject: [PATCH 071/264] attestation: Use enum rather than option --- dstack-attest/src/attestation.rs | 498 +++++++++++++++---------------- dstack-types/src/lib.rs | 6 +- dstack-util/src/host_api.rs | 2 +- dstack-util/src/main.rs | 15 +- guest-agent/src/rpc_service.rs | 8 +- kms/src/main_service.rs | 20 +- ra-tls/src/cert.rs | 14 +- tpm-qvl/src/lib.rs | 2 +- verifier/src/verification.rs | 34 +-- 9 files changed, 288 insertions(+), 311 deletions(-) diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index 15f8ff5e0..b6f806cd1 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -4,7 +4,7 @@ //! Attestation functions -use std::{borrow::Cow, str::FromStr}; +use std::borrow::Cow; use anyhow::{anyhow, bail, Context, Result}; use cc_eventlog::{RuntimeEvent, TdxEvent}; @@ -16,7 +16,7 @@ use dstack_types::{Platform, VmConfig}; use ez_hash::{sha256, Hasher, Sha256, Sha384}; use or_panic::ResultOrPanic; use scale::{Decode, Encode}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use serde_human_bytes as hex_bytes; use sha2::Digest as _; use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; @@ -25,43 +25,18 @@ use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; pub use tpm_types::TpmQuote; /// Attestation mode -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, Encode, Decode)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode)] pub enum AttestationMode { /// Intel TDX with DCAP quote only - #[serde(rename = "dstack-tdx")] #[default] DstackTdx, /// GCP TDX with DCAP quote only - #[serde(rename = "gcp-tdx")] - GcpTdx, + DstackGcpTdx, /// Dstack attestation SDK in AWS Nitro Enclave - #[serde(rename = "dstack-nitro")] - DstackNitro, -} - -impl FromStr for AttestationMode { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "dstack-tdx" => Ok(Self::DstackTdx), - "gcp-tdx" => Ok(Self::GcpTdx), - "dstack-nitro" => Ok(Self::DstackNitro), - _ => bail!("Invalid attestation mode: {s}"), - } - } + DstackNitroEnclave, } impl AttestationMode { - /// Get string representation - pub fn as_str(&self) -> &str { - match self { - Self::DstackTdx => "dstack-tdx", - Self::GcpTdx => "gcp-tdx", - Self::DstackNitro => "dstack-nitro", - } - } - /// Detect attestation mode from system pub fn detect() -> Result { let has_tdx = std::path::Path::new("/dev/tdx_guest").exists(); @@ -78,11 +53,11 @@ impl AttestationMode { Platform::Gcp => { // GCP platform: TDX + TPM dual mode if has_tdx { - return Ok(Self::GcpTdx); + return Ok(Self::DstackGcpTdx); } bail!("Unsupported platform: GCP(-tdx)"); } - Platform::AwsNitroEnclave => Ok(Self::DstackNitro), + Platform::NitroEnclave => Ok(Self::DstackNitroEnclave), } } @@ -90,26 +65,26 @@ impl AttestationMode { pub fn has_tdx(&self) -> bool { match self { Self::DstackTdx => true, - Self::GcpTdx => true, - Self::DstackNitro => false, + Self::DstackGcpTdx => true, + Self::DstackNitroEnclave => false, } } - /// Check if TPM quote should be included - pub fn has_tpm(&self) -> bool { + /// Get TPM runtime event PCR index + pub fn tpm_runtime_pcr(&self) -> Option { match self { - Self::DstackTdx => false, - Self::GcpTdx => true, - Self::DstackNitro => true, + Self::DstackGcpTdx => Some(14), + Self::DstackTdx => None, + Self::DstackNitroEnclave => None, } } - /// Get TPM runtime event PCR index - pub fn tpm_runtime_pcr(&self) -> Option { + /// As string for debug + pub fn as_str(&self) -> &'static str { match self { - Self::GcpTdx => Some(14), - Self::DstackTdx => None, - Self::DstackNitro => None, + Self::DstackTdx => "dstack-tdx", + Self::DstackGcpTdx => "dstack-gcp-tdx", + Self::DstackNitroEnclave => "dstack-nitro-enclave", } } } @@ -188,27 +163,20 @@ impl QuoteContentType<'_> { /// Represents a verified attestation #[derive(Clone)] -pub struct DstackVerifiedReport { - /// The attestation mode - pub mode: AttestationMode, - /// The verified TDX report - pub tdx_report: Option, - /// The verified TPM report - pub tpm_report: Option, -} - -fn get_report_data(report: &TdxVerifiedReport) -> [u8; 64] { - match report.report { - Report::SgxEnclave(enclave_report) => enclave_report.report_data, - Report::TD10(tdreport10) => tdreport10.report_data, - Report::TD15(tdreport15) => tdreport15.base.report_data, - } +pub enum DstackVerifiedReport { + DstackTdx(TdxVerifiedReport), + DstackGcpTdx { + tdx_report: TdxVerifiedReport, + tpm_report: TpmVerifiedReport, + }, } impl DstackVerifiedReport { - /// Check if the report is empty - pub fn is_empty(&self) -> bool { - self.tdx_report.is_none() && self.tpm_report.is_none() + pub fn tdx_report(&self) -> Option<&TdxVerifiedReport> { + match self { + DstackVerifiedReport::DstackTdx(report) => Some(report), + DstackVerifiedReport::DstackGcpTdx { tdx_report, .. } => Some(tdx_report), + } } } @@ -224,6 +192,13 @@ pub struct TdxQuote { pub event_log: Vec, } +/// Represents an NSM (Nitro Security Module) attestation document +#[derive(Clone, Encode, Decode)] +pub struct NsmQuote { + /// The COSE Sign1 attestation document from NSM + pub document: Vec, +} + /// Represents a versioned attestation #[derive(Clone, Encode, Decode)] pub enum VersionedAttestation { @@ -255,7 +230,7 @@ impl VersionedAttestation { /// Strip data for certificate embedding (e.g. keep RTMR3 event logs only). pub fn into_stripped(mut self) -> Self { let VersionedAttestation::V0 { attestation } = &mut self; - if let Some(tdx_quote) = &mut attestation.tdx_quote { + if let Some(tdx_quote) = attestation.tdx_quote_mut() { tdx_quote.event_log = tdx_quote .event_log .iter() @@ -267,17 +242,39 @@ impl VersionedAttestation { } } -/// Attestation data #[derive(Clone, Encode, Decode)] -pub struct Attestation { - /// Attestation mode - pub mode: AttestationMode, +pub struct DstackGcpTdxQuote { + pub tdx_quote: TdxQuote, + pub tpm_quote: TpmQuote, +} - /// TDX quote (only for TDX mode) - pub tdx_quote: Option, +#[derive(Clone, Encode, Decode)] +pub struct DstackNitroQuote { + pub nsm_quote: Vec, +} + +#[derive(Clone, Encode, Decode)] +pub enum AttestationQuote { + DstackTdx(TdxQuote), + DstackGcpTdx(DstackGcpTdxQuote), + DstackNitroEnclave(DstackNitroQuote), +} - /// TPM quote (only for TPM mode) - pub tpm_quote: Option, +impl AttestationQuote { + pub fn mode(&self) -> AttestationMode { + match self { + AttestationQuote::DstackTdx { .. } => AttestationMode::DstackTdx, + AttestationQuote::DstackGcpTdx { .. } => AttestationMode::DstackGcpTdx, + AttestationQuote::DstackNitroEnclave { .. } => AttestationMode::DstackNitroEnclave, + } + } +} + +/// Attestation data +#[derive(Clone, Encode, Decode)] +pub struct Attestation { + /// The quote + pub quote: AttestationQuote, /// Runtime events (only for TDX mode) pub runtime_events: Vec, @@ -293,49 +290,70 @@ pub struct Attestation { } impl Attestation { + pub fn tdx_quote_mut(&mut self) -> Option<&mut TdxQuote> { + match &mut self.quote { + AttestationQuote::DstackTdx(quote) => Some(quote), + AttestationQuote::DstackGcpTdx(q) => Some(&mut q.tdx_quote), + AttestationQuote::DstackNitroEnclave(_) => None, + } + } + + pub fn tdx_quote(&self) -> Option<&TdxQuote> { + match &self.quote { + AttestationQuote::DstackTdx(quote) => Some(quote), + AttestationQuote::DstackGcpTdx(q) => Some(&q.tdx_quote), + AttestationQuote::DstackNitroEnclave(_) => None, + } + } + + pub fn tpm_quote(&self) -> Option<&TpmQuote> { + match &self.quote { + AttestationQuote::DstackTdx(_) => None, + AttestationQuote::DstackGcpTdx(q) => Some(&q.tpm_quote), + AttestationQuote::DstackNitroEnclave(_) => None, + } + } + /// Get TDX quote bytes pub fn get_tdx_quote_bytes(&self) -> Option> { - self.tdx_quote.as_ref().map(|q| q.quote.clone()) + self.tdx_quote().map(|q| q.quote.clone()) } /// Get TDX event log bytes pub fn get_tdx_event_log_bytes(&self) -> Option> { - self.tdx_quote - .as_ref() + self.tdx_quote() .map(|q| serde_json::to_vec(&q.event_log).unwrap_or_default()) } /// Get TDX event log string pub fn get_tdx_event_log_string(&self) -> Option { - self.tdx_quote - .as_ref() + self.tdx_quote() .map(|q| serde_json::to_string(&q.event_log).unwrap_or_default()) } pub fn get_td10_report(&self) -> Option { - self.tdx_quote - .as_ref() + self.tdx_quote() .and_then(|q| Quote::parse(&q.quote).ok()) .and_then(|quote| quote.report.as_td10().cloned()) } } -pub trait GetPpid { - fn get_ppid(&self) -> Vec; +pub trait GetDeviceId { + fn get_devide_id(&self) -> Vec; } -impl GetPpid for () { - fn get_ppid(&self) -> Vec { +impl GetDeviceId for () { + fn get_devide_id(&self) -> Vec { Vec::new() } } -impl GetPpid for DstackVerifiedReport { - fn get_ppid(&self) -> Vec { - let Some(tdx_report) = &self.tdx_report else { - return Vec::new(); - }; - tdx_report.ppid.clone() +impl GetDeviceId for DstackVerifiedReport { + fn get_devide_id(&self) -> Vec { + match self { + DstackVerifiedReport::DstackTdx(tdx_report) => tdx_report.ppid.to_vec(), + DstackVerifiedReport::DstackGcpTdx { tdx_report, .. } => tdx_report.ppid.to_vec(), + } } } @@ -344,15 +362,15 @@ struct Mrs { mr_aggregated: [u8; 32], } -impl Attestation { +impl Attestation { fn decode_mr_gcp_tpm( &self, boottime_mr: bool, mr_key_provider: &[u8], os_image_hash: &[u8], + tpm_quote: &TpmQuote, ) -> Result { let mr_system = sha256([os_image_hash, mr_key_provider]); - let tpm_quote = self.tpm_quote.as_ref().context("TPM quote not found")?; let pcr0 = tpm_quote .pcr_values .iter() @@ -372,30 +390,14 @@ impl Attestation { }) } - fn decode_mr_nitro_tpm( + fn decode_mr_nitro_nsm( &self, _boottime_mr: bool, mr_key_provider: &[u8], os_image_hash: &[u8], + nsm_quote: &[u8], ) -> Result { - let mr_system = sha256([os_image_hash, mr_key_provider]); - let tpm_quote = self.tpm_quote.as_ref().context("TPM quote not found")?; - let pcr0 = tpm_quote - .pcr_values - .iter() - .find(|p| p.index == 0) - .context("PCR 0 not found")?; - let pcr2 = tpm_quote - .pcr_values - .iter() - .find(|p| p.index == 2) - .context("PCR 2 not found")?; - let todo = "Maybe more pcrs"; - let mr_aggregated = sha256([&pcr0.value[..], &pcr2.value]); - Ok(Mrs { - mr_system, - mr_aggregated, - }) + todo!() } fn decode_mr_tdx(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { @@ -437,17 +439,25 @@ impl Attestation { }) } - fn decode_os_image_hash(&self) -> Result> { - if self.config.is_empty() { - return Ok(vec![]); + /// Decode the VM config from the external or embedded config + pub fn decode_vm_config<'a>(&'a self, mut config: &'a str) -> Result { + if config.is_empty() { + config = &self.config; + } + if config.is_empty() { + config = "{}"; } let vm_config: VmConfig = - serde_json::from_str(&self.config).context("Failed to parse vm config")?; - Ok(vm_config.os_image_hash) + serde_json::from_str(config).context("Failed to parse vm config")?; + Ok(vm_config) } /// Decode the app info from the event log pub fn decode_app_info(&self, boottime_mr: bool) -> Result { + self.decode_app_info_ex(boottime_mr, "") + } + + pub fn decode_app_info_ex(&self, boottime_mr: bool, vm_config: &str) -> Result { let key_provider_info = if boottime_mr { vec![] } else { @@ -459,22 +469,26 @@ impl Attestation { sha256(&key_provider_info) }; let os_image_hash = self - .decode_os_image_hash() - .context("Failed to decode os image hash")?; - let mrs = match self.mode { - AttestationMode::DstackTdx => self.decode_mr_tdx(boottime_mr, &mr_key_provider)?, - AttestationMode::GcpTdx => { - self.decode_mr_gcp_tpm(boottime_mr, &mr_key_provider, &os_image_hash)? - } - AttestationMode::DstackNitro => { - self.decode_mr_nitro_tpm(boottime_mr, &mr_key_provider, &os_image_hash)? + .decode_vm_config(vm_config) + .context("Failed to decode os image hash")? + .os_image_hash; + let mrs = match &self.quote { + AttestationQuote::DstackTdx(_) => self.decode_mr_tdx(boottime_mr, &mr_key_provider)?, + AttestationQuote::DstackGcpTdx(q) => { + self.decode_mr_gcp_tpm(boottime_mr, &mr_key_provider, &os_image_hash, &q.tpm_quote)? } + AttestationQuote::DstackNitroEnclave(q) => self.decode_mr_nitro_nsm( + boottime_mr, + &mr_key_provider, + &os_image_hash, + &q.nsm_quote, + )?, }; Ok(AppInfo { app_id: self.find_event_payload("app-id").unwrap_or_default(), compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), instance_id: self.find_event_payload("instance-id").unwrap_or_default(), - device_id: sha256(self.report.get_ppid()).to_vec(), + device_id: sha256(self.report.get_devide_id()).to_vec(), mr_system: mrs.mr_system, mr_aggregated: mrs.mr_aggregated, key_provider_info, @@ -486,7 +500,7 @@ impl Attestation { impl Attestation { /// Decode the quote pub fn decode_tdx_quote(&self) -> Result { - let Some(tdx_quote) = &self.tdx_quote else { + let Some(tdx_quote) = self.tdx_quote() else { bail!("tdx_quote not found"); }; Quote::parse(&tdx_quote.quote) @@ -539,13 +553,7 @@ impl Attestation { } } -#[cfg(feature = "quote")] impl Attestation { - /// Create an attestation for local machine (auto-detect mode) - pub fn local() -> Result { - Self::quote(&[0u8; 64]) - } - /// Reconstruct from tdx quote and event log, for backward compatibility pub fn from_tdx_quote(quote: Vec, event_log: &[u8]) -> Result { let tdx_eventlog: Vec = @@ -560,9 +568,7 @@ impl Attestation { report.report_data }; Ok(Attestation { - mode: AttestationMode::DstackTdx, - tpm_quote: None, - tdx_quote: Some(TdxQuote { + quote: AttestationQuote::DstackTdx(TdxQuote { quote, event_log: tdx_eventlog, }), @@ -572,39 +578,63 @@ impl Attestation { report: (), }) } +} + +#[cfg(feature = "quote")] +impl Attestation { + /// Create an attestation for local machine (auto-detect mode) + pub fn local() -> Result { + Self::quote(&[0u8; 64]) + } /// Create an attestation from a report data pub fn quote(report_data: &[u8; 64]) -> Result { let mode = AttestationMode::detect()?; let runtime_events = RuntimeEvent::read_all().context("Failed to read runtime events")?; - let tpm_qualifying_data; - let tdx_quote; - if mode.has_tdx() { - let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; - let event_log = - cc_eventlog::tdx::read_event_log().context("Failed to read event log")?; - tpm_qualifying_data = sha256("e); - tdx_quote = Some(TdxQuote { quote, event_log }); - } else { - tpm_qualifying_data = sha256(report_data); - tdx_quote = None; + + let quote = match mode { + AttestationMode::DstackTdx => { + let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; + let event_log = + cc_eventlog::tdx::read_event_log().context("Failed to read event log")?; + AttestationQuote::DstackTdx(TdxQuote { quote, event_log }) + } + AttestationMode::DstackGcpTdx => { + let quote = tdx_attest::get_quote(report_data).context("Failed to get quote")?; + let event_log = + cc_eventlog::tdx::read_event_log().context("Failed to read event log")?; + let tpm_qualifying_data = sha256("e); + let tdx_quote = TdxQuote { quote, event_log }; + let tpm_ctx = + tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; + let tpm_quote = tpm_ctx + .create_quote(&tpm_qualifying_data, &tpm_attest::dstack_pcr_policy()) + .context("Failed to create TPM quote")?; + AttestationQuote::DstackGcpTdx(DstackGcpTdxQuote { + tdx_quote, + tpm_quote, + }) + } + AttestationMode::DstackNitroEnclave => { + let nsm_quote = nsm_attest::get_attestation(report_data) + .context("Failed to get NSM attestation")?; + AttestationQuote::DstackNitroEnclave(DstackNitroQuote { nsm_quote }) + } }; - let tpm_quote = if mode.has_tpm() { - let tpm_ctx = tpm_attest::TpmContext::detect().context("Failed to open TPM context")?; - let quote = tpm_ctx - .create_quote(&tpm_qualifying_data, &tpm_attest::dstack_pcr_policy()) - .context("Failed to create TPM quote")?; - Some(quote) - } else { - None + let todo = "fill the os hash"; + let config = match mode { + AttestationMode::DstackTdx | AttestationMode::DstackGcpTdx => { + // TODO: Find a better way handling this hardcode path + fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default() + } + AttestationMode::DstackNitroEnclave => serde_json::to_string(&serde_json::json!({ + "os_image_hash": "", + })) + .unwrap(), }; - // TODO: Find a better way handling this hardcode path - let config = - fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default(); + Ok(Self { - mode, - tdx_quote, - tpm_quote, + quote, runtime_events, report_data: *report_data, config, @@ -634,100 +664,26 @@ impl Attestation { /// Verify the quote pub async fn verify(self, pccs_url: Option<&str>) -> Result { - let tpm_report = if self.mode.has_tpm() { - let report = self - .verify_tpm() - .await - .context("Failed to verify TPM quote")?; - let pcr_ind = self - .mode - .tpm_runtime_pcr() - .context("Failed to get runtime PCR no")?; - let replayed_rt_pcr = self.replay_runtime_events::(None); - let quoted_rt_pcr = report - .get_pcr(pcr_ind) - .context("No runtime PCR in TPM report")?; - if replayed_rt_pcr != quoted_rt_pcr[..] { - bail!( - "PCR{pcr_ind} mismatch, quoted: {}, replayed: {}", - hex::encode(quoted_rt_pcr), - hex::encode(replayed_rt_pcr), - ); + let report = match &self.quote { + AttestationQuote::DstackTdx(q) => { + let report = self.verify_tdx(pccs_url, &q.quote).await?; + DstackVerifiedReport::DstackTdx(report) } - Some(report) - } else { - None - }; - let tdx_report = if self.mode.has_tdx() { - let report = self.verify_tdx(pccs_url).await?; - let td_report = report.report.as_td10().context("no td report")?; - let replayed_rtmr = self.replay_runtime_events::(None); - if replayed_rtmr != td_report.rt_mr3 { - bail!( - "RTMR3 mismatch, quoted: {}, replayed: {}", - hex::encode(td_report.rt_mr3), - hex::encode(replayed_rtmr) - ); - } - Some(report) - } else { - None - }; - - match self.mode { - AttestationMode::DstackTdx => { - // For bare dstack TDX machine, we only make sure the report data is correct - let Some(tdx_report) = &tdx_report else { - bail!("TDX report is missing in dstack-tdx mode"); - }; - let td_report_data = get_report_data(tdx_report); - if td_report_data != self.report_data[..] { - bail!("tdx report_data mismatch"); + AttestationQuote::DstackGcpTdx(q) => { + let tdx_report = self.verify_tdx(pccs_url, &q.tdx_quote.quote).await?; + let tpm_report = self + .verify_tpm(&q.tpm_quote, &sha256(&q.tdx_quote.quote)) + .await + .context("Failed to verify TPM quote")?; + DstackVerifiedReport::DstackGcpTdx { + tdx_report, + tpm_report, } } - AttestationMode::GcpTdx => { - let Some(tdx_report) = &tdx_report else { - bail!("TDX report is missing in gcp-tdx mode"); - }; - let Some(td10_report) = tdx_report.report.as_td10() else { - bail!("TD10 report is missing in gcp-tdx mode"); - }; - if td10_report.report_data != self.report_data[..] { - bail!("tdx report_data mismatch"); - } - let tdx_quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; - let Some(tpm_report) = &tpm_report else { - bail!("TPM report is missing in gcp-tdx mode"); - }; - // TPM quote the TDX quote - let qualifying_data = sha256(tdx_quote); - if qualifying_data != tpm_report.attest.qualified_data[..] { - bail!("tpm qualified_data mismatch"); - } - } - AttestationMode::DstackNitro => { - let Some(tpm_report) = &tpm_report else { - bail!("TPM report is missing in dstack-nitro mode"); - }; - // TPM quote the TDX quote - let qualifying_data = sha256(self.report_data); - if qualifying_data != tpm_report.attest.qualified_data[..] { - bail!("tpm qualified_data mismatch"); - } - } - } - let report = DstackVerifiedReport { - mode: self.mode, - tdx_report, - tpm_report, + AttestationQuote::DstackNitroEnclave(quote) => todo!(), }; - if report.is_empty() { - bail!("nothing verified"); - } Ok(VerifiedAttestation { - mode: self.mode, - tdx_quote: self.tdx_quote, - tpm_quote: self.tpm_quote, + quote: self.quote, runtime_events: self.runtime_events, report_data: self.report_data, config: self.config, @@ -735,13 +691,35 @@ impl Attestation { }) } - async fn verify_tpm(&self) -> Result { - let tpm_quote = self.tpm_quote.as_ref().context("TPM quote missing")?; - tpm_qvl::get_collateral_and_verify(tpm_quote).await + async fn verify_tpm( + &self, + quote: &TpmQuote, + qualifying_data: &[u8], + ) -> Result { + let report = tpm_qvl::get_collateral_and_verify(quote).await?; + let pcr_ind = self + .quote + .mode() + .tpm_runtime_pcr() + .context("Failed to get runtime PCR no")?; + let replayed_rt_pcr = self.replay_runtime_events::(None); + let quoted_rt_pcr = report + .get_pcr(pcr_ind) + .context("No runtime PCR in TPM report")?; + if replayed_rt_pcr != quoted_rt_pcr[..] { + bail!( + "PCR{pcr_ind} mismatch, quoted: {}, replayed: {}", + hex::encode(quoted_rt_pcr), + hex::encode(replayed_rt_pcr), + ); + } + if report.attest.qualified_data != qualifying_data { + bail!("tpm qualified_data mismatch"); + } + Ok(report) } - async fn verify_tdx(&self, pccs_url: Option<&str>) -> Result { - let quote = &self.tdx_quote.as_ref().context("TDX quote missing")?.quote; + async fn verify_tdx(&self, pccs_url: Option<&str>, quote: &[u8]) -> Result { let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default()); if pccs_url.is_empty() { // try to read from PCCS_URL env var @@ -755,6 +733,20 @@ impl Attestation { .await .context("Failed to get collateral")?; validate_tcb(&tdx_report)?; + + let td_report = tdx_report.report.as_td10().context("no td report")?; + let replayed_rtmr = self.replay_runtime_events::(None); + if replayed_rtmr != td_report.rt_mr3 { + bail!( + "RTMR3 mismatch, quoted: {}, replayed: {}", + hex::encode(td_report.rt_mr3), + hex::encode(replayed_rtmr) + ); + } + + if td_report.report_data != self.report_data[..] { + bail!("tdx report_data mismatch"); + } Ok(tdx_report) } } diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index e9023405b..4af37cfc2 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -274,7 +274,7 @@ pub enum Platform { /// Google Cloud Platform Gcp, /// AWS Nitro Enclave - AwsNitroEnclave, + NitroEnclave, } impl Platform { @@ -282,7 +282,7 @@ impl Platform { pub fn detect() -> Option { // Nitro Enclave: NSM device exists only inside enclave if Path::new("/dev/nsm").exists() { - return Some(Self::AwsNitroEnclave); + return Some(Self::NitroEnclave); } if let Ok(board_name) = std::fs::read_to_string("/sys/class/dmi/id/product_name") { @@ -305,7 +305,7 @@ impl Platform { match self { Self::Dstack => "dstack", Self::Gcp => "gcp", - Self::AwsNitroEnclave => "aws-nitro-enclave", + Self::NitroEnclave => "aws-nitro-enclave", } } } diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index 88aeae4d4..6bcc270a6 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -55,7 +55,7 @@ impl HostApi { pub async fn notify(&self, event: &str, payload: &str) -> Result<()> { match Platform::detect_or_dstack() { Platform::Dstack => {} - Platform::Gcp | Platform::AwsNitroEnclave => { + Platform::Gcp | Platform::NitroEnclave => { // Skip notify on unsupported platforms return Ok(()); } diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index 6a0cc65ee..b3f31dbd7 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -426,9 +426,9 @@ fn cmd_attest_info(args: AttestInfoArgs) -> Result<()> { match attestation { VersionedAttestation::V0 { attestation } => { println!("version: V0"); - println!("mode: {:?}", attestation.mode); + println!("mode: {:?}", attestation.quote.mode()); println!("config_bytes: {}", attestation.config.len()); - match attestation.tdx_quote { + match attestation.tdx_quote() { Some(tdx) => { let event_log_json = serde_json::to_vec(&tdx.event_log) .context("Failed to serialize event log")?; @@ -442,7 +442,7 @@ fn cmd_attest_info(args: AttestInfoArgs) -> Result<()> { println!("event_log_json_bytes: 0"); } } - match attestation.tpm_quote { + match attestation.tpm_quote() { Some(tpm) => { let tpm_bytes = tpm.encode(); println!("tpm_quote_bytes: {}", tpm_bytes.len()); @@ -465,16 +465,15 @@ fn cmd_attest_json(args: AttestJsonArgs) -> Result<()> { let json = match attestation { VersionedAttestation::V0 { attestation } => { - let mode = serde_json::to_value(attestation.mode) - .context("Failed to serialize attestation mode")?; - let tdx_quote = match attestation.tdx_quote { + let mode = attestation.quote.mode().as_str(); + let tdx_quote = match attestation.tdx_quote() { Some(tdx) => serde_json::json!({ - "quote": hex::encode(tdx.quote), + "quote": hex::encode(&tdx.quote), "event_log": tdx.event_log, }), None => serde_json::Value::Null, }; - let tpm_quote = match attestation.tpm_quote { + let tpm_quote = match attestation.tpm_quote() { Some(tpm) => serde_json::to_value(tpm).context("Failed to serialize TPM quote")?, None => serde_json::Value::Null, }; diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 14f24ddfe..03967547c 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -165,8 +165,7 @@ pub async fn get_info(state: &AppState, external: bool) -> Result { .decode_app_info(false) .context("Failed to decode app info")?; let event_log = attestation - .tdx_quote - .as_ref() + .tdx_quote() .map(|q| &q.event_log[..]) .unwrap_or_default(); let tcb_info = if hide_tcb_info { @@ -452,13 +451,14 @@ fn simulate_quote( let VersionedAttestation::V0 { attestation } = VersionedAttestation::from_scale(&attestation_bytes) .context("Failed to decode simulator attestation")?; - let Some(mut quote) = attestation.tdx_quote else { + let mut attestation = attestation; + let Some(quote) = attestation.tdx_quote_mut() else { return Err(anyhow::anyhow!("Quote not found")); }; quote.quote[568..632].copy_from_slice(&report_data); Ok(GetQuoteResponse { - quote: quote.quote, + quote: quote.quote.to_vec(), event_log: serde_json::to_string("e.event_log) .context("Failed to serialize event log")?, report_data: report_data.to_vec(), diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 24b9e06df..94b61476e 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -11,7 +11,6 @@ use dstack_kms_rpc::{ GetMetaResponse, GetTempCaCertResponse, KmsKeyResponse, KmsKeys, PublicKeyResponse, SignCertRequest, SignCertResponse, }; -use dstack_types::VmConfig; use dstack_verifier::{CvmVerifier, VerificationDetails}; use fs_err as fs; use k256::ecdsa::SigningKey; @@ -23,7 +22,7 @@ use ra_tls::{ }; use scale::Decode; use sha2::Digest; -use tracing::{debug, info}; +use tracing::info; use upgrade_authority::BootInfo; use crate::{ @@ -92,7 +91,6 @@ pub struct RpcHandler { struct BootConfig { boot_info: BootInfo, gateway_app_id: String, - os_image_hash: Vec, } impl RpcHandler { @@ -170,17 +168,15 @@ impl RpcHandler { use_boottime_mr: bool, vm_config_str: &str, ) -> Result { - let Some(tdx_report) = &att.report.tdx_report else { + let todo = "Allow non tdx attestation"; + let Some(tdx_report) = &att.report.tdx_report() else { bail!("No TD report in attestation"); }; - debug!("vm_config: {vm_config_str}"); - let vm_config: VmConfig = - serde_json::from_str(vm_config_str).context("Failed to decode VM config")?; - let app_info = att.decode_app_info(use_boottime_mr)?; - let os_image_hash = vm_config.os_image_hash.clone(); + let app_info = att.decode_app_info_ex(use_boottime_mr, vm_config_str)?; + let todo = "Add attestation mode in BootInfo"; let boot_info = BootInfo { mr_aggregated: app_info.mr_aggregated.to_vec(), - os_image_hash: os_image_hash.clone(), + os_image_hash: app_info.os_image_hash, mr_system: app_info.mr_system.to_vec(), app_id: app_info.app_id, compose_hash: app_info.compose_hash, @@ -205,7 +201,6 @@ impl RpcHandler { Ok(BootConfig { boot_info, gateway_app_id: response.gateway_app_id, - os_image_hash, }) } @@ -238,7 +233,6 @@ impl KmsRpc for RpcHandler { let BootConfig { boot_info, gateway_app_id, - os_image_hash, } = self .ensure_app_boot_allowed(&request.vm_config) .await @@ -271,7 +265,7 @@ impl KmsRpc for RpcHandler { k256_signature, tproxy_app_id: gateway_app_id.clone(), gateway_app_id, - os_image_hash, + os_image_hash: boot_info.os_image_hash, }) } diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index eb5bb2c88..f9bbed7cc 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -28,9 +28,9 @@ use crate::oids::{ PHALA_RATLS_TDX_QUOTE, }; use crate::traits::CertExt; -use dstack_attest::attestation::{ - Attestation, AttestationMode, QuoteContentType, VersionedAttestation, -}; +#[cfg(feature = "quote")] +use dstack_attest::attestation::QuoteContentType; +use dstack_attest::attestation::{Attestation, AttestationQuote, VersionedAttestation}; /// A CA certificate and private key. pub struct CaCert { @@ -337,12 +337,9 @@ impl CertRequest<'_, Key> { } if let Some(ver_att) = self.attestation { let VersionedAttestation::V0 { attestation } = &ver_att; - match attestation.mode { - AttestationMode::DstackTdx => { + match &attestation.quote { + AttestationQuote::DstackTdx(tdx_quote) => { // For backward compatibility, we serialize the quote to the classic oids. - let Some(tdx_quote) = &attestation.tdx_quote else { - bail!("missing tdx quote") - }; let event_log = serde_json::to_vec(&tdx_quote.event_log) .context("Failed to serialize event log")?; add_ext(&mut params, PHALA_RATLS_TDX_QUOTE, &tdx_quote.quote); @@ -480,6 +477,7 @@ pub fn decompress_ext_value(data: &[u8]) -> Result> { } /// Generate a certificate with RA-TLS quote and event log. +#[cfg(feature = "quote")] pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result { use rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; diff --git a/tpm-qvl/src/lib.rs b/tpm-qvl/src/lib.rs index f7e81cb3b..b7c93c76c 100644 --- a/tpm-qvl/src/lib.rs +++ b/tpm-qvl/src/lib.rs @@ -28,7 +28,7 @@ pub const AWS_NITRO_ENCLAVES_ROOT_G1: &str = include_str!("../certs/AWS_NitroEnc pub fn get_root_ca(platform: Platform) -> Result<&'static str> { match platform { Platform::Gcp => Ok(GCP_ROOT_CA), - Platform::AwsNitroEnclave => Ok(AWS_NITRO_ENCLAVES_ROOT_G1), + Platform::NitroEnclave => Ok(AWS_NITRO_ENCLAVES_ROOT_G1), Platform::Dstack => bail!("dstack platform does not use TPM attestation"), } } diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 20d0ba977..5844a26e4 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -14,7 +14,7 @@ use dstack_mr::{RtmrLog, TdxMeasurementDetails, TdxMeasurements}; use dstack_types::VmConfig; use hex_literal::hex; use ra_tls::attestation::{ - Attestation, AttestationMode, TpmQuote, VerifiedAttestation, VersionedAttestation, + Attestation, AttestationQuote, TpmQuote, VerifiedAttestation, VersionedAttestation, }; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; @@ -413,11 +413,10 @@ impl CvmVerifier { let verified_attestation = match verified { Ok(att) => { details.quote_verified = true; - details.tcb_status = att.report.tdx_report.as_ref().map(|r| r.status.clone()); + details.tcb_status = att.report.tdx_report().map(|r| r.status.clone()); details.advisory_ids = att .report - .tdx_report - .as_ref() + .tdx_report() .map(|r| r.advisory_ids.clone()) .unwrap_or_default(); details.report_data = Some(hex::encode(att.report_data)); @@ -475,29 +474,24 @@ impl CvmVerifier { pub async fn verify_os_image_hash( &self, - mut vm_config: String, + vm_config: String, attestation: &VerifiedAttestation, debug: bool, details: &mut VerificationDetails, ) -> Result { - if vm_config.is_empty() { - vm_config = attestation.config.clone() - } - let vm_config: VmConfig = - serde_json::from_str(&vm_config).context("Failed to decode VM config JSON")?; - match attestation.mode { - AttestationMode::GcpTdx => { - let Some(tpm_quote) = &attestation.tpm_quote else { - bail!("No TPM quote"); - }; - self.verify_os_image_hash_for_gcp_tdx(&vm_config, tpm_quote) + let vm_config = attestation + .decode_vm_config(&vm_config) + .context("Failed to decode VM config")?; + match &attestation.quote { + AttestationQuote::DstackGcpTdx(quote) => { + self.verify_os_image_hash_for_gcp_tdx(&vm_config, "e.tpm_quote) .await?; } - AttestationMode::DstackTdx => { + AttestationQuote::DstackTdx(_) => { self.verify_os_image_hash_for_dstack_tdx(&vm_config, attestation, debug, details) .await?; } - AttestationMode::DstackNitro => { + AttestationQuote::DstackNitroEnclave(_) => { let todo = "Implement it"; bail!("Nitro not supported"); } @@ -512,10 +506,10 @@ impl CvmVerifier { debug: bool, details: &mut VerificationDetails, ) -> Result<()> { - let Some(report) = &attestation.report.tdx_report else { + let Some(report) = &attestation.report.tdx_report() else { bail!("No TDX report"); }; - let Some(tdx_quote) = &attestation.tdx_quote else { + let Some(tdx_quote) = attestation.tdx_quote() else { bail!("No TDX quote"); }; let event_log = &tdx_quote.event_log; From 440d59294c1bcb49eb5025b799faec08aefb2660 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 12:22:03 +0000 Subject: [PATCH 072/264] Add nsm-qvl --- Cargo.toml | 2 + nsm-qvl/Cargo.toml | 33 +++ nsm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem | 14 ++ nsm-qvl/src/lib.rs | 180 ++++++++++++++++ nsm-qvl/src/verify.rs | 227 ++++++++++++++++++++ nsm-qvl/tests/nitro_attestation.bin | Bin 0 -> 4682 bytes nsm-qvl/tests/verify_test.rs | 102 +++++++++ 7 files changed, 558 insertions(+) create mode 100644 nsm-qvl/Cargo.toml create mode 100644 nsm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem create mode 100644 nsm-qvl/src/lib.rs create mode 100644 nsm-qvl/src/verify.rs create mode 100644 nsm-qvl/tests/nitro_attestation.bin create mode 100644 nsm-qvl/tests/verify_test.rs diff --git a/Cargo.toml b/Cargo.toml index c9eee846e..0177162d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "tpm2", "tpm-types", "tpm-qvl", + "nsm-qvl", "dstack-attest", "dstack-util", "iohash", @@ -79,6 +80,7 @@ tpm2 = { path = "tpm2" } tpm-types = { path = "tpm-types" } dstack-attest = { path = "dstack-attest" } tpm-qvl = { path = "tpm-qvl" } +nsm-qvl = { path = "nsm-qvl" } certbot = { path = "certbot" } rocket-vsock-listener = { path = "rocket-vsock-listener" } host-api = { path = "host-api", default-features = false } diff --git a/nsm-qvl/Cargo.toml b/nsm-qvl/Cargo.toml new file mode 100644 index 000000000..41e6c5b8f --- /dev/null +++ b/nsm-qvl/Cargo.toml @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: © 2025 Phala Network +# +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nsm-qvl" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +description = "AWS Nitro Enclave NSM Quote Verification Library" + +[dependencies] +anyhow.workspace = true +hex.workspace = true +serde = { workspace = true, features = ["derive"] } +tracing.workspace = true + +# CBOR/COSE parsing +ciborium = "0.2" + +# Cryptographic verification +p384 = { version = "0.13", features = ["ecdsa"] } +sha2 = { workspace = true, features = ["oid"] } +pem = "3.0" + +# Certificate chain verification +x509-parser = "0.16" +rustls-webpki = { version = "0.103.8", features = ["alloc", "ring"] } +rustls-pki-types = "1.13.1" + +[dev-dependencies] +nsm-attest.workspace = true diff --git a/nsm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem b/nsm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem new file mode 100644 index 000000000..03fd45442 --- /dev/null +++ b/nsm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD +VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4 +MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL +DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG +BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb +48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE +h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF +R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC +MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW +rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N +IwLz3/Y= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/nsm-qvl/src/lib.rs b/nsm-qvl/src/lib.rs new file mode 100644 index 000000000..acbb58363 --- /dev/null +++ b/nsm-qvl/src/lib.rs @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! AWS Nitro Enclave NSM Quote Verification Library (QVL) +//! +//! This module provides quote verification for AWS Nitro Enclave attestation documents. +//! It verifies: +//! - COSE Sign1 signature using ECDSA P-384 with SHA-384 +//! - Certificate chain from the attestation document to the AWS Nitro root CA +//! +//! # Architecture +//! The verification follows AWS Nitro Enclave attestation document specification: +//! 1. Decode CBOR/COSE Sign1 structure +//! 2. Extract attestation document payload +//! 3. Verify certificate chain (cabundle + certificate against root CA) +//! 4. Verify COSE signature using the certificate's public key +//! +//! # References +//! - https://docs.aws.amazon.com/enclaves/latest/user/verify-root.html +//! - https://github.com/aws/aws-nitro-enclaves-nsm-api/blob/main/docs/attestation_process.md + +use anyhow::{bail, Context, Result}; +use serde::Deserialize; +use std::collections::BTreeMap; + +mod verify; + +pub use verify::{verify_attestation, verify_attestation_with_ca, NsmVerifiedReport}; + +/// AWS Nitro Enclaves Root CA certificate (G1) +/// +/// Subject: CN=aws.nitro-enclaves, C=US, O=Amazon, OU=AWS +/// Valid: 2019-10-28 to 2049-10-28 (30 years) +/// Fingerprint: 64:1A:03:21:A3:E2:44:EF:E4:56:46:31:95:D6:06:31:7E:D7:CD:CC:3C:17:56:E0:98:93:F3:C6:8F:79:BB:5B +pub const AWS_NITRO_ENCLAVES_ROOT_G1: &str = include_str!("../certs/AWS_NitroEnclaves_Root-G1.pem"); + +/// Parsed COSE Sign1 structure for NSM attestation +#[derive(Debug)] +pub struct CoseSign1 { + /// Protected header (contains algorithm) + pub protected: Vec, + /// Unprotected header (usually empty for NSM) + pub unprotected: BTreeMap, + /// Payload (CBOR-encoded attestation document) + pub payload: Vec, + /// Signature (ECDSA P-384) + pub signature: Vec, +} + +impl CoseSign1 { + /// Parse COSE Sign1 from raw bytes + pub fn from_bytes(data: &[u8]) -> Result { + // COSE Sign1 structure is a CBOR array: [protected, unprotected, payload, signature] + let value: ciborium::Value = + ciborium::from_reader(data).context("Failed to parse COSE Sign1 CBOR")?; + + let array = match value { + ciborium::Value::Array(arr) => arr, + ciborium::Value::Tag(18, inner) => { + // COSE_Sign1 tag is 18 + match *inner { + ciborium::Value::Array(arr) => arr, + _ => bail!("COSE Sign1 tag content is not an array"), + } + } + _ => bail!("COSE Sign1 is not an array"), + }; + + if array.len() != 4 { + bail!("COSE Sign1 array must have 4 elements, got {}", array.len()); + } + + let protected = match &array[0] { + ciborium::Value::Bytes(b) => b.clone(), + _ => bail!("COSE Sign1 protected header is not bytes"), + }; + + let unprotected = match &array[1] { + ciborium::Value::Map(m) => { + let mut map = BTreeMap::new(); + for (k, v) in m { + if let ciborium::Value::Integer(i) = k { + let key: i128 = (*i).into(); + map.insert(key as i64, v.clone()); + } + } + map + } + _ => BTreeMap::new(), + }; + + let payload = match &array[2] { + ciborium::Value::Bytes(b) => b.clone(), + _ => bail!("COSE Sign1 payload is not bytes"), + }; + + let signature = match &array[3] { + ciborium::Value::Bytes(b) => b.clone(), + _ => bail!("COSE Sign1 signature is not bytes"), + }; + + Ok(Self { + protected, + unprotected, + payload, + signature, + }) + } + + /// Get the algorithm from protected header + pub fn algorithm(&self) -> Result { + let protected_map: BTreeMap = + ciborium::from_reader(&self.protected[..]) + .context("Failed to parse protected header")?; + + // Algorithm is key 1 in COSE + let alg = protected_map + .get(&1) + .context("No algorithm in protected header")?; + + match alg { + ciborium::Value::Integer(i) => { + let val: i128 = (*i).into(); + Ok(val as i64) + } + _ => bail!("Algorithm is not an integer"), + } + } + + /// Build the Sig_structure for verification + /// Sig_structure = ["Signature1", protected, external_aad, payload] + pub fn sig_structure(&self) -> Result> { + let sig_structure = ciborium::Value::Array(vec![ + ciborium::Value::Text("Signature1".to_string()), + ciborium::Value::Bytes(self.protected.clone()), + ciborium::Value::Bytes(vec![]), // external_aad is empty + ciborium::Value::Bytes(self.payload.clone()), + ]); + + let mut buf = Vec::new(); + ciborium::into_writer(&sig_structure, &mut buf) + .context("Failed to encode Sig_structure")?; + Ok(buf) + } +} + +/// Attestation document structure (parsed from COSE payload) +#[derive(Debug, Clone, Deserialize)] +pub struct AttestationDocument { + /// Module ID + pub module_id: String, + /// Digest algorithm used + pub digest: String, + /// Timestamp (milliseconds since epoch) + pub timestamp: u64, + /// PCR values + pub pcrs: BTreeMap>, + /// Certificate (DER-encoded) - the signing certificate + pub certificate: Vec, + /// CA bundle (list of DER-encoded certificates) + /// Order: [ROOT_CERT, INTERM_1, INTERM_2, ..., INTERM_N] + pub cabundle: Vec>, + /// Optional public key + #[serde(default)] + pub public_key: Option>, + /// Optional user data + #[serde(default)] + pub user_data: Option>, + /// Optional nonce + #[serde(default)] + pub nonce: Option>, +} + +impl AttestationDocument { + /// Parse attestation document from CBOR payload + pub fn from_cbor(data: &[u8]) -> Result { + ciborium::from_reader(data).context("Failed to parse attestation document CBOR") + } +} diff --git a/nsm-qvl/src/verify.rs b/nsm-qvl/src/verify.rs new file mode 100644 index 000000000..1f995cbe2 --- /dev/null +++ b/nsm-qvl/src/verify.rs @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! NSM Attestation Verification Module + +use std::time::SystemTime; + +use anyhow::{bail, Context, Result}; +use p384::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; +use rustls_pki_types::{CertificateDer, UnixTime}; +use sha2::{Digest, Sha384}; +use tracing::debug; +use webpki::EndEntityCert; +use x509_parser::prelude::*; + +use crate::{AttestationDocument, CoseSign1}; + +/// Verified NSM attestation report +#[derive(Debug, Clone)] +pub struct NsmVerifiedReport { + /// Module ID + pub module_id: String, + /// Digest algorithm + pub digest: String, + /// Timestamp (milliseconds since epoch) + pub timestamp: u64, + /// PCR values + pub pcrs: std::collections::BTreeMap>, + /// User data from attestation + pub user_data: Option>, + /// Nonce from attestation + pub nonce: Option>, + /// Public key from attestation + pub public_key: Option>, +} + +/// Verify Nitro attestation with custom root CA (for testing) +pub fn verify_attestation_with_ca( + cose_sign1_bytes: &[u8], + root_ca_pem: &str, +) -> Result { + verify_attestation(cose_sign1_bytes, root_ca_pem, None) +} + +/// Verify Nitro attestation with custom root CA and custom time (for testing) +pub fn verify_attestation( + cose_sign1_bytes: &[u8], + root_ca_pem: &str, + now: Option, +) -> Result { + let cose = CoseSign1::from_bytes(cose_sign1_bytes).context("Failed to parse COSE Sign1")?; + let alg = cose.algorithm().context("Failed to get algorithm")?; + if alg != -35 { + bail!("Unsupported COSE algorithm: {alg}. Expected -35 (ES384)"); + } + let doc = AttestationDocument::from_cbor(&cose.payload) + .context("Failed to parse attestation document")?; + verify_certificate_chain(&doc, root_ca_pem, now) + .context("Certificate chain verification failed")?; + verify_cose_signature(&cose, &doc.certificate).context("COSE signature verification failed")?; + + Ok(NsmVerifiedReport { + module_id: doc.module_id, + digest: doc.digest, + timestamp: doc.timestamp, + pcrs: doc.pcrs, + user_data: doc.user_data, + nonce: doc.nonce, + public_key: doc.public_key, + }) +} + +/// Verify the certificate chain from attestation document +fn verify_certificate_chain( + doc: &AttestationDocument, + root_ca_pem: &str, + now_override: Option, +) -> Result<()> { + // Parse root CA from PEM + let root_ca_der = parse_pem_cert(root_ca_pem).context("Failed to parse root CA PEM")?; + + // The cabundle order is: [ROOT_CERT, INTERM_1, INTERM_2, ..., INTERM_N] + // We need to verify: TARGET_CERT <- INTERM_N <- ... <- INTERM_1 <- ROOT_CERT + // But we use the verifier-provided root CA, not the one from cabundle + + // Build intermediate chain from cabundle (excluding root at index 0) + let intermediates: Vec> = doc + .cabundle + .iter() + .skip(1) // Skip the root cert from cabundle, use verifier-provided root + .map(|der| CertificateDer::from(der.clone())) + .collect(); + + debug!( + "Certificate chain: 1 leaf + {} intermediates + 1 root", + intermediates.len() + ); + + // Parse the leaf certificate (signing certificate) + let leaf_cert_der = CertificateDer::from(doc.certificate.clone()); + let leaf_cert = + EndEntityCert::try_from(&leaf_cert_der).context("Failed to parse leaf certificate")?; + + // Log certificate info + if let Ok((_, cert)) = X509Certificate::from_der(&doc.certificate) { + debug!( + "Leaf certificate: subject={}, issuer={}", + cert.subject(), + cert.issuer() + ); + } + + // Create trust anchor from root CA + let root_cert_der = CertificateDer::from(root_ca_der); + let trust_anchor = webpki::anchor_from_trusted_cert(&root_cert_der) + .context("Failed to create trust anchor from root CA")?; + + if let Ok((_, cert)) = X509Certificate::from_der(root_cert_der.as_ref()) { + debug!( + "Root CA: subject={}, issuer={}", + cert.subject(), + cert.issuer() + ); + } + + // Get current time + let now = now_override.unwrap_or(SystemTime::now()); + let now = now + .duration_since(std::time::UNIX_EPOCH) + .context("Failed to get current time")?; + let time = UnixTime::since_unix_epoch(now); + + // Verify certificate chain + // Note: AWS Nitro Enclaves don't use CRL, so we disable revocation checking + let trust_anchors = [trust_anchor]; + + leaf_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &trust_anchors, + &intermediates, + time, + webpki::KeyUsage::client_auth(), + None, // No revocation checking + None, + ) + .context("Certificate chain verification failed")?; + + Ok(()) +} + +/// Verify COSE signature using the certificate's public key +fn verify_cose_signature(cose: &CoseSign1, cert_der: &[u8]) -> Result<()> { + // Extract public key from certificate + let (_, cert) = + X509Certificate::from_der(cert_der).context("Failed to parse signing certificate")?; + + let spki = cert.public_key(); + let public_key_bytes = spki.subject_public_key.data.as_ref(); + + // Parse as P-384 public key + let verifying_key = VerifyingKey::from_sec1_bytes(public_key_bytes) + .context("Failed to parse P-384 public key from certificate")?; + + // Build Sig_structure for verification + let sig_structure = cose + .sig_structure() + .context("Failed to build Sig_structure")?; + + // Hash the Sig_structure with SHA-384 + let mut hasher = Sha384::new(); + hasher.update(&sig_structure); + let message_hash = hasher.finalize(); + + // Parse signature (P-384 signature is 96 bytes: 48 bytes r + 48 bytes s) + if cose.signature.len() != 96 { + bail!( + "Invalid P-384 signature length: {} (expected 96)", + cose.signature.len() + ); + } + + let signature = + Signature::from_slice(&cose.signature).context("Failed to parse ECDSA signature")?; + + // Verify signature + verifying_key + .verify_prehash(&message_hash, &signature) + .context("ECDSA signature verification failed")?; + + Ok(()) +} + +/// Parse a PEM certificate to DER +fn parse_pem_cert(pem_str: &str) -> Result> { + let pem_block = ::pem::parse(pem_str).context("Failed to parse PEM")?; + if pem_block.tag() != "CERTIFICATE" { + bail!("PEM is not a certificate: {}", pem_block.tag()); + } + Ok(pem_block.into_contents()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_root_ca_parsing() { + let der = parse_pem_cert(AWS_NITRO_ENCLAVES_ROOT_G1).expect("Failed to parse root CA"); + let (_, cert) = X509Certificate::from_der(&der).expect("Failed to parse X509"); + + // Verify it's the AWS Nitro Enclaves root CA + let subject = cert.subject().to_string(); + assert!( + subject.contains("aws.nitro-enclaves"), + "Subject should contain aws.nitro-enclaves: {}", + subject + ); + assert!( + subject.contains("Amazon"), + "Subject should contain Amazon: {}", + subject + ); + assert!(cert.is_ca()); + } +} diff --git a/nsm-qvl/tests/nitro_attestation.bin b/nsm-qvl/tests/nitro_attestation.bin new file mode 100644 index 0000000000000000000000000000000000000000..676305f1b723ae1d111cfb5891918749050fc2d8 GIT binary patch literal 4682 zcmd6qc~lek7RQrG*f)X7A_Z$u#3lHfWF`xW0zt%w)heO^wQ-V}EJ6q+0YRTYC|IZz z7YYj6ibbuURuFs^9^h8+Aqqw7F5-?AK?SO^Sl&d`LdEw^PW#^Job&tR-prl3ncwGo z@BLArO8;cP9aJ78FT`4KM4?pS(cy$hfya(lFaeLnCAfS(A>p&wEF3|YM1%yud@+h* z9G)0w5jY~2;tCm|)<}Z_eAqlhp;0JF3sXiqQ7F`{!Miga;E|FjbrD4fmL7((1Y?qv zar_Hcl5)Sah)rlkPv95?_iegE$}nd zZhp2ew{7RN-eRFf-5STvg^1Qb0lx)7{JHT$>5 zr#}1?y`iEHPA7ZJ0h?V{!VG<%rqmU{jn1%>};z1I$id@t@8bpPl z5jth9JKGkh16T1OuN#;e8c$psQ13;j(SSAsHUlPxv|zfSEfgFC+X8EyVQg-?7*op? z5i*S`0v-V@bxtF5qaXs)Xu=8D3Rvhorsjq|N^BK*i#hpJ(qU#!^I04O^8r_9(agax zg~20Q%_K0jh6#IME7cwm3Qd$sS1iJ@7(xwP>@8R*%pw;XM%hT{8wY}_ zL1vXzd6CGB-4U6yZbnRAf^D0Zq$ZL0=j8Od)Hw~w+fsT8mwA7?{6=DG-O@%E#+HiW zQ$KCoUtj;dxqa?TH{%!|#Mt-dTtrzi>N#U(}qGk`EXie#cqvcK;4%~G1KV5pK<@ut)8G_kEclN(9 z<^&uEw#;UDieNqrSUi}`;sKPT2pKSum*$`Os&{OCAGEM_xACz~dE~9!D;ui(d8|$9 zh_t}|r%{*F`~p9ke;3(V{PgPWqZPtYti#1IMwZuYF67R5FvdyRvY=s2W0Q$UWgFRL zZ!EZcZ3K19odyZj-9+m5S#6v+8GKkhqbuJRcmu09TW`ibK>0 zqO|w=`WuGMD}o#owq5zSQ{9gqomo!^v|tycu+8&s_~2pt-*Yzxy$2~juX?7mEIf2~#iPssY4I^QY-H9> zr{=Ss`P%b9dluNdfyNjZk}Qx-0*u!ATt;T~$X2lld&=(H?RDW{Om zDVP}HbAUv`gnCa}EEP&qLr0OlkHpxx?<=}6x^4ZE?d)Aa zi>4ln?^zk1>1`B{v%0qAYOP6idX72wiS}v_R%=;4<<6xzg&yJ5? zB)6Xg-Egd3SA*VuVM)p8FtDm*hGtG2xmkLS{A#g7$NHN7P`*`9%C~D4-Wexa5g9EGS4c!*MBGzFw3>(#;g|*!dh1S7f8-(} zR1p&5X}@rJ(WBVFhscW6kH71(aOsTq9vemt9-o$~ofj9o_`sZ|X8O}P_Aql@XM@#> zgE2ftS}QEB$iCUF==F3I$W4q)Gw%|mnf3m^C?fh@SqBa3@U;8`Wq@&ODqP%^r|3KO zN3whwJD<9mp;C(!N=!zG &[u8] { + // Find COSE Sign1 structure (starts with 0x84 for 4-element array) + for i in 0..data.len().saturating_sub(2) { + if data[i] == 0x84 && data[i + 1] == 0x44 { + return &data[i..]; + } + } + panic!("Could not find COSE Sign1 marker in attestation data"); +} + +#[test] +fn test_parse_cose_sign1() { + let cose_data = extract_cose_sign1(ATTESTATION_BIN); + + let cose = CoseSign1::from_bytes(cose_data).expect("Failed to parse COSE Sign1"); + + // Verify algorithm is ES384 (-35) + let alg = cose.algorithm().expect("Failed to get algorithm"); + assert_eq!(alg, -35, "Algorithm should be ES384 (-35)"); + + // Verify signature is 96 bytes (P-384) + assert_eq!( + cose.signature.len(), + 96, + "P-384 signature should be 96 bytes" + ); + + println!("COSE Sign1 parsed successfully"); + println!(" Protected header: {} bytes", cose.protected.len()); + println!(" Payload: {} bytes", cose.payload.len()); + println!(" Signature: {} bytes", cose.signature.len()); +} + +#[test] +fn test_parse_attestation_document() { + let cose_data = extract_cose_sign1(ATTESTATION_BIN); + let cose = CoseSign1::from_bytes(cose_data).expect("Failed to parse COSE Sign1"); + + let doc = AttestationDocument::from_cbor(&cose.payload) + .expect("Failed to parse attestation document"); + + println!("Attestation document parsed:"); + println!(" Module ID: {}", doc.module_id); + println!(" Digest: {}", doc.digest); + println!(" Timestamp: {}", doc.timestamp); + println!(" Certificate: {} bytes", doc.certificate.len()); + println!(" CA bundle: {} certificates", doc.cabundle.len()); + println!(" PCRs: {} entries", doc.pcrs.len()); + + assert!(!doc.module_id.is_empty()); + assert_eq!(doc.digest, "SHA384"); + assert!(!doc.certificate.is_empty()); + assert!(!doc.cabundle.is_empty()); +} + +#[test] +fn test_verify_attestation_full() { + let cose_data = extract_cose_sign1(ATTESTATION_BIN); + + // This test verifies the full attestation: + // 1. COSE Sign1 signature verification + // 2. Certificate chain verification against AWS Nitro root CA + let result = verify_attestation(cose_data); + + match result { + Ok(report) => { + println!("✓ Attestation verified successfully!"); + println!(" Module ID: {}", report.module_id); + println!(" Digest: {}", report.digest); + println!(" Timestamp: {}", report.timestamp); + println!(" PCRs: {} entries", report.pcrs.len()); + + // Print non-zero PCR values + for (idx, value) in &report.pcrs { + if !value.iter().all(|&b| b == 0) { + println!(" PCR{}: {:02x?}", idx, value); + } + } + } + Err(e) => { + // The test attestation may be expired or have other issues + // Print the error for debugging but don't fail the test + // since the attestation document is from a real enclave + // and may have time-based validity constraints + println!( + "Attestation verification failed (may be expected for old attestations): {:#}", + e + ); + + // Still verify we can parse the structure + let cose = CoseSign1::from_bytes(cose_data).expect("Should parse COSE"); + let _doc = AttestationDocument::from_cbor(&cose.payload) + .expect("Should parse attestation document"); + } + } +} From 349595ab30b307da937f996bdef7c46ae63a59b3 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 13:04:47 +0000 Subject: [PATCH 073/264] Add nitro enclave attestation --- Cargo.lock | 97 ++++++- dstack-attest/Cargo.toml | 7 +- dstack-attest/src/attestation.rs | 262 ++++++++++++++---- dstack-attest/tests/nitro_attestation.bin | Bin 0 -> 4709 bytes dstack-attest/tests/nitro_attestation_dbg.bin | Bin 0 -> 4684 bytes dstack-attest/tests/nitro_verify.rs | 40 +++ .../snapshots/nitro_verify__app_info.snap | 15 + .../snapshots/nitro_verify__nitro_report.snap | 15 + dstack-util/src/main.rs | 52 +++- kms/src/main_service.rs | 20 +- kms/src/main_service/upgrade_authority.rs | 2 + ra-tls/src/cert.rs | 13 +- tdx-attest/src/dummy.rs | 4 +- tdx-attest/src/lib.rs | 8 +- tpm-qvl/src/lib.rs | 5 +- verifier/Cargo.toml | 2 + verifier/src/verification.rs | 29 +- 17 files changed, 484 insertions(+), 87 deletions(-) create mode 100644 dstack-attest/tests/nitro_attestation.bin create mode 100644 dstack-attest/tests/nitro_attestation_dbg.bin create mode 100644 dstack-attest/tests/nitro_verify.rs create mode 100644 dstack-attest/tests/snapshots/nitro_verify__app_info.snap create mode 100644 dstack-attest/tests/snapshots/nitro_verify__nitro_report.snap diff --git a/Cargo.lock b/Cargo.lock index e1ec07386..4ee1f3c2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -893,6 +893,20 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "aws-nitro-enclaves-nsm-api" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92c1f4471b33f6a7af9ea421b249ed18a11c71156564baf6293148fa6ad1b09" +dependencies = [ + "libc", + "log", + "nix 0.26.4", + "serde", + "serde_bytes", + "serde_cbor", +] + [[package]] name = "axum" version = "0.8.7" @@ -1385,7 +1399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half", + "half 2.7.1", ] [[package]] @@ -2162,9 +2176,12 @@ dependencies = [ "dstack-types", "ez-hash", "fs-err", + "futures", "hex", "hex_fmt", + "insta", "nsm-attest", + "nsm-qvl", "or-panic", "parity-scale-codec", "serde", @@ -2506,10 +2523,12 @@ dependencies = [ "dcap-qvl", "dstack-mr", "dstack-types", + "ez-hash", "figment", "fs-err", "hex", "hex-literal 1.1.0", + "nsm-attest", "ra-tls", "reqwest", "rocket", @@ -2672,6 +2691,7 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core 0.6.4", @@ -3256,6 +3276,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "half" version = "2.7.1" @@ -3959,7 +3985,7 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" dependencies = [ - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -4349,6 +4375,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -4537,6 +4572,19 @@ dependencies = [ "log", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + [[package]] name = "nix" version = "0.29.0" @@ -4559,7 +4607,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset", + "memoffset 0.9.1", ] [[package]] @@ -4608,14 +4656,31 @@ name = "nsm-attest" version = "0.5.5" dependencies = [ "anyhow", + "aws-nitro-enclaves-nsm-api", "ciborium", "hex", - "nix 0.29.0", "serde", - "serde_bytes", "tracing", ] +[[package]] +name = "nsm-qvl" +version = "0.5.5" +dependencies = [ + "anyhow", + "ciborium", + "hex", + "nsm-attest", + "p384", + "pem", + "rustls-pki-types", + "rustls-webpki 0.103.8", + "serde", + "sha2 0.10.9", + "tracing", + "x509-parser", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -4851,6 +4916,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parcelona" version = "0.4.3" @@ -6705,6 +6782,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml index e66d2903d..d772ebe87 100644 --- a/dstack-attest/Cargo.toml +++ b/dstack-attest/Cargo.toml @@ -28,8 +28,13 @@ sha3.workspace = true tdx-attest.workspace = true tpm-attest.workspace = true nsm-attest.workspace = true +nsm-qvl.workspace = true tpm-qvl.workspace = true tpm-types.workspace = true +insta.workspace = true [features] -quote = [] \ No newline at end of file +quote = [] + +[dev-dependencies] +futures = { workspace = true } diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index b6f806cd1..841c84ee9 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -4,7 +4,7 @@ //! Attestation functions -use std::borrow::Cow; +use std::{borrow::Cow, time::SystemTime}; use anyhow::{anyhow, bail, Context, Result}; use cc_eventlog::{RuntimeEvent, TdxEvent}; @@ -16,7 +16,7 @@ use dstack_types::{Platform, VmConfig}; use ez_hash::{sha256, Hasher, Sha256, Sha384}; use or_panic::ResultOrPanic; use scale::{Decode, Encode}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; use sha2::Digest as _; use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; @@ -24,15 +24,22 @@ use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; // Re-export TpmQuote from tpm-types pub use tpm_types::TpmQuote; +const DSTACK_TDX: &str = "dstack-tdx"; +const DSTACK_GCP_TDX: &str = "dstack-gcp-tdx"; +const DSTACK_NITRO_ENCLAVE: &str = "dstack-nitro-enclave"; + /// Attestation mode -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)] pub enum AttestationMode { /// Intel TDX with DCAP quote only #[default] + #[serde(rename = "dstack-tdx")] DstackTdx, /// GCP TDX with DCAP quote only + #[serde(rename = "dstack-gcp-tdx")] DstackGcpTdx, /// Dstack attestation SDK in AWS Nitro Enclave + #[serde(rename = "dstack-nitro-enclave")] DstackNitroEnclave, } @@ -82,9 +89,18 @@ impl AttestationMode { /// As string for debug pub fn as_str(&self) -> &'static str { match self { - Self::DstackTdx => "dstack-tdx", - Self::DstackGcpTdx => "dstack-gcp-tdx", - Self::DstackNitroEnclave => "dstack-nitro-enclave", + Self::DstackTdx => DSTACK_TDX, + Self::DstackGcpTdx => DSTACK_GCP_TDX, + Self::DstackNitroEnclave => DSTACK_NITRO_ENCLAVE, + } + } + + /// Returns true if the attestation mode supports composability (OS image + runtime loadable application) + pub fn is_composable(&self) -> bool { + match self { + Self::DstackTdx => true, + Self::DstackGcpTdx => true, + Self::DstackNitroEnclave => false, } } } @@ -161,6 +177,20 @@ impl QuoteContentType<'_> { } } +/// Verified Nitro Enclave attestation report +#[derive(Clone, Debug, Serialize)] +pub struct NitroVerifiedReport { + /// Module ID + pub module_id: String, + /// PCR0 - Enclave image hash + pub pcrs: NitroPcrs, + /// User data from attestation + #[serde(with = "serde_human_bytes")] + pub user_data: Vec, + /// Timestamp + pub timestamp: u64, +} + /// Represents a verified attestation #[derive(Clone)] pub enum DstackVerifiedReport { @@ -169,6 +199,7 @@ pub enum DstackVerifiedReport { tdx_report: TdxVerifiedReport, tpm_report: TpmVerifiedReport, }, + DstackNitroEnclave(NitroVerifiedReport), } impl DstackVerifiedReport { @@ -176,6 +207,7 @@ impl DstackVerifiedReport { match self { DstackVerifiedReport::DstackTdx(report) => Some(report), DstackVerifiedReport::DstackGcpTdx { tdx_report, .. } => Some(tdx_report), + DstackVerifiedReport::DstackNitroEnclave(_) => None, } } } @@ -253,6 +285,49 @@ pub struct DstackNitroQuote { pub nsm_quote: Vec, } +#[derive(Clone, Debug, Serialize)] +pub struct NitroPcrs { + #[serde(with = "serde_human_bytes")] + pub pcr0: Vec, + #[serde(with = "serde_human_bytes")] + pub pcr1: Vec, + #[serde(with = "serde_human_bytes")] + pub pcr2: Vec, +} + +impl NitroPcrs { + fn is_zero(&self) -> bool { + self.pcr0.iter().all(|&b| b == 0) + && self.pcr1.iter().all(|&b| b == 0) + && self.pcr2.iter().all(|&b| b == 0) + } +} + +impl DstackNitroQuote { + pub fn decode_cose(&self) -> Result { + nsm_attest::AttestationDocument::from_cose(&self.nsm_quote) + .context("Failed to decode NSM attestation document") + } + + pub fn decode_image_hash(&self) -> Result> { + let pcrs = self.decode_pcrs()?; + let hash = if pcrs.is_zero() { + [0u8; 32] + } else { + sha256([&pcrs.pcr0, &pcrs.pcr1, &pcrs.pcr2]) + }; + Ok(hash.to_vec()) + } + + pub fn decode_pcrs(&self) -> Result { + let doc = self.decode_cose()?; + let pcr0 = doc.pcrs.get(&0).cloned().context("PCR 0 not found")?; + let pcr1 = doc.pcrs.get(&1).cloned().context("PCR 1 not found")?; + let pcr2 = doc.pcrs.get(&2).cloned().context("PCR 2 not found")?; + Ok(NitroPcrs { pcr0, pcr1, pcr2 }) + } +} + #[derive(Clone, Encode, Decode)] pub enum AttestationQuote { DstackTdx(TdxQuote), @@ -353,6 +428,14 @@ impl GetDeviceId for DstackVerifiedReport { match self { DstackVerifiedReport::DstackTdx(tdx_report) => tdx_report.ppid.to_vec(), DstackVerifiedReport::DstackGcpTdx { tdx_report, .. } => tdx_report.ppid.to_vec(), + DstackVerifiedReport::DstackNitroEnclave(report) => { + // i-1234567890abcdef0-enc9876543210abcde -> i-1234567890abcdef0 + report + .module_id + .split_once('-') + .map(|(id, _)| id.as_bytes().to_vec()) + .unwrap_or_default() + } } } } @@ -390,18 +473,27 @@ impl Attestation { }) } - fn decode_mr_nitro_nsm( + fn decode_mr_nitro_nsm(&self, nsm_quote: &DstackNitroQuote) -> Result { + // Parse NSM attestation document to get PCR values + let pcrs = nsm_quote.decode_pcrs()?; + + // Compute mr_system from PCR values and mr_key_provider + let mr_system = sha256([&pcrs.pcr0, &pcrs.pcr1, &pcrs.pcr2]); + let mr_aggregated = mr_system; + + Ok(Mrs { + mr_system, + mr_aggregated, + }) + } + + fn decode_mr_tdx( &self, - _boottime_mr: bool, + boottime_mr: bool, mr_key_provider: &[u8], - os_image_hash: &[u8], - nsm_quote: &[u8], + tdx_quote: &TdxQuote, ) -> Result { - todo!() - } - - fn decode_mr_tdx(&self, boottime_mr: bool, mr_key_provider: &[u8]) -> Result { - let quote = self.decode_tdx_quote()?; + let quote = Quote::parse(&tdx_quote.quote).context("Failed to parse quote")?; let rtmr3 = self.replay_runtime_events::(boottime_mr.then_some("boot-mr-done")); let td_report = quote.report.as_td10().context("TDX report not found")?; let mr_system = sha256([ @@ -445,6 +537,7 @@ impl Attestation { config = &self.config; } if config.is_empty() { + // No vm config for nitro enclave config = "{}"; } let vm_config: VmConfig = @@ -473,26 +566,28 @@ impl Attestation { .context("Failed to decode os image hash")? .os_image_hash; let mrs = match &self.quote { - AttestationQuote::DstackTdx(_) => self.decode_mr_tdx(boottime_mr, &mr_key_provider)?, + AttestationQuote::DstackTdx(q) => { + self.decode_mr_tdx(boottime_mr, &mr_key_provider, q)? + } AttestationQuote::DstackGcpTdx(q) => { self.decode_mr_gcp_tpm(boottime_mr, &mr_key_provider, &os_image_hash, &q.tpm_quote)? } - AttestationQuote::DstackNitroEnclave(q) => self.decode_mr_nitro_nsm( - boottime_mr, - &mr_key_provider, - &os_image_hash, - &q.nsm_quote, - )?, + AttestationQuote::DstackNitroEnclave(q) => self.decode_mr_nitro_nsm(q)?, + }; + let compose_hash = if self.quote.mode().is_composable() { + self.find_event_payload("compose-hash").unwrap_or_default() + } else { + os_image_hash.clone() }; Ok(AppInfo { app_id: self.find_event_payload("app-id").unwrap_or_default(), - compose_hash: self.find_event_payload("compose-hash").unwrap_or_default(), instance_id: self.find_event_payload("instance-id").unwrap_or_default(), device_id: sha256(self.report.get_devide_id()).to_vec(), mr_system: mrs.mr_system, mr_aggregated: mrs.mr_aggregated, key_provider_info, os_image_hash, + compose_hash, }) } } @@ -589,8 +684,18 @@ impl Attestation { /// Create an attestation from a report data pub fn quote(report_data: &[u8; 64]) -> Result { + Self::quote_with_app_id(report_data, None) + } + + pub fn quote_with_app_id(report_data: &[u8; 64], app_id: Option<[u8; 20]>) -> Result { let mode = AttestationMode::detect()?; - let runtime_events = RuntimeEvent::read_all().context("Failed to read runtime events")?; + let runtime_events = if mode.is_composable() { + RuntimeEvent::read_all().context("Failed to read runtime events")? + } else if let Some(app_id) = app_id { + vec![RuntimeEvent::new("app-id".to_string(), app_id.to_vec())] + } else { + vec![] + }; let quote = match mode { AttestationMode::DstackTdx => { @@ -621,16 +726,20 @@ impl Attestation { AttestationQuote::DstackNitroEnclave(DstackNitroQuote { nsm_quote }) } }; - let todo = "fill the os hash"; - let config = match mode { - AttestationMode::DstackTdx | AttestationMode::DstackGcpTdx => { + let config = match "e { + AttestationQuote::DstackTdx(_) | AttestationQuote::DstackGcpTdx(_) => { // TODO: Find a better way handling this hardcode path fs_err::read_to_string("/dstack/.host-shared/.sys-config.json").unwrap_or_default() } - AttestationMode::DstackNitroEnclave => serde_json::to_string(&serde_json::json!({ - "os_image_hash": "", - })) - .unwrap(), + AttestationQuote::DstackNitroEnclave(quote) => { + let os_image_hash = quote + .decode_image_hash() + .context("Failed to decode image hash")?; + serde_json::to_string(&serde_json::json!({ + "os_image_hash": hex::encode(os_image_hash), + })) + .context("Failed to serialize config")? + } }; Ok(Self { @@ -644,26 +753,12 @@ impl Attestation { } impl Attestation { - /// Wrap into a versioned attestation for encoding - pub fn into_versioned(self) -> VersionedAttestation { - VersionedAttestation::V0 { attestation: self } - } - - /// Verify the quote - pub async fn verify_with_ra_pubkey( + /// Verify the quote with optional custom time (testing hook) + pub async fn verify_with_time( self, - ra_pubkey_der: &[u8], pccs_url: Option<&str>, + now: Option, ) -> Result { - let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); - if self.report_data != expected_report_data { - bail!("report data mismatch"); - } - self.verify(pccs_url).await - } - - /// Verify the quote - pub async fn verify(self, pccs_url: Option<&str>) -> Result { let report = match &self.quote { AttestationQuote::DstackTdx(q) => { let report = self.verify_tdx(pccs_url, &q.quote).await?; @@ -680,8 +775,14 @@ impl Attestation { tpm_report, } } - AttestationQuote::DstackNitroEnclave(quote) => todo!(), + AttestationQuote::DstackNitroEnclave(quote) => { + let report = self + .verify_nitro_enclave_with_time(quote, now) + .context("Failed to verify Nitro Enclave")?; + DstackVerifiedReport::DstackNitroEnclave(report) + } }; + Ok(VerifiedAttestation { quote: self.quote, runtime_events: self.runtime_events, @@ -691,6 +792,69 @@ impl Attestation { }) } + /// Wrap into a versioned attestation for encoding + pub fn into_versioned(self) -> VersionedAttestation { + VersionedAttestation::V0 { attestation: self } + } + + /// Verify the quote + pub async fn verify_with_ra_pubkey( + self, + ra_pubkey_der: &[u8], + pccs_url: Option<&str>, + ) -> Result { + let expected_report_data = QuoteContentType::RaTlsCert.to_report_data(ra_pubkey_der); + if self.report_data != expected_report_data { + bail!("report data mismatch"); + } + self.verify(pccs_url).await + } + + /// Verify the quote + pub async fn verify(self, pccs_url: Option<&str>) -> Result { + self.verify_with_time(pccs_url, None).await + } + + /// Verify Nitro Enclave attestation with optional custom time (testing hook) + /// + /// This performs full cryptographic verification: + /// 1. Verifies COSE Sign1 signature using ECDSA P-384 with SHA-384 + /// 2. Verifies certificate chain from attestation document to AWS Nitro root CA + /// 3. Validates user_data matches expected report_data + fn verify_nitro_enclave_with_time( + &self, + nsm_quote: &DstackNitroQuote, + now: Option, + ) -> Result { + // Verify COSE signature and certificate chain using nsm-qvl + let verified_report = nsm_qvl::verify_attestation( + &nsm_quote.nsm_quote, + nsm_qvl::AWS_NITRO_ENCLAVES_ROOT_G1, + now, + ) + .context("NSM attestation verification failed")?; + + // Verify user_data matches report_data + let Some(user_data) = verified_report.user_data.clone() else { + bail!("NSM attestation document does not contain user_data"); + }; + if user_data != self.report_data { + bail!("NSM user_data does not match report_data"); + } + + // Decode PCRs from quote + let pcrs = nsm_quote + .decode_pcrs() + .context("Failed to decode nitro pcrs")?; + + Ok(NitroVerifiedReport { + module_id: verified_report.module_id, + pcrs, + user_data, + timestamp: verified_report.timestamp, + }) + } + async fn verify_tpm( &self, quote: &TpmQuote, diff --git a/dstack-attest/tests/nitro_attestation.bin b/dstack-attest/tests/nitro_attestation.bin new file mode 100644 index 0000000000000000000000000000000000000000..e0bbb2aa147de56d065a7a5ccbf84adaddbbd84f GIT binary patch literal 4709 zcmd6rc~lek7RQrG*aU*Cf{0Y02-4thvInZN30PV;wg6R_WF{n<5Sj$UQh^{UxS+xV z1UD$SSBu3BDyUUN#HDVy^dVMItP6;sh(Pluq82K?o}AP7PIJ!hk9#wB=Fa>+-+S*2 z0jl)R@GBv59Eu}MeU6ExGBiPgMT^m-iDD|?(AXH8%f$p-8l8qR7*s4y0AMbk$zlK@ zDrC_C$`YdDSWKZ51_yf6ISjEF7W2a!9Ur7cPm780ueecudN;%a*UNXrW}c{V%35_P z&@`pQF)yi#B7c74Yx?%n0Wr?8QL;j@*HiwTmZt}wTyE;gG$ru>LCYVR2a=h}y&F?9 zA1`G-xR9@~d{rm6_8VW&zH9NkHuCjnv1<%nv(J}q4EfwDOSm<#VcP>8?XRM%eTbg+ zA^O^f7-%0dO8XE)?L&;T4>8t0gra?jiS{821(;kZ7K#Ok5{rORK{}L-FJdtXB0|t; zTv=WNFF>OPT=5q%OTj2z{YlFMFH9kmNI;bh8v+AeQYcy1910DF&4H=L&^H+(@9x{a++J z6?{6FFmr^2N2+AQZvrHO(YkuCA1XxD)p1ji2@WJafh;%^wuX8_&u1%h;7DCS@6Slq ze99G<9LkQhjpjM=hpXCi)_##Y?!E>4NX3tozSQW(o{6dDS0o)FmCGED7N{L&bJR{H zGpg=IeH*AImQbiw&YOR|+cm##erodXk?qDaOYMLyFv2spGba+&WC##|p~f;HlSz>7 z@u53S5)l9f!Gs>3S+!rlip9^4>11EO8NoacHQ^C+WDV5(AxGgY-oyL#_+BE z3&?DN&ETGyjf@e@g#nEN(`g*Q#4%zFnvB=x-+8GI?0r8J@@TvMi7rw6-J)CTYWz90 z97SAKkotxEW|m*j>^b)sW&2ZL`LrX%8pZ3ftyhTym+&AX;V~ z-#td(_vXJx6DPD@6cAkugnldP7xOc~7q!#6OMQSBFnPE4I)H>D0PHjl3fYQmvF_u8 z#y52W+VW54zmjjhSh%Y>DF3yN?MyVR;D8X94}JP1F#!m1KuS|5uP>>up5Jza|Bcm# z%b}!fdy;Q&lwcQs^%QPe-t?03M~}IChThoM{?&n#jeEQBkZ-#-TIu`{@3O5as91cE z7>6C`yS?^~uHAxQo77FWp5OF6iuAiZA~nd`*Em95%`YOLQ#cpL@iqM|7kvj^x8C{I zM<$*(-eYvo74^`}{?5&MTV7z83D}I+<%I=c7~s{0&0t0XR4WAW5;ce*6{QQfsF2MN zq8OjXN0`Iw!=H=X2ax%soVv^_!ieM}PwT!XZT4#O=H05@nqTEp+Z$V*nf-DTOgdMX zN3Jq&fFke99wgZ3Uch`+U>hVhFZR0l(c|`Siq;2PK+=^pJyOcrSH4vOp%C zj5{YIdWzne&8Xu){F|LrxD`}g8Xq6*n@JwmqSL=;W4^g%o?5*6g|2xkc z)2%C$u8djf!sDsp=t6wT&PH;xHME$oU^Ku$$@U zsneitJGWs$EkX31Z}rl#Yu}TBDs4~)i@_q0P^>8aYPsx<4sdL#?*<|2`>N0=;@$rv zXai#lVVKJiQfX`!i^>onfXZQFOez~;GlhVOa#`>PwXqWop6X+3MR_@6UgZ;?CT@ZE zDd&&ouOOTg4e67_I_1Uw;eU> z+LQ9EmP6R-P|-T~2T8@}-JP>iPN%ENoQSPm&!ZkO^&?Ml(boH=h`gqwQeC6HBajx zRFzFQR9cfT5jx(mq#~&i>0n#e+h+g>OlV%If5aio)yn>>%9|=t`^QteB#hr&k4qMIU?7nv2zY3STG%kLrIb>GWFyT*l-ct*NiOn?`Z*Mk5HD#yQyz_!ZE z*{&uAhq??x5+6?$qq9d!c_g1Uw=sv9>>yJ_ zi={{`7A?Zx79BhsU=EkT2YeddrWptq<-;tD!$D|Vm>~rCcOi^P44g}@9pM#K1QuEUc50OGS#h5!Hn literal 0 HcmV?d00001 diff --git a/dstack-attest/tests/nitro_attestation_dbg.bin b/dstack-attest/tests/nitro_attestation_dbg.bin new file mode 100644 index 0000000000000000000000000000000000000000..3f909e5cbcafe5f6bc067866e14c07eda4b9cfbb GIT binary patch literal 4684 zcmd6rc~leE9>zyzwmn^W?23UDjNMs7JT8afr#L<%^On{5oxEMji983r}7*EK=Ut*@e#9V)gh5nLo6pKsEyXi{KPU}8vFN;R~Gmiocg zz)BYw(}pZZl%W!Nh)N-cErGdi(1>Q_haoDJ6oV~*nQmkV&Cp$jtR*g?5qBj9OlcJN z<$kab@O2S|1_mh%4r(P6LzF5e?0~FMI>;reD21+ANRic;5{w>U#^S;(78eLufWr># z3t^ZDd4WK){eO}0OyJy`Fn5rI2diWQ{{&bKEDep`y;O*7XfRDfCD~I%Br51~34fG! z@aYsKqkabcQErye%)|Q=enYb6fAd7XW2m_x(JrW@tt5VxEBw{-S%RGQJIOKrEph*B zDDrJ@eYjQW;^fzEr3H?Zrm*NWvx6qiEOuH@WU^p|%Ng5NZ{u~+JX^p3G$M0bYclyQ z6#^t+s*5bBR0?Exs{c$=BqV@CFzKqxGuPR|mi+N6x{*|5j2zl~^^iR9*4-H4BTjF~c^CefkNV8yzlN5y?KeK% z85(gnyD6>QTfo|;lqdPT?T)&hG|Oj^?*mSLVfT%Dr%w8hWSuBnZDig&v@zTD=dldD zb#eWMOSepd71j}5BaA(--?Su;yp%=P-jbb08~YAaf&#-`nc#R znUZsRuZKbsG}_71=gQmunf;lQ)mL9&m<#wsK_Ti355O>hc|^z!1Y*4q`ft6LrGKWeu?jzw`+8SpXX-ksCx#l5AMvcW>gnBK|R%GEB*1!fg_3TM__FHdNNT31taqsHWHa=D48SysAnxcA(14gTi>D0C1{(UrLD z_q|qLlA9>Y7i^@b*F>JT8*9)%vHufVeSmXdA&bKT?;9u^fZh;jl3-`x)JrWJ=doD` ziozTgpUY=shy&5f5$S(q_jj%SlNvo>Iq^O)ra{ZyKV3$Au5D5jna=A?=!Uj8#)Q?* zfAvG<&4m}s1XCn&MG@?od4UV}6cs=PrDd|hwDfNu18i<&sYBY52?()BFrHt%KNrJirEna{kc<0dYT+ zv`9CZwZf+H=M|Dz%Y`iuXPsuw{N&2{Au)h0YNlejmge z7$1jWp#W#H_&gqygCc+_;9^`RAK`Oxz!eL5@MpbIy(=x|80+u5H02{Uy2jaTFT7%S zq;q_>jX&+tG&Gj~%qC^jMY?ASS!>ZTr!fq(B)sv&NsgAXCp4)asUwYtwVY5!@u6b`7a}ii}GWqa27X5_cV{H*fuo@qrJHjS+;djLd(rPZp+RbklB&%Z48Ks>bUPY(^DB+ z`1E__JflOwO+Gwl{^V18uH0)5o#yHx4k~pw8;IYoUaa00x1@bzfIYzSwZ_z!}C!P2iSQ8|ghGPuF=FY{}RB zi1!@wKH(g_CVEQ!ty=aeIoPRn6=e+wBz${ zACHNjATFyp^V^q&#WJyj$6AZV5n&N(kyL^Phhs6_615VG3KkQmVy{haUWuKP8zKl>8cW{v`c&XF{>N1P`YF zku2Y7eaY+W70O_V3<<%4Ly1kHy_3BFMtLxdv$-N1=L?C2HJ43%;=%bS&H^yX=L-1< ej|U@Ml=%3=W3#y&;(DTaqXGnnc`zHd-|%k{G1N-{ literal 0 HcmV?d00001 diff --git a/dstack-attest/tests/nitro_verify.rs b/dstack-attest/tests/nitro_verify.rs new file mode 100644 index 000000000..f32384e5c --- /dev/null +++ b/dstack-attest/tests/nitro_verify.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Integration test: verify Nitro Enclave attestation end-to-end + +use dstack_attest::attestation::{DstackVerifiedReport, VersionedAttestation}; +use futures::executor::block_on; +use std::time::{Duration, SystemTime}; + +// Real Nitro Enclave attestation captured from an enclave +const NITRO_ATTESTATION_BIN: &[u8] = include_bytes!("nitro_attestation.bin"); + +#[test] +fn verify_nitro_attestation_bin() { + // Decode VersionedAttestation from SCALE + let versioned = VersionedAttestation::from_scale(NITRO_ATTESTATION_BIN) + .expect("decode VersionedAttestation"); + let attestation = versioned.into_inner(); + + let app_info = attestation.decode_app_info(false).unwrap(); + let app_info_str = serde_json::to_string_pretty(&app_info).unwrap(); + + println!("App Info: {app_info_str}"); + insta::assert_snapshot!("app_info", app_info_str); + + // Perform full verification (COSE signature + cert chain + user_data) + // Use a fixed historical time to tolerate expired certs in this captured sample + // just before not_after + let fixed_now = SystemTime::UNIX_EPOCH + Duration::from_secs(1766678656); + let verified = block_on(attestation.verify_with_time(None, Some(fixed_now))).unwrap(); + let DstackVerifiedReport::DstackNitroEnclave(report) = verified.report else { + panic!("Nitro attestation verification failed"); + }; + println!("✓ Nitro attestation verified successfully"); + insta::assert_snapshot!( + "nitro_report", + serde_json::to_string_pretty(&report).unwrap() + ); +} diff --git a/dstack-attest/tests/snapshots/nitro_verify__app_info.snap b/dstack-attest/tests/snapshots/nitro_verify__app_info.snap new file mode 100644 index 000000000..cc08badca --- /dev/null +++ b/dstack-attest/tests/snapshots/nitro_verify__app_info.snap @@ -0,0 +1,15 @@ +--- +source: dstack-attest/tests/nitro_verify.rs +assertion_line: 25 +expression: app_info_str +--- +{ + "app_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4", + "compose_hash": "1894b0b29e94a9db16e88a2914f0923e52bb16c08bf3bb484df786a147e2eb79", + "instance_id": "", + "device_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "mr_system": "1894b0b29e94a9db16e88a2914f0923e52bb16c08bf3bb484df786a147e2eb79", + "mr_aggregated": "1894b0b29e94a9db16e88a2914f0923e52bb16c08bf3bb484df786a147e2eb79", + "os_image_hash": "1894b0b29e94a9db16e88a2914f0923e52bb16c08bf3bb484df786a147e2eb79", + "key_provider_info": "" +} diff --git a/dstack-attest/tests/snapshots/nitro_verify__nitro_report.snap b/dstack-attest/tests/snapshots/nitro_verify__nitro_report.snap new file mode 100644 index 000000000..dafa10583 --- /dev/null +++ b/dstack-attest/tests/snapshots/nitro_verify__nitro_report.snap @@ -0,0 +1,15 @@ +--- +source: dstack-attest/tests/nitro_verify.rs +assertion_line: 36 +expression: "serde_json::to_string_pretty(&report).unwrap()" +--- +{ + "module_id": "i-0827e799ec9232d44-enc019b5640fdf630d6", + "pcrs": { + "pcr0": "eb7d0dab08ff41546d7e5659aee883af7b32bb2e58e46815b719f9f7bfae7b880188a10d86317e6509923740526cf74a", + "pcr1": "0343b056cd8485ca7890ddd833476d78460aed2aa161548e4e26bedf321726696257d623e8805f3f605946b3d8b0c6aa", + "pcr2": "d7b0a76788c1be24898bd148117ea1239578ba0e72f5d87a33a6c6476026675b6f996940f062e0e3f0b5edd2ddf78811" + }, + "user_data": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8550000000000000000000000000000000000000000000000000000000000000000", + "timestamp": 1766678659346 +} diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index b3f31dbd7..0caf5f648 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -13,7 +13,7 @@ use k256::schnorr::SigningKey; use ra_rpc::Attestation; use ra_tls::{ attestation::{QuoteContentType, VersionedAttestation}, - cert::generate_ra_cert, + cert::{generate_ra_cert, generate_ra_cert_with_app_id}, kdf::{derive_ecdsa_key, derive_ecdsa_key_pair_from_bytes}, rcgen::KeyPair, }; @@ -295,6 +295,10 @@ struct AttestArgs { #[arg(long)] report_data: Option, + /// app id (20 bytes in hex) - optional + #[arg(long)] + app_id: Option, + /// output file (default: attestation.bin) #[arg(short, long)] output: Option, @@ -340,9 +344,9 @@ struct GetKeysArgs { #[arg(short, long)] kms_url: String, - /// PCCS URL for quote verification (optional) - #[arg(short, long)] - pccs_url: Option, + /// Application ID (20 bytes in hex) - optional + #[arg(long)] + app_id: Option, /// Output file path (default: stdout as JSON) #[arg(short, long)] @@ -366,7 +370,7 @@ fn cmd_quote_report(args: QuoteReportArgs) -> Result<()> { let report_data = match args.report_data { Some(hex_data) => { - pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? + pad64(&hex_decode(&hex_data).context("Failed to decode report_data hex")?)? } None => [0u8; 64], }; @@ -385,14 +389,29 @@ fn cmd_quote_report(args: QuoteReportArgs) -> Result<()> { Ok(()) } +fn decode_app_id(hex_str: Option<&str>) -> Result> { + let Some(hex_str) = hex_str else { + return Ok(None); + }; + let bytes = hex_decode(hex_str).context("Invalid app_id hex string")?; + if bytes.len() != 20 { + anyhow::bail!("app_id must be exactly 20 bytes (40 hex characters)"); + } + let mut arr = [0u8; 20]; + arr.copy_from_slice(&bytes); + Ok(Some(arr)) +} + fn cmd_attest(args: AttestArgs) -> Result<()> { let report_data = match args.report_data { Some(hex_data) => { - pad64(&hex::decode(hex_data).context("Failed to decode report_data hex")?)? + pad64(&hex_decode(&hex_data).context("Failed to decode report_data hex")?)? } None => [0u8; 64], }; - let attestation = Attestation::quote(&report_data).context("Failed to get attestation")?; + let app_id = decode_app_id(args.app_id.as_deref())?; + let attestation = Attestation::quote_with_app_id(&report_data, app_id) + .context("Failed to get attestation")?; let attestation = attestation.into_versioned().to_scale(); if args.hex { @@ -515,7 +534,6 @@ fn cmd_attest_strip(args: AttestStripArgs) -> Result<()> { async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { use dstack_kms_rpc::kms_client::KmsClient; use ra_rpc::client::{RaClient, RaClientConfig}; - use ra_tls::cert::generate_ra_cert; let kms_url = if args.kms_url.ends_with("/prpc") { args.kms_url.clone() @@ -535,8 +553,13 @@ async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { }; // Step 2: Generate RA-TLS client certificate - let cert_pair = generate_ra_cert(tmp_ca.temp_ca_cert.clone(), tmp_ca.temp_ca_key.clone()) - .context("Failed to generate RA cert")?; + let app_id = decode_app_id(args.app_id.as_deref())?; + let cert_pair = generate_ra_cert_with_app_id( + tmp_ca.temp_ca_cert.clone(), + tmp_ca.temp_ca_key.clone(), + app_id, + ) + .context("Failed to generate RA cert")?; // Step 3: Create authenticated client and request app keys let ra_client = RaClientConfig::builder() @@ -546,7 +569,6 @@ async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { .tls_client_cert(cert_pair.cert_pem) .tls_client_key(cert_pair.key_pem) .tls_ca_cert(tmp_ca.ca_cert.clone()) - .maybe_pccs_url(args.pccs_url.clone()) .build() .into_client() .context("Failed to create RA client")?; @@ -612,8 +634,12 @@ fn cmd_eventlog() -> Result<()> { Ok(()) } +fn hex_decode(hex_str: &str) -> Result> { + hex::decode(hex_str.trim_start_matches("0x")).context("Invalid hex string") +} + fn cmd_extend(extend_args: ExtendArgs) -> Result<()> { - let payload = hex::decode(&extend_args.payload).context("Failed to decode payload")?; + let payload = hex_decode(&extend_args.payload).context("Failed to decode payload")?; emit_runtime_event(&extend_args.event, &payload).context("Failed to extend RTMR") } @@ -1055,7 +1081,7 @@ fn cmd_vtpm_attest(args: VtpmAttestArgs) -> Result<()> { fn cmd_tpm_quote(args: TpmQuoteArgs) -> Result<()> { let data = if let Some(hex_data) = args.data { - let decoded = hex::decode(&hex_data).context("Failed to decode hex data")?; + let decoded = hex_decode(&hex_data).context("Failed to decode hex data")?; if decoded.len() > 64 { anyhow::bail!("Qualifying data must be at most 64 bytes"); } diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 94b61476e..57fead253 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -168,13 +168,21 @@ impl RpcHandler { use_boottime_mr: bool, vm_config_str: &str, ) -> Result { - let todo = "Allow non tdx attestation"; - let Some(tdx_report) = &att.report.tdx_report() else { - bail!("No TD report in attestation"); + let tcb_status; + let advisory_ids; + match att.report.tdx_report() { + Some(report) => { + tcb_status = report.status.clone(); + advisory_ids = report.advisory_ids.clone(); + } + None => { + tcb_status = "".to_string(); + advisory_ids = Vec::new(); + } }; let app_info = att.decode_app_info_ex(use_boottime_mr, vm_config_str)?; - let todo = "Add attestation mode in BootInfo"; let boot_info = BootInfo { + attestation_mode: att.quote.mode(), mr_aggregated: app_info.mr_aggregated.to_vec(), os_image_hash: app_info.os_image_hash, mr_system: app_info.mr_system.to_vec(), @@ -183,8 +191,8 @@ impl RpcHandler { instance_id: app_info.instance_id, device_id: app_info.device_id, key_provider_info: app_info.key_provider_info, - tcb_status: tdx_report.status.clone(), - advisory_ids: tdx_report.advisory_ids.clone(), + tcb_status, + advisory_ids, }; let response = self .state diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index b3dd2db3e..169fd495a 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -4,12 +4,14 @@ use crate::config::AuthApi; use anyhow::{bail, Result}; +use ra_tls::attestation::AttestationMode; use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct BootInfo { + pub attestation_mode: AttestationMode, #[serde(with = "hex_bytes")] pub mr_aggregated: Vec, #[serde(with = "hex_bytes")] diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index f9bbed7cc..9340734b0 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -479,6 +479,17 @@ pub fn decompress_ext_value(data: &[u8]) -> Result> { /// Generate a certificate with RA-TLS quote and event log. #[cfg(feature = "quote")] pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result { + generate_ra_cert_with_app_id(ca_cert_pem, ca_key_pem, None) +} + +/// Generate a certificate with RA-TLS quote and event log. +/// If app_id is provided, it will be included in the quote. +#[cfg(feature = "quote")] +pub fn generate_ra_cert_with_app_id( + ca_cert_pem: String, + ca_key_pem: String, + app_id: Option<[u8; 20]>, +) -> Result { use rcgen::{KeyPair, PKCS_ECDSA_P256_SHA256}; let ca = CaCert::new(ca_cert_pem, ca_key_pem)?; @@ -488,7 +499,7 @@ pub fn generate_ra_cert(ca_cert_pem: String, ca_key_pem: String) -> Result Result<( Err(TdxAttestError::NotSupported) } -pub fn log_rtmr_event(_log: &TdxEventLog) -> Result<()> { +pub fn log_rtmr_event(_log: &TdxEvent) -> Result<()> { Err(TdxAttestError::NotSupported) } diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index 56808b2af..b8bc5a94e 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -2,15 +2,15 @@ // // SPDX-License-Identifier: Apache-2.0 -#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] pub use linux::*; -#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] mod linux; -#[cfg(not(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu")))] +#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] pub use dummy::*; -#[cfg(not(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu")))] +#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] mod dummy; pub use cc_eventlog as eventlog; diff --git a/tpm-qvl/src/lib.rs b/tpm-qvl/src/lib.rs index b7c93c76c..2fe2b47de 100644 --- a/tpm-qvl/src/lib.rs +++ b/tpm-qvl/src/lib.rs @@ -22,13 +22,14 @@ use serde::{Deserialize, Serialize}; /// Subject: CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US /// Valid: 2022-07-08 to 2122-07-08 (100 years) pub const GCP_ROOT_CA: &str = include_str!("../certs/gcp-root-ca.pem"); -pub const AWS_NITRO_ENCLAVES_ROOT_G1: &str = include_str!("../certs/AWS_NitroEnclaves_Root-G1.pem"); /// Get TPM root CA certificate for the given platform pub fn get_root_ca(platform: Platform) -> Result<&'static str> { match platform { Platform::Gcp => Ok(GCP_ROOT_CA), - Platform::NitroEnclave => Ok(AWS_NITRO_ENCLAVES_ROOT_G1), + Platform::NitroEnclave => { + bail!("Nitro Enclave uses NSM attestation, not TPM. Use nsm-qvl instead.") + } Platform::Dstack => bail!("dstack platform does not use TPM attestation"), } } diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 0b8917b7e..1f0513ef4 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -45,6 +45,8 @@ cc-eventlog.workspace = true sha2.workspace = true tpm-qvl.workspace = true tpm-types.workspace = true +nsm-attest.workspace = true +ez-hash.workspace = true serde-human-bytes.workspace = true hex-literal.workspace = true diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 5844a26e4..1c9786fdf 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -14,7 +14,8 @@ use dstack_mr::{RtmrLog, TdxMeasurementDetails, TdxMeasurements}; use dstack_types::VmConfig; use hex_literal::hex; use ra_tls::attestation::{ - Attestation, AttestationQuote, TpmQuote, VerifiedAttestation, VersionedAttestation, + Attestation, AttestationQuote, DstackNitroQuote, TpmQuote, VerifiedAttestation, + VersionedAttestation, }; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; @@ -491,9 +492,9 @@ impl CvmVerifier { self.verify_os_image_hash_for_dstack_tdx(&vm_config, attestation, debug, details) .await?; } - AttestationQuote::DstackNitroEnclave(_) => { - let todo = "Implement it"; - bail!("Nitro not supported"); + AttestationQuote::DstackNitroEnclave(quote) => { + self.verify_os_image_hash_for_nitro_enclave(&vm_config, quote) + .await?; } } Ok(vm_config) @@ -633,6 +634,26 @@ impl CvmVerifier { /// IMPORTANT: Extracting the 3rd event is GCP OVMF-specific behavior. /// On GCP, PCR 2 events are ordered as: /// [0]=EV_SEPARATOR, [1]=EV_EFI_GPT_EVENT, [2]=UKI (Event 28), [3]=Linux kernel + async fn verify_os_image_hash_for_nitro_enclave( + &self, + vm_config: &VmConfig, + nsm_quote: &DstackNitroQuote, + ) -> Result<()> { + // Parse NSM attestation document + let os_image_hash = nsm_quote + .decode_image_hash() + .context("Failed to decode PCR values")?; + // Compare with expected os_image_hash from vm_config + if os_image_hash != vm_config.os_image_hash { + bail!( + "os_image_hash mismatch: expected={}, computed={}", + hex::encode(&vm_config.os_image_hash), + hex::encode(&os_image_hash) + ); + } + Ok(()) + } + async fn verify_os_image_hash_for_gcp_tdx( &self, vm_config: &VmConfig, From 0fc0fdedd72d4d056b435675697e805c194b3bd6 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 08:34:04 +0000 Subject: [PATCH 074/264] Put crates in workspace --- Cargo.toml | 6 +++++- nsm-qvl/Cargo.toml | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0177162d6..2e03e9350 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,6 +142,7 @@ figment = "0.10.19" object = "0.36.4" fatfs = "0.3.6" fscommon = "0.1.1" +ciborium = "0.2" # Networking/HTTP bollard = "0.18.1" @@ -178,9 +179,11 @@ elliptic-curve = { version = "0.13.8", features = ["pkcs8"] } getrandom = "0.3.1" hkdf = "0.12.4" p256 = "0.13.2" +p384 = "0.13" ring = "0.17.14" rustls = "0.23.23" -rustls-pki-types = "1.11.0" +rustls-pki-types = "1.13.1" +rustls-webpki = "0.103.8" schnorrkel = "0.11.4" sha2 = { version = "0.10.8", default-features = false } sha3 = "0.10.8" @@ -199,6 +202,7 @@ ez-hash = "1.1.0" # Certificate/DNS hickory-resolver = "0.24.4" instant-acme = "0.7.2" +pem = "3.0" rcgen = { version = "0.13.2", features = ["pem"] } x509-parser = "0.16.0" pkcs8 = { version = "0.10", default-features = false } diff --git a/nsm-qvl/Cargo.toml b/nsm-qvl/Cargo.toml index 41e6c5b8f..11f68e208 100644 --- a/nsm-qvl/Cargo.toml +++ b/nsm-qvl/Cargo.toml @@ -17,17 +17,17 @@ serde = { workspace = true, features = ["derive"] } tracing.workspace = true # CBOR/COSE parsing -ciborium = "0.2" +ciborium.workspace = true # Cryptographic verification -p384 = { version = "0.13", features = ["ecdsa"] } +p384 = { workspace = true, features = ["ecdsa"] } sha2 = { workspace = true, features = ["oid"] } -pem = "3.0" +pem.workspace = true # Certificate chain verification -x509-parser = "0.16" -rustls-webpki = { version = "0.103.8", features = ["alloc", "ring"] } -rustls-pki-types = "1.13.1" +x509-parser.workspace = true +rustls-webpki = { workspace = true, features = ["alloc", "ring"] } +rustls-pki-types.workspace = true [dev-dependencies] nsm-attest.workspace = true From 7e3fb1aaafa1c92d92f46c334c35828f79c7a893 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 08:34:21 +0000 Subject: [PATCH 075/264] Fix REUSE warn --- REUSE.toml | 6 ++++++ nsm-attest/tests/attestation_test.rs | 2 ++ nsm-qvl/tests/verify_test.rs | 2 ++ 3 files changed, 10 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index 228ffbd63..c0af5702a 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -72,6 +72,12 @@ path = [ "cc-eventlog/samples/tpm_eventlog.bin", "tpm-attest/tests/tpm_quote_sample.bin", "tpm-qvl/certs/gcp-root-ca.pem", + "dstack-attest/tests/nitro_attestation.bin", + "dstack-attest/tests/nitro_attestation_dbg.bin", + "nsm-attest/tests/nitro_attestation.bin", + "nsm-qvl/tests/nitro_attestation.bin", + "nsm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem", + "tpm-qvl/certs/AWS_NitroEnclaves_Root-G1.pem", ] SPDX-FileCopyrightText = "NONE" SPDX-License-Identifier = "CC0-1.0" diff --git a/nsm-attest/tests/attestation_test.rs b/nsm-attest/tests/attestation_test.rs index 2b3569afc..3e556855c 100644 --- a/nsm-attest/tests/attestation_test.rs +++ b/nsm-attest/tests/attestation_test.rs @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 The Project Contributors +// SPDX-License-Identifier: Apache-2.0 // Test for NSM attestation document parsing use nsm_attest::AttestationDocument; diff --git a/nsm-qvl/tests/verify_test.rs b/nsm-qvl/tests/verify_test.rs index 91da39ea6..f29e722dc 100644 --- a/nsm-qvl/tests/verify_test.rs +++ b/nsm-qvl/tests/verify_test.rs @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2024-2025 The Project Contributors +// SPDX-License-Identifier: Apache-2.0 // Test for NSM attestation verification use nsm_qvl::{verify_attestation, AttestationDocument, CoseSign1}; From e7d5a943010c12af59cfe96e2d717b85924abf7b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 08:56:48 +0000 Subject: [PATCH 076/264] nsm-qvl: More restriction verification --- dstack-attest/tests/nitro_verify.rs | 21 +++++++--- nsm-qvl/src/lib.rs | 61 ++++++++++++++++++++++++++--- nsm-qvl/src/verify.rs | 50 +++++++++++++++++++---- nsm-qvl/tests/verify_test.rs | 17 +++++--- 4 files changed, 124 insertions(+), 25 deletions(-) diff --git a/dstack-attest/tests/nitro_verify.rs b/dstack-attest/tests/nitro_verify.rs index f32384e5c..a8d231b8d 100644 --- a/dstack-attest/tests/nitro_verify.rs +++ b/dstack-attest/tests/nitro_verify.rs @@ -4,8 +4,9 @@ //! Integration test: verify Nitro Enclave attestation end-to-end -use dstack_attest::attestation::{DstackVerifiedReport, VersionedAttestation}; +use dstack_attest::attestation::{AttestationQuote, DstackVerifiedReport, VersionedAttestation}; use futures::executor::block_on; +use nsm_qvl::{AttestationDocument, CoseSign1}; use std::time::{Duration, SystemTime}; // Real Nitro Enclave attestation captured from an enclave @@ -24,10 +25,20 @@ fn verify_nitro_attestation_bin() { println!("App Info: {app_info_str}"); insta::assert_snapshot!("app_info", app_info_str); - // Perform full verification (COSE signature + cert chain + user_data) - // Use a fixed historical time to tolerate expired certs in this captured sample - // just before not_after - let fixed_now = SystemTime::UNIX_EPOCH + Duration::from_secs(1766678656); + // Perform full verification (COSE signature + cert chain + user_data). + // Use the attestation's own timestamp to keep freshness checks stable for this sample. + let fixed_now = match &attestation.quote { + AttestationQuote::DstackNitroEnclave(quote) => { + let cose = + CoseSign1::from_bytes("e.nsm_quote).expect("parse COSE Sign1 from quote"); + let doc = + AttestationDocument::from_cbor(&cose.payload).expect("parse attestation document"); + SystemTime::UNIX_EPOCH + .checked_add(Duration::from_millis(doc.timestamp)) + .expect("attestation timestamp overflow") + } + _ => panic!("unexpected quote type"), + }; let verified = block_on(attestation.verify_with_time(None, Some(fixed_now))).unwrap(); let DstackVerifiedReport::DstackNitroEnclave(report) = verified.report else { panic!("Nitro attestation verification failed"); diff --git a/nsm-qvl/src/lib.rs b/nsm-qvl/src/lib.rs index acbb58363..bf469b0df 100644 --- a/nsm-qvl/src/lib.rs +++ b/nsm-qvl/src/lib.rs @@ -22,7 +22,7 @@ use anyhow::{bail, Context, Result}; use serde::Deserialize; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, io::Cursor}; mod verify; @@ -52,8 +52,12 @@ impl CoseSign1 { /// Parse COSE Sign1 from raw bytes pub fn from_bytes(data: &[u8]) -> Result { // COSE Sign1 structure is a CBOR array: [protected, unprotected, payload, signature] + let mut reader = Cursor::new(data); let value: ciborium::Value = - ciborium::from_reader(data).context("Failed to parse COSE Sign1 CBOR")?; + ciborium::from_reader(&mut reader).context("Failed to parse COSE Sign1 CBOR")?; + if reader.position() != data.len() as u64 { + bail!("Trailing bytes after COSE Sign1"); + } let array = match value { ciborium::Value::Array(arr) => arr, @@ -110,9 +114,7 @@ impl CoseSign1 { /// Get the algorithm from protected header pub fn algorithm(&self) -> Result { - let protected_map: BTreeMap = - ciborium::from_reader(&self.protected[..]) - .context("Failed to parse protected header")?; + let protected_map = self.protected_map()?; // Algorithm is key 1 in COSE let alg = protected_map @@ -128,6 +130,38 @@ impl CoseSign1 { } } + /// Validate critical headers (crit) per COSE rules. + pub fn validate_critical_headers(&self) -> Result<()> { + let protected_map = self.protected_map()?; + let Some(crit) = protected_map.get(&2) else { + return Ok(()); + }; + + let crit_list = match crit { + ciborium::Value::Array(arr) => arr, + _ => bail!("COSE crit header is not an array"), + }; + + for item in crit_list { + match item { + ciborium::Value::Integer(i) => { + let val: i128 = (*i).into(); + if val as i64 != 1 { + bail!("Unsupported critical header parameter: {val}"); + } + } + ciborium::Value::Text(name) => { + if name != "alg" { + bail!("Unsupported critical header parameter: {name}"); + } + } + _ => bail!("Invalid critical header parameter type"), + } + } + + Ok(()) + } + /// Build the Sig_structure for verification /// Sig_structure = ["Signature1", protected, external_aad, payload] pub fn sig_structure(&self) -> Result> { @@ -143,6 +177,15 @@ impl CoseSign1 { .context("Failed to encode Sig_structure")?; Ok(buf) } + + fn protected_map(&self) -> Result> { + let mut reader = Cursor::new(&self.protected); + let map = ciborium::from_reader(&mut reader).context("Failed to parse protected header")?; + if reader.position() != self.protected.len() as u64 { + bail!("Trailing bytes after protected header"); + } + Ok(map) + } } /// Attestation document structure (parsed from COSE payload) @@ -175,6 +218,12 @@ pub struct AttestationDocument { impl AttestationDocument { /// Parse attestation document from CBOR payload pub fn from_cbor(data: &[u8]) -> Result { - ciborium::from_reader(data).context("Failed to parse attestation document CBOR") + let mut reader = Cursor::new(data); + let doc = ciborium::from_reader(&mut reader) + .context("Failed to parse attestation document CBOR")?; + if reader.position() != data.len() as u64 { + bail!("Trailing bytes after attestation document CBOR"); + } + Ok(doc) } } diff --git a/nsm-qvl/src/verify.rs b/nsm-qvl/src/verify.rs index 1f995cbe2..ec9a92ba1 100644 --- a/nsm-qvl/src/verify.rs +++ b/nsm-qvl/src/verify.rs @@ -16,6 +16,9 @@ use x509_parser::prelude::*; use crate::{AttestationDocument, CoseSign1}; +const DIGEST_SHA384: &str = "SHA384"; +const PCR_SHA384_LEN: usize = 48; + /// Verified NSM attestation report #[derive(Debug, Clone)] pub struct NsmVerifiedReport { @@ -44,19 +47,25 @@ pub fn verify_attestation_with_ca( } /// Verify Nitro attestation with custom root CA and custom time (for testing) +/// +/// This enforces digest/PCR consistency and a freshness window based on `now`. pub fn verify_attestation( cose_sign1_bytes: &[u8], root_ca_pem: &str, now: Option, ) -> Result { + let now = now.unwrap_or_else(SystemTime::now); let cose = CoseSign1::from_bytes(cose_sign1_bytes).context("Failed to parse COSE Sign1")?; + cose.validate_critical_headers() + .context("Unsupported COSE critical headers")?; let alg = cose.algorithm().context("Failed to get algorithm")?; if alg != -35 { bail!("Unsupported COSE algorithm: {alg}. Expected -35 (ES384)"); } let doc = AttestationDocument::from_cbor(&cose.payload) .context("Failed to parse attestation document")?; - verify_certificate_chain(&doc, root_ca_pem, now) + validate_attestation_document(&doc).context("Attestation document validation failed")?; + verify_certificate_chain(&doc, root_ca_pem, Some(now)) .context("Certificate chain verification failed")?; verify_cose_signature(&cose, &doc.certificate).context("COSE signature verification failed")?; @@ -102,14 +111,17 @@ fn verify_certificate_chain( let leaf_cert = EndEntityCert::try_from(&leaf_cert_der).context("Failed to parse leaf certificate")?; - // Log certificate info - if let Ok((_, cert)) = X509Certificate::from_der(&doc.certificate) { - debug!( - "Leaf certificate: subject={}, issuer={}", - cert.subject(), - cert.issuer() - ); + // Log certificate info and enforce basic constraints. + let (_, leaf_cert_parsed) = X509Certificate::from_der(&doc.certificate) + .context("Failed to parse leaf certificate for constraints")?; + if leaf_cert_parsed.is_ca() { + bail!("Leaf certificate must not be a CA"); } + debug!( + "Leaf certificate: subject={}, issuer={}", + leaf_cert_parsed.subject(), + leaf_cert_parsed.issuer() + ); // Create trust anchor from root CA let root_cert_der = CertificateDer::from(root_ca_der); @@ -150,6 +162,27 @@ fn verify_certificate_chain( Ok(()) } +fn validate_attestation_document(doc: &AttestationDocument) -> Result<()> { + if doc.digest != DIGEST_SHA384 { + bail!("Unsupported digest algorithm: {}", doc.digest); + } + + if doc.pcrs.is_empty() { + bail!("No PCRs in attestation document"); + } + + for (idx, value) in &doc.pcrs { + if value.len() != PCR_SHA384_LEN { + bail!( + "PCR{idx} length mismatch: {} (expected {PCR_SHA384_LEN})", + value.len() + ); + } + } + + Ok(()) +} + /// Verify COSE signature using the certificate's public key fn verify_cose_signature(cose: &CoseSign1, cert_der: &[u8]) -> Result<()> { // Extract public key from certificate @@ -204,6 +237,7 @@ fn parse_pem_cert(pem_str: &str) -> Result> { #[cfg(test)] mod tests { use super::*; + use crate::AWS_NITRO_ENCLAVES_ROOT_G1; #[test] fn test_root_ca_parsing() { diff --git a/nsm-qvl/tests/verify_test.rs b/nsm-qvl/tests/verify_test.rs index f29e722dc..6bc88905e 100644 --- a/nsm-qvl/tests/verify_test.rs +++ b/nsm-qvl/tests/verify_test.rs @@ -2,15 +2,20 @@ // SPDX-License-Identifier: Apache-2.0 // Test for NSM attestation verification use nsm_qvl::{verify_attestation, AttestationDocument, CoseSign1}; +use std::io::Cursor; // Real attestation captured from Nitro Enclave const ATTESTATION_BIN: &[u8] = include_bytes!("nitro_attestation.bin"); -fn extract_cose_sign1(data: &[u8]) -> &[u8] { +fn extract_cose_sign1(data: &[u8]) -> Vec { // Find COSE Sign1 structure (starts with 0x84 for 4-element array) for i in 0..data.len().saturating_sub(2) { if data[i] == 0x84 && data[i + 1] == 0x44 { - return &data[i..]; + let mut reader = Cursor::new(&data[i..]); + let _: ciborium::Value = + ciborium::from_reader(&mut reader).expect("Failed to parse COSE Sign1 CBOR"); + let len = reader.position() as usize; + return data[i..i + len].to_vec(); } } panic!("Could not find COSE Sign1 marker in attestation data"); @@ -20,7 +25,7 @@ fn extract_cose_sign1(data: &[u8]) -> &[u8] { fn test_parse_cose_sign1() { let cose_data = extract_cose_sign1(ATTESTATION_BIN); - let cose = CoseSign1::from_bytes(cose_data).expect("Failed to parse COSE Sign1"); + let cose = CoseSign1::from_bytes(&cose_data).expect("Failed to parse COSE Sign1"); // Verify algorithm is ES384 (-35) let alg = cose.algorithm().expect("Failed to get algorithm"); @@ -42,7 +47,7 @@ fn test_parse_cose_sign1() { #[test] fn test_parse_attestation_document() { let cose_data = extract_cose_sign1(ATTESTATION_BIN); - let cose = CoseSign1::from_bytes(cose_data).expect("Failed to parse COSE Sign1"); + let cose = CoseSign1::from_bytes(&cose_data).expect("Failed to parse COSE Sign1"); let doc = AttestationDocument::from_cbor(&cose.payload) .expect("Failed to parse attestation document"); @@ -68,7 +73,7 @@ fn test_verify_attestation_full() { // This test verifies the full attestation: // 1. COSE Sign1 signature verification // 2. Certificate chain verification against AWS Nitro root CA - let result = verify_attestation(cose_data); + let result = verify_attestation(&cose_data, nsm_qvl::AWS_NITRO_ENCLAVES_ROOT_G1, None); match result { Ok(report) => { @@ -96,7 +101,7 @@ fn test_verify_attestation_full() { ); // Still verify we can parse the structure - let cose = CoseSign1::from_bytes(cose_data).expect("Should parse COSE"); + let cose = CoseSign1::from_bytes(&cose_data).expect("Should parse COSE"); let _doc = AttestationDocument::from_cbor(&cose.payload) .expect("Should parse attestation document"); } From 1e5159dcf662c26a478bf0212030153df8e0cbd3 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 11:46:37 +0000 Subject: [PATCH 077/264] nsm-qvl: Add optional crl check --- Cargo.lock | 5 + Cargo.toml | 1 + dstack-attest/Cargo.toml | 1 + dstack-attest/src/attestation.rs | 5 +- dstack-attest/tests/nitro_verify.rs | 10 +- nsm-qvl/Cargo.toml | 6 ++ nsm-qvl/src/collateral.rs | 161 ++++++++++++++++++++++++++++ nsm-qvl/src/lib.rs | 17 ++- nsm-qvl/src/verify.rs | 125 ++++++++++++++++----- nsm-qvl/tests/verify_test.rs | 60 ++++------- 10 files changed, 321 insertions(+), 70 deletions(-) create mode 100644 nsm-qvl/src/collateral.rs diff --git a/Cargo.lock b/Cargo.lock index 4ee1f3c2e..eb315eda8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2190,6 +2190,7 @@ dependencies = [ "sha2 0.10.9", "sha3", "tdx-attest", + "tokio", "tpm-attest", "tpm-qvl", "tpm-types", @@ -4669,15 +4670,19 @@ version = "0.5.5" dependencies = [ "anyhow", "ciborium", + "dcap-qvl-webpki", "hex", "nsm-attest", "p384", "pem", + "reqwest", "rustls-pki-types", "rustls-webpki 0.103.8", "serde", "sha2 0.10.9", + "tokio", "tracing", + "tracing-subscriber", "x509-parser", ] diff --git a/Cargo.toml b/Cargo.toml index 2e03e9350..6a24de3a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,6 +175,7 @@ default-net = "0.22.0" aes-gcm = "0.10.3" curve25519-dalek = "4.1.3" dcap-qvl = "0.3.4" +dcap-qvl-webpki = "0.103" elliptic-curve = { version = "0.13.8", features = ["pkcs8"] } getrandom = "0.3.1" hkdf = "0.12.4" diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml index d772ebe87..9cfa26b44 100644 --- a/dstack-attest/Cargo.toml +++ b/dstack-attest/Cargo.toml @@ -38,3 +38,4 @@ quote = [] [dev-dependencies] futures = { workspace = true } +tokio = { workspace = true, features = ["full"] } diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index 841c84ee9..b27a5f506 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -778,6 +778,7 @@ impl Attestation { AttestationQuote::DstackNitroEnclave(quote) => { let report = self .verify_nitro_enclave_with_time(quote, now) + .await .context("Failed to verify Nitro Enclave")?; DstackVerifiedReport::DstackNitroEnclave(report) } @@ -821,15 +822,17 @@ impl Attestation { /// 1. Verifies COSE Sign1 signature using ECDSA P-384 with SHA-384 /// 2. Verifies certificate chain from attestation document to AWS Nitro root CA /// 3. Validates user_data matches expected report_data - fn verify_nitro_enclave_with_time( + async fn verify_nitro_enclave_with_time( &self, nsm_quote: &DstackNitroQuote, now: Option, ) -> Result { // Verify COSE signature and certificate chain using nsm-qvl + // CRL fetch is unreliable (e.g. 403 from S3), so keep it disabled here by default. let verified_report = nsm_qvl::verify_attestation( &nsm_quote.nsm_quote, nsm_qvl::AWS_NITRO_ENCLAVES_ROOT_G1, + None, now, ) .context("NSM attestation verification failed")?; diff --git a/dstack-attest/tests/nitro_verify.rs b/dstack-attest/tests/nitro_verify.rs index a8d231b8d..b5f8a07f1 100644 --- a/dstack-attest/tests/nitro_verify.rs +++ b/dstack-attest/tests/nitro_verify.rs @@ -5,15 +5,14 @@ //! Integration test: verify Nitro Enclave attestation end-to-end use dstack_attest::attestation::{AttestationQuote, DstackVerifiedReport, VersionedAttestation}; -use futures::executor::block_on; use nsm_qvl::{AttestationDocument, CoseSign1}; use std::time::{Duration, SystemTime}; // Real Nitro Enclave attestation captured from an enclave const NITRO_ATTESTATION_BIN: &[u8] = include_bytes!("nitro_attestation.bin"); -#[test] -fn verify_nitro_attestation_bin() { +#[tokio::test] +async fn verify_nitro_attestation_bin() { // Decode VersionedAttestation from SCALE let versioned = VersionedAttestation::from_scale(NITRO_ATTESTATION_BIN) .expect("decode VersionedAttestation"); @@ -39,7 +38,10 @@ fn verify_nitro_attestation_bin() { } _ => panic!("unexpected quote type"), }; - let verified = block_on(attestation.verify_with_time(None, Some(fixed_now))).unwrap(); + let verified = attestation + .verify_with_time(None, Some(fixed_now)) + .await + .unwrap(); let DstackVerifiedReport::DstackNitroEnclave(report) = verified.report else { panic!("Nitro attestation verification failed"); }; diff --git a/nsm-qvl/Cargo.toml b/nsm-qvl/Cargo.toml index 11f68e208..007a1f613 100644 --- a/nsm-qvl/Cargo.toml +++ b/nsm-qvl/Cargo.toml @@ -28,6 +28,12 @@ pem.workspace = true x509-parser.workspace = true rustls-webpki = { workspace = true, features = ["alloc", "ring"] } rustls-pki-types.workspace = true +dcap-qvl-webpki = { workspace = true, features = ["alloc", "ring"] } + +# CRL download +reqwest = { workspace = true, features = ["rustls-tls"] } [dev-dependencies] nsm-attest.workspace = true +tokio = { workspace = true, features = ["full"] } +tracing-subscriber.workspace = true diff --git a/nsm-qvl/src/collateral.rs b/nsm-qvl/src/collateral.rs new file mode 100644 index 000000000..a12d73563 --- /dev/null +++ b/nsm-qvl/src/collateral.rs @@ -0,0 +1,161 @@ +// SPDX-FileCopyrightText: © 2025 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +//! Collateral retrieval module +//! +//! Extracts CRL distribution points from the device-provided cert chain and +//! downloads CRLs for revocation checking, similar to dcap-qvl/tpm-qvl. + +use anyhow::{bail, Context, Result}; +use tracing::{debug, warn}; +use x509_parser::{extensions::DistributionPointName, prelude::*}; + +use crate::{ + verify::verify_attestation_with_collateral, AttestationDocument, CoseSign1, NsmCollateral, +}; + +pub async fn get_collateral_and_verify( + cose_sign1_bytes: &[u8], + root_ca_pem: &str, + now: Option, +) -> Result { + let collateral = get_collateral(cose_sign1_bytes, root_ca_pem).await?; + verify_attestation_with_collateral(cose_sign1_bytes, root_ca_pem, &collateral, now) +} + +pub async fn get_collateral(cose_sign1_bytes: &[u8], root_ca_pem: &str) -> Result { + debug!("fetching NSM collateral (intermediate CRLs + root CA CRL)"); + + let cose = CoseSign1::from_bytes(cose_sign1_bytes).context("failed to parse COSE Sign1")?; + let doc = + AttestationDocument::from_cbor(&cose.payload).context("failed to parse attestation doc")?; + + let certs = build_chain_from_doc(&doc); + let crls = download_crls_for_certs(&certs).await?; + + let root_ca_crl = { + let root_ca_der = + extract_certs_webpki(root_ca_pem.as_bytes()).context("failed to parse root CA PEM")?; + if root_ca_der.len() != 1 { + bail!("expected 1 root CA, found {}", root_ca_der.len()); + } + download_crl_for_cert(&root_ca_der[0]).await? + }; + + debug!( + "✓ collateral fetched: {} CRL(s), root CA CRL: {}", + crls.len(), + if root_ca_crl.is_some() { "yes" } else { "no" } + ); + + Ok(NsmCollateral { crls, root_ca_crl }) +} + +fn build_chain_from_doc(doc: &AttestationDocument) -> Vec> { + let mut chain = Vec::new(); + chain.push(doc.certificate.clone()); + chain.extend(doc.cabundle.iter().skip(1).cloned()); + chain +} + +async fn download_crls_for_certs(certs: &[Vec]) -> Result>> { + debug!("downloading CRLs from device-provided cert chain..."); + + let mut crls = Vec::new(); + + for cert_der in certs { + let Some(crl) = download_crl_for_cert(cert_der) + .await + .context("failed to download CRL")? + else { + continue; + }; + crls.push(crl); + } + Ok(crls) +} + +async fn download_crl_for_cert(cert: &[u8]) -> Result>> { + let crl_urls = extract_crl_urls(cert)?; + if crl_urls.is_empty() { + debug!("no CRL Distribution Points found in certificate"); + return Ok(None); + } + + download_first_available_crl(&crl_urls).await.map(Some) +} + +async fn download_first_available_crl(urls: &[String]) -> Result> { + for url in urls { + debug!("downloading CRL from {url}"); + match download_crl(url).await { + Ok(crl) => return Ok(crl), + Err(e) => { + warn!("✗ failed to download CRL from {url}: {e:?}"); + continue; + } + } + } + bail!("failed to download CRL") +} + +fn extract_certs_webpki(cert_pem: &[u8]) -> Result>> { + let pem_items = ::pem::parse_many(cert_pem).context("failed to parse PEM")?; + let certs = pem_items + .into_iter() + .map(|pem| rustls_pki_types::CertificateDer::from(pem.into_contents())) + .collect(); + Ok(certs) +} + +async fn download_crl(url: &str) -> Result> { + debug!("downloading CRL from {url}"); + + let response = reqwest::get(url) + .await + .context(format!("failed to download CRL from {url}"))?; + + if !response.status().is_success() { + bail!("CRL download failed with status: {}", response.status()); + } + + let crl_bytes = response + .bytes() + .await + .context("failed to read CRL response body")? + .to_vec(); + + debug!("downloaded {} bytes CRL from {}", crl_bytes.len(), url); + + Ok(crl_bytes) +} + +fn extract_crl_urls(cert_der: &[u8]) -> Result> { + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + let mut crl_urls = Vec::new(); + + for ext in cert.extensions() { + let ParsedExtension::CRLDistributionPoints(crl_dist_points) = ext.parsed_extension() else { + continue; + }; + for dist_point in crl_dist_points.points.iter() { + let Some(dist_point_name) = &dist_point.distribution_point else { + continue; + }; + + let DistributionPointName::FullName(names) = dist_point_name else { + continue; + }; + for name in names.iter() { + let x509_parser::extensions::GeneralName::URI(uri) = name else { + continue; + }; + crl_urls.push(uri.to_string()); + debug!("found CRL URL: {uri}"); + } + } + } + + Ok(crl_urls) +} diff --git a/nsm-qvl/src/lib.rs b/nsm-qvl/src/lib.rs index bf469b0df..fd6290460 100644 --- a/nsm-qvl/src/lib.rs +++ b/nsm-qvl/src/lib.rs @@ -24,9 +24,22 @@ use anyhow::{bail, Context, Result}; use serde::Deserialize; use std::{collections::BTreeMap, io::Cursor}; +pub use collateral::{get_collateral, get_collateral_and_verify}; + mod verify; -pub use verify::{verify_attestation, verify_attestation_with_ca, NsmVerifiedReport}; +pub use verify::{ + verify_attestation, verify_attestation_with_ca, verify_attestation_with_collateral, + verify_attestation_with_crl, NsmVerifiedReport, +}; + +#[derive(Debug, Clone)] +pub struct NsmCollateral { + /// All CRLs extracted from device-provided cert chain + pub crls: Vec>, + /// Root CA CRL extracted from verifier-provided root CA + pub root_ca_crl: Option>, +} /// AWS Nitro Enclaves Root CA certificate (G1) /// @@ -227,3 +240,5 @@ impl AttestationDocument { Ok(doc) } } + +pub mod collateral; diff --git a/nsm-qvl/src/verify.rs b/nsm-qvl/src/verify.rs index ec9a92ba1..3844e754e 100644 --- a/nsm-qvl/src/verify.rs +++ b/nsm-qvl/src/verify.rs @@ -11,10 +11,10 @@ use p384::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; use rustls_pki_types::{CertificateDer, UnixTime}; use sha2::{Digest, Sha384}; use tracing::debug; -use webpki::EndEntityCert; +use webpki::{BorrowedCertRevocationList, CertRevocationList, EndEntityCert}; use x509_parser::prelude::*; -use crate::{AttestationDocument, CoseSign1}; +use crate::{AttestationDocument, CoseSign1, NsmCollateral}; const DIGEST_SHA384: &str = "SHA384"; const PCR_SHA384_LEN: usize = 48; @@ -42,8 +42,9 @@ pub struct NsmVerifiedReport { pub fn verify_attestation_with_ca( cose_sign1_bytes: &[u8], root_ca_pem: &str, + collateral: Option<&NsmCollateral>, ) -> Result { - verify_attestation(cose_sign1_bytes, root_ca_pem, None) + verify_attestation(cose_sign1_bytes, root_ca_pem, collateral, None) } /// Verify Nitro attestation with custom root CA and custom time (for testing) @@ -52,6 +53,7 @@ pub fn verify_attestation_with_ca( pub fn verify_attestation( cose_sign1_bytes: &[u8], root_ca_pem: &str, + collateral: Option<&NsmCollateral>, now: Option, ) -> Result { let now = now.unwrap_or_else(SystemTime::now); @@ -65,7 +67,7 @@ pub fn verify_attestation( let doc = AttestationDocument::from_cbor(&cose.payload) .context("Failed to parse attestation document")?; validate_attestation_document(&doc).context("Attestation document validation failed")?; - verify_certificate_chain(&doc, root_ca_pem, Some(now)) + verify_certificate_chain(&doc, root_ca_pem, collateral, Some(now)) .context("Certificate chain verification failed")?; verify_cose_signature(&cose, &doc.certificate).context("COSE signature verification failed")?; @@ -80,10 +82,34 @@ pub fn verify_attestation( }) } +pub fn verify_attestation_with_collateral( + cose_sign1_bytes: &[u8], + root_ca_pem: &str, + collateral: &NsmCollateral, + now: Option, +) -> Result { + verify_attestation(cose_sign1_bytes, root_ca_pem, Some(collateral), now) +} + +pub async fn verify_attestation_with_crl( + cose_sign1_bytes: &[u8], + root_ca_pem: &str, + enable_crl: bool, + now: Option, +) -> Result { + if enable_crl { + let collateral = crate::get_collateral(cose_sign1_bytes, root_ca_pem).await?; + verify_attestation(cose_sign1_bytes, root_ca_pem, Some(&collateral), now) + } else { + verify_attestation(cose_sign1_bytes, root_ca_pem, None, now) + } +} + /// Verify the certificate chain from attestation document fn verify_certificate_chain( doc: &AttestationDocument, root_ca_pem: &str, + collateral: Option<&NsmCollateral>, now_override: Option, ) -> Result<()> { // Parse root CA from PEM @@ -111,31 +137,11 @@ fn verify_certificate_chain( let leaf_cert = EndEntityCert::try_from(&leaf_cert_der).context("Failed to parse leaf certificate")?; - // Log certificate info and enforce basic constraints. - let (_, leaf_cert_parsed) = X509Certificate::from_der(&doc.certificate) - .context("Failed to parse leaf certificate for constraints")?; - if leaf_cert_parsed.is_ca() { - bail!("Leaf certificate must not be a CA"); - } - debug!( - "Leaf certificate: subject={}, issuer={}", - leaf_cert_parsed.subject(), - leaf_cert_parsed.issuer() - ); - // Create trust anchor from root CA let root_cert_der = CertificateDer::from(root_ca_der); let trust_anchor = webpki::anchor_from_trusted_cert(&root_cert_der) .context("Failed to create trust anchor from root CA")?; - if let Ok((_, cert)) = X509Certificate::from_der(root_cert_der.as_ref()) { - debug!( - "Root CA: subject={}, issuer={}", - cert.subject(), - cert.issuer() - ); - } - // Get current time let now = now_override.unwrap_or(SystemTime::now()); let now = now @@ -143,10 +149,65 @@ fn verify_certificate_chain( .context("Failed to get current time")?; let time = UnixTime::since_unix_epoch(now); - // Verify certificate chain - // Note: AWS Nitro Enclaves don't use CRL, so we disable revocation checking + let chain_has_crl_dp = has_crl_distribution_points(&doc.certificate)? + || doc + .cabundle + .iter() + .skip(1) // Skip the root cert from cabundle + .any(|cert| has_crl_distribution_points(cert).unwrap_or(false)); + + let root_has_crl_dp = has_crl_distribution_points(root_cert_der.as_ref()).unwrap_or(false); + let trust_anchors = [trust_anchor]; + let Some(collateral) = collateral else { + leaf_cert + .verify_for_usage( + webpki::ALL_VERIFICATION_ALGS, + &trust_anchors, + &intermediates, + time, + webpki::KeyUsage::client_auth(), + None, + None, + ) + .context("Certificate chain verification failed")?; + return Ok(()); + }; + if root_has_crl_dp && collateral.root_ca_crl.is_none() { + bail!("Root CA has CRL distribution points but no root CA CRL provided"); + } + if chain_has_crl_dp && collateral.crls.is_empty() { + bail!("CRL distribution points present but no CRLs downloaded"); + } + + if let Some(root_ca_crl) = &collateral.root_ca_crl { + let crl_refs = vec![root_ca_crl.as_slice()]; + dcap_qvl_webpki::check_single_cert_crl(root_cert_der.as_ref(), &crl_refs, time) + .context("root CA revoked or invalid CRL")?; + } + + let crls: Vec = collateral + .crls + .iter() + .enumerate() + .map(|(i, der)| { + BorrowedCertRevocationList::from_der(der) + .map(|crl| crl.into()) + .with_context(|| format!("failed to parse intermediate CRL #{i}")) + }) + .collect::>>()?; + let crl_refs: Vec<&CertRevocationList> = crls.iter().collect(); + + let revocation_builder = webpki::RevocationOptionsBuilder::new(&crl_refs) + .map_err(|_| anyhow::anyhow!("failed to create RevocationOptionsBuilder"))?; + + let revocation = revocation_builder + .with_depth(webpki::RevocationCheckDepth::Chain) + .with_status_policy(webpki::UnknownStatusPolicy::Allow) + .with_expiration_policy(webpki::ExpirationPolicy::Enforce) + .build(); + leaf_cert .verify_for_usage( webpki::ALL_VERIFICATION_ALGS, @@ -154,7 +215,7 @@ fn verify_certificate_chain( &intermediates, time, webpki::KeyUsage::client_auth(), - None, // No revocation checking + Some(revocation), None, ) .context("Certificate chain verification failed")?; @@ -234,6 +295,16 @@ fn parse_pem_cert(pem_str: &str) -> Result> { Ok(pem_block.into_contents()) } +fn has_crl_distribution_points(cert_der: &[u8]) -> Result { + let (_, cert) = X509Certificate::from_der(cert_der).context("failed to parse certificate")?; + for ext in cert.extensions() { + if let ParsedExtension::CRLDistributionPoints(_) = ext.parsed_extension() { + return Ok(true); + } + } + Ok(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/nsm-qvl/tests/verify_test.rs b/nsm-qvl/tests/verify_test.rs index 6bc88905e..ec6e9ec26 100644 --- a/nsm-qvl/tests/verify_test.rs +++ b/nsm-qvl/tests/verify_test.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 The Project Contributors // SPDX-License-Identifier: Apache-2.0 // Test for NSM attestation verification -use nsm_qvl::{verify_attestation, AttestationDocument, CoseSign1}; +use nsm_qvl::{AttestationDocument, CoseSign1}; use std::io::Cursor; // Real attestation captured from Nitro Enclave @@ -66,44 +66,30 @@ fn test_parse_attestation_document() { assert!(!doc.cabundle.is_empty()); } -#[test] -fn test_verify_attestation_full() { - let cose_data = extract_cose_sign1(ATTESTATION_BIN); - - // This test verifies the full attestation: - // 1. COSE Sign1 signature verification - // 2. Certificate chain verification against AWS Nitro root CA - let result = verify_attestation(&cose_data, nsm_qvl::AWS_NITRO_ENCLAVES_ROOT_G1, None); +#[tokio::test] +async fn test_verify_attestation_full() { + tracing_subscriber::fmt::try_init().ok(); - match result { - Ok(report) => { - println!("✓ Attestation verified successfully!"); - println!(" Module ID: {}", report.module_id); - println!(" Digest: {}", report.digest); - println!(" Timestamp: {}", report.timestamp); - println!(" PCRs: {} entries", report.pcrs.len()); - - // Print non-zero PCR values - for (idx, value) in &report.pcrs { - if !value.iter().all(|&b| b == 0) { - println!(" PCR{}: {:02x?}", idx, value); - } - } - } - Err(e) => { - // The test attestation may be expired or have other issues - // Print the error for debugging but don't fail the test - // since the attestation document is from a real enclave - // and may have time-based validity constraints - println!( - "Attestation verification failed (may be expected for old attestations): {:#}", - e - ); + let cose_data = extract_cose_sign1(ATTESTATION_BIN); - // Still verify we can parse the structure - let cose = CoseSign1::from_bytes(&cose_data).expect("Should parse COSE"); - let _doc = AttestationDocument::from_cbor(&cose.payload) - .expect("Should parse attestation document"); + let report = nsm_qvl::verify_attestation_with_crl( + &cose_data, + nsm_qvl::AWS_NITRO_ENCLAVES_ROOT_G1, + std::env::var("TEST_FETCH_CRL").is_ok(), + None, + ) + .await + .unwrap(); + println!("✓ Attestation verified successfully!"); + println!(" Module ID: {}", report.module_id); + println!(" Digest: {}", report.digest); + println!(" Timestamp: {}", report.timestamp); + println!(" PCRs: {} entries", report.pcrs.len()); + + // Print non-zero PCR values + for (idx, value) in &report.pcrs { + if !value.iter().all(|&b| b == 0) { + println!(" PCR{idx}: {value:02x?}"); } } } From 013be17661e1ebfd26b5550e8e2b40f9344b7d49 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Fri, 26 Dec 2025 12:09:04 +0000 Subject: [PATCH 078/264] Align workspace deps --- Cargo.toml | 3 +++ cert-client/Cargo.toml | 2 +- dstack-mr/Cargo.toml | 6 +++--- nsm-attest/Cargo.toml | 2 +- tpm-qvl/Cargo.toml | 23 ++++++++++------------- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6a24de3a6..bfb0c747f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,6 +138,7 @@ toml = "0.8.20" toml_edit = { version = "0.22.24", features = ["serde"] } yasna = "0.5.2" bytes = "1.10.1" +nom = "7.1" figment = "0.10.19" object = "0.36.4" fatfs = "0.3.6" @@ -181,6 +182,7 @@ getrandom = "0.3.1" hkdf = "0.12.4" p256 = "0.13.2" p384 = "0.13" +rsa = "0.9" ring = "0.17.14" rustls = "0.23.23" rustls-pki-types = "1.13.1" @@ -243,6 +245,7 @@ yaml-rust2 = "0.10.4" luks2 = "0.5.0" scopeguard = "1.2.0" flate2 = "1.1" +tar = "0.4" [profile.release] panic = "abort" diff --git a/cert-client/Cargo.toml b/cert-client/Cargo.toml index 087d6f09d..089b927bf 100644 --- a/cert-client/Cargo.toml +++ b/cert-client/Cargo.toml @@ -14,6 +14,6 @@ anyhow.workspace = true dstack-types.workspace = true dstack-kms-rpc.workspace = true ra-rpc = { workspace = true, features = ["client"] } -ra-tls.workspace = true +ra-tls = { workspace = true, features = ["quote"] } serde_json.workspace = true tdx-attest.workspace = true diff --git a/dstack-mr/Cargo.toml b/dstack-mr/Cargo.toml index 32a96f96e..12ca50091 100644 --- a/dstack-mr/Cargo.toml +++ b/dstack-mr/Cargo.toml @@ -28,6 +28,6 @@ scale.workspace = true [dev-dependencies] dstack-types.workspace = true -reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } -flate2 = "1.0" -tar = "0.4" +reqwest = { workspace = true, features = ["blocking"] } +flate2.workspace = true +tar.workspace = true diff --git a/nsm-attest/Cargo.toml b/nsm-attest/Cargo.toml index 1e8ca1726..09d3b3e4e 100644 --- a/nsm-attest/Cargo.toml +++ b/nsm-attest/Cargo.toml @@ -15,7 +15,7 @@ anyhow.workspace = true serde.workspace = true tracing.workspace = true aws-nitro-enclaves-nsm-api = "0.4" -ciborium = "0.2" +ciborium.workspace = true [dev-dependencies] hex.workspace = true diff --git a/tpm-qvl/Cargo.toml b/tpm-qvl/Cargo.toml index 6f66be59f..04d5fbb68 100644 --- a/tpm-qvl/Cargo.toml +++ b/tpm-qvl/Cargo.toml @@ -18,27 +18,24 @@ tracing.workspace = true dstack-types.workspace = true # Cryptographic verification -rsa = "0.9" -p256 = { version = "0.13", features = ["ecdsa", "pem"] } -x509-parser = "0.16" -nom = "7.1" -base64 = "0.22" +p256 = { workspace = true, features = ["ecdsa", "pem"] } +rsa.workspace = true +x509-parser.workspace = true +nom.workspace = true +base64.workspace = true sha2 = { workspace = true, features = ["oid"] } # Certificate chain verification -rustls-webpki = { version = "0.103.8", features = ["alloc", "ring"] } -rustls-pki-types = "1.13.1" -dcap-qvl-webpki = { version = "0.103", features = ["alloc", "ring"] } -pem = "3.0" +rustls-webpki = { workspace = true, features = ["alloc", "ring"] } +rustls-pki-types.workspace = true +dcap-qvl-webpki = { workspace = true, features = ["alloc", "ring"] } +pem.workspace = true # TPM quote data structures tpm-types.workspace = true # CRL download (optional) -reqwest = { version = "0.12", default-features = false, features = [ - "rustls-tls", - "blocking", -], optional = true } +reqwest = { workspace = true, features = ["blocking"], optional = true } [features] default = ["crl-download"] From ffb41d9a14bbff196a5b77b43a988a7507d56191 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Sun, 4 Jan 2026 03:16:15 +0000 Subject: [PATCH 079/264] Add attestation doc for gpc and nitro --- docs/attestation-gcp.md | 50 +++++++++++++++++++++++++ docs/attestation-nitro-enclave.md | 62 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 docs/attestation-gcp.md create mode 100644 docs/attestation-nitro-enclave.md diff --git a/docs/attestation-gcp.md b/docs/attestation-gcp.md new file mode 100644 index 000000000..01be5c658 --- /dev/null +++ b/docs/attestation-gcp.md @@ -0,0 +1,50 @@ +# Dstack GCP Attestation Flow (GCP TDX + TPM) + +This document describes how dstack produces and verifies attestation on GCP using +TDX plus a TPM quote. It follows the implementation in `dstack-attest`. + +## Components +- TDX quote generator: `tdx-attest::get_quote` +- TDX event log reader: `cc-eventlog::tdx::read_event_log` +- TPM quote generator: `tpm-attest::TpmContext::create_quote` +- Verifier: `dstack-attest` + `dcap-qvl` + `tpm-qvl` + +## Attestation Creation (guest side) +1. **Collect report_data** (64 bytes), optionally bound to RA TLS pubkey. +2. **Generate TDX quote** via `tdx-attest::get_quote(report_data)`. +3. **Read TDX event log** via `cc-eventlog::tdx::read_event_log()`. +4. **Compute TPM qualifying data** as `sha256(tdx_quote)`. +5. **Create TPM quote** with qualifying data and dstack PCR policy: + `tpm_attest::TpmContext::create_quote(qualifying_data, policy)`. +6. **Bundle** into `DstackGcpTdxQuote { tdx_quote, tpm_quote }`. +7. **Include config** from `/dstack/.host-shared/.sys-config.json`. + +## Attestation Verification (verifier side) +Verification runs in `Attestation::verify_with_time` and splits into TDX + TPM. + +### TDX verification +1. **Fetch TDX collateral** and verify quote: + `dcap_qvl::collateral::get_collateral_and_verify(quote, pccs_url)`. +2. **Validate TCB**: + - Debug mode must be off. + - `mr_signer_seam` must be all-zero. +3. **Replay runtime events** to compute RTMR3 and compare with quote RTMR3. +4. **Check report_data** in TD report equals the attestation `report_data`. + +### TPM verification +1. **Fetch TPM collateral** and verify quote: + `tpm_qvl::get_collateral_and_verify(tpm_quote)`. +2. **Replay runtime events** to compute runtime PCR and compare with quoted PCR. +3. **Check qualifying data** equals `sha256(tdx_quote)`. + +### Optional RA TLS binding +If the verifier provides a RA TLS pubkey, it enforces: +`report_data == QuoteContentType::RaTlsCert.to_report_data(pubkey)`. + +## Output +The verifier returns `DstackVerifiedReport::DstackGcpTdx` containing: +- `tdx_report` (verified TDX report and collateral info) +- `tpm_report` (verified TPM quote and PCRs) + +## Relevant Code +- `dstack-attest/src/attestation.rs` diff --git a/docs/attestation-nitro-enclave.md b/docs/attestation-nitro-enclave.md new file mode 100644 index 000000000..52881c640 --- /dev/null +++ b/docs/attestation-nitro-enclave.md @@ -0,0 +1,62 @@ +# Dstack Nitro Enclave Attestation Flow (NSM) + +This document describes how dstack produces and verifies attestation on AWS +Nitro Enclaves using the NSM attestation document. It follows the +implementation in `dstack-attest` and `nsm-qvl`. + +## Components +- NSM attestation generator: `nsm-attest::get_attestation` +- Verifier: `dstack-attest` + `nsm-qvl` + +## Attestation Creation (enclave side) +1. **Collect report_data** (64 bytes), optionally bound to RA TLS pubkey. +2. **Request NSM attestation** with user_data = report_data: + `nsm_attest::get_attestation(report_data)`. +3. **Bundle** into `DstackNitroQuote { nsm_quote }`. +4. **Include config** derived from PCRs: + `os_image_hash = sha256(PCR0 || PCR1 || PCR2)` (all zeros if PCRs are zero). + +The NSM attestation document (COSE_Sign1 payload) includes: +- `module_id`, `digest`, `timestamp` +- `pcrs` map +- signing `certificate` and `cabundle` +- optional `user_data`, `nonce`, `public_key` + +## Attestation Verification (verifier side) +Verification runs in `Attestation::verify_with_time`: + +### COSE and document checks (nsm-qvl) +1. **Parse COSE_Sign1** and require `alg = ES384 (-35)`. +2. **Validate COSE critical headers** (`crit`) if present. +3. **Parse attestation document** from payload and enforce: + - `digest == "SHA384"` + - PCR lengths are 48 bytes + - freshness window against `now` + +### Certificate chain and signature +4. **Verify cert chain** to `AWS_NITRO_ENCLAVES_ROOT_G1`. +5. **Verify COSE signature** using the leaf certificate P-384 key. +6. **Key usage sanity** on leaf cert (if present): + - must allow `digitalSignature` + - must not allow `keyCertSign` or `cRLSign` + +### Optional CRL verification +`nsm-qvl` exposes async CRL verification via: +`verify_attestation_with_crl(..., enable_crl, ...)`. +This is **disabled by default** in `dstack-attest` because CRL fetch from +S3 may return 403. The caller can enable CRL explicitly. + +### Dstack-specific checks +7. **Match user_data** to `report_data`. +8. **Decode PCRs** and return verified report. + +## Output +The verifier returns `DstackVerifiedReport::DstackNitroEnclave` containing: +- `module_id` +- `pcrs` (PCR0/1/2) +- `user_data` (report_data) +- `timestamp` + +## Relevant Code +- `dstack-attest/src/attestation.rs` +- `nsm-qvl/src/verify.rs` From cec804b87b416575b8dfae38ce701cc44ff3bcaf Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 14 Jan 2026 03:27:34 +0000 Subject: [PATCH 080/264] Update comment --- verifier/src/verification.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index 1c9786fdf..d9e6da519 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -625,15 +625,12 @@ impl CvmVerifier { } } - /// Verify GCP TDX image hash using PCR 2 Event Log + /// Verify Nitro Enclave OS image hash using NSM PCRs /// - /// For GCP TDX, we verify: - /// 1. PCR 0 matches expected GCP OVMF v2 firmware - /// 2. UKI hash by extracting Event 28 (3rd event in PCR 2) from TPM Event Log - /// - /// IMPORTANT: Extracting the 3rd event is GCP OVMF-specific behavior. - /// On GCP, PCR 2 events are ordered as: - /// [0]=EV_SEPARATOR, [1]=EV_EFI_GPT_EVENT, [2]=UKI (Event 28), [3]=Linux kernel + /// For Nitro: + /// 1. PCR0/1/2 come from the EIF build (code + kernel + app) in production mode. + /// 2. In debug mode, PCR0/1/2 are zeroed by AWS; we tolerate that and return 32 zero bytes. + /// 3. The computed image hash is compared against vm_config.os_image_hash. async fn verify_os_image_hash_for_nitro_enclave( &self, vm_config: &VmConfig, From db8356abd5d5b4a5cd8118bf68bc31c2afb9a2f9 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 14 Jan 2026 08:34:01 +0000 Subject: [PATCH 081/264] Update dcap-qvl to 0.3.8 --- Cargo.lock | 39 +++++++++++++++++++++++---------------- Cargo.toml | 2 +- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb315eda8..eb735d209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ "auto_impl", "borsh", "c-kzg", - "derive_more 2.1.0", + "derive_more 2.1.1", "either", "k256", "once_cell", @@ -184,7 +184,7 @@ dependencies = [ "auto_impl", "borsh", "c-kzg", - "derive_more 2.1.0", + "derive_more 2.1.1", "either", "serde", "serde_with", @@ -238,7 +238,7 @@ dependencies = [ "alloy-sol-types", "async-trait", "auto_impl", - "derive_more 2.1.0", + "derive_more 2.1.1", "futures-utils-wasm", "serde", "serde_json", @@ -268,7 +268,7 @@ dependencies = [ "bytes", "cfg-if", "const-hex", - "derive_more 2.1.0", + "derive_more 2.1.1", "foldhash 0.2.0", "hashbrown 0.16.1", "indexmap 2.12.1", @@ -460,7 +460,7 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "arrayvec 0.7.6", - "derive_more 2.1.0", + "derive_more 2.1.1", "nybbles", "serde", "smallvec", @@ -1880,9 +1880,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "dcap-qvl" -version = "0.3.4" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435989ce7ba46ba3f837f9df3c8139469e72ae810e707893b19f8b6b370d14ef" +checksum = "82f4049f76ea6a67262a7501b82f9cda2425c022a86b45902d195d70fa67a4f7" dependencies = [ "anyhow", "asn1_der", @@ -1893,6 +1893,7 @@ dependencies = [ "const-oid", "dcap-qvl-webpki", "der", + "derive_more 2.1.1", "futures", "hex", "log", @@ -2009,11 +2010,11 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ - "derive_more-impl 2.1.0", + "derive_more-impl 2.1.1", ] [[package]] @@ -2031,9 +2032,9 @@ dependencies = [ [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case 0.10.0", "proc-macro2", @@ -6114,7 +6115,7 @@ name = "rocket-vsock-listener" version = "0.5.5" dependencies = [ "anyhow", - "derive_more 2.1.0", + "derive_more 2.1.1", "pin-project", "rocket", "serde", @@ -6830,16 +6831,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "indexmap 2.12.1", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -8930,3 +8931,9 @@ dependencies = [ "quote", "syn 2.0.111", ] + +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/Cargo.toml b/Cargo.toml index bfb0c747f..19bfed8eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,7 +175,7 @@ default-net = "0.22.0" # Cryptography/Security aes-gcm = "0.10.3" curve25519-dalek = "4.1.3" -dcap-qvl = "0.3.4" +dcap-qvl = "0.3.8" dcap-qvl-webpki = "0.103" elliptic-curve = { version = "0.13.8", features = ["pkcs8"] } getrandom = "0.3.1" From 23ccf00807806f1dbec9eaba81e8105e260e0979 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 15 Jan 2026 01:36:58 +0000 Subject: [PATCH 082/264] Update docs for agent.Attest --- sdk/curl/api.md | 1 + sdk/go/README.md | 9 +++++++++ sdk/js/README.md | 14 ++++++++++++++ sdk/python/README.md | 15 +++++++++++++++ sdk/rust/README.md | 8 ++++++++ verifier/README.md | 25 ++++++++++++++++++++++--- 6 files changed, 69 insertions(+), 3 deletions(-) diff --git a/sdk/curl/api.md b/sdk/curl/api.md index 99f6c232a..1e05bd42c 100644 --- a/sdk/curl/api.md +++ b/sdk/curl/api.md @@ -268,6 +268,7 @@ curl --unix-socket /var/run/dstack.sock -X POST \ ### 8. Attest Generates a versioned attestation with the given report data. Returns a dstack-defined attestation format that supports different attestation modes across platforms. +You can submit the returned `attestation` directly to the verifier `/verify` endpoint. **Endpoint:** `/Attest` diff --git a/sdk/go/README.md b/sdk/go/README.md index 4379d2e61..adeba6c36 100644 --- a/sdk/go/README.md +++ b/sdk/go/README.md @@ -62,6 +62,14 @@ func main() { return } fmt.Println(rtmrs) // map[0:00000000000000000 ... + + // Generate versioned attestation + attestResp, err := client.Attest(context.Background(), []byte("test")) + if err != nil { + fmt.Println(err) + return + } + fmt.Println(attestResp.Attestation) // 0x00000000000000000 ... } ``` @@ -93,6 +101,7 @@ NOTE: Leave endpoint empty in production. You only need to add `volumes` in your - `Info(ctx context.Context) (*InfoResponse, error)`: Retrieves information about the CVM instance. - `GetKey(ctx context.Context, path string, purpose string, algorithm string) (*GetKeyResponse, error)`: Derives a key for the given path, purpose and algorithm. - `GetQuote(ctx context.Context, reportData []byte) (*GetQuoteResponse, error)`: Generates a TDX quote using SHA512 as the hash algorithm. +- `Attest(ctx context.Context, reportData []byte) (*AttestResponse, error)`: Generates a versioned attestation with the given report data. - `GetTlsKey(ctx context.Context, path string, subject string, altNames []string, usageRaTls bool, usageServerAuth bool, usageClientAuth bool, randomSeed bool) (*GetTlsKeyResponse, error)`: Derives a key for the given path and purpose. - `Sign(ctx context.Context, algorithm string, data []byte) (*SignResponse, error)`: Signs a payload - `Verify(ctx context.Context, algorithm string, data []byte, signature []byte, public_key []byte) (*VerifyResponse, error)`: Verifies a payload diff --git a/sdk/js/README.md b/sdk/js/README.md index 7d6da3af0..7fa7aa5f0 100644 --- a/sdk/js/README.md +++ b/sdk/js/README.md @@ -557,6 +557,20 @@ Generates a TDX attestation quote containing the provided report data. - Cryptographic proof of execution environment - Audit trail generation +##### `attest(reportData: string | Buffer | Uint8Array): Promise` + +Generates a versioned attestation containing the provided report data. + +**Parameters:** +- `reportData`: Data to include in attestation (max 64 bytes) + +**Returns:** `AttestResponse` +- `attestation`: Hex-encoded attestation payload + +**Use Cases:** +- Remote attestation across multiple platform types +- Verifier APIs that accept versioned attestations + ##### `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. diff --git a/sdk/python/README.md b/sdk/python/README.md index 8b6d521a6..855c1f1bc 100644 --- a/sdk/python/README.md +++ b/sdk/python/README.md @@ -532,6 +532,7 @@ All methods below are available in both synchronous and asynchronous clients wit | `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.attest(...)` | `await async_client.attest(...)` | Generate versioned attestation | | `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 | @@ -616,6 +617,20 @@ Generates a TDX attestation quote containing the provided report data. - Cryptographic proof of execution environment - Audit trail generation +##### `attest(report_data: str | bytes) -> AttestResponse` + +Generates a versioned attestation containing the provided report data. + +**Parameters:** +- `report_data`: Data to include in attestation (max 64 bytes) + +**Returns:** `AttestResponse` +- `attestation`: Hex-encoded attestation payload + +**Use Cases:** +- Remote attestation across multiple platform types +- Verifier APIs that accept versioned attestations + ##### `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. diff --git a/sdk/rust/README.md b/sdk/rust/README.md index 2ae47ad17..83118132a 100644 --- a/sdk/rust/README.md +++ b/sdk/rust/README.md @@ -35,6 +35,10 @@ async fn main() -> Result<(), Box> { let rtmrs = quote_resp.replay_rtmrs()?; println!("Replayed RTMRs: {:?}", rtmrs); + // Generate versioned attestation + let attest_resp = client.attest(b"test-data".to_vec()).await?; + println!("Attestation: {}", attest_resp.attestation); + // Emit an event client.emit_event("BootComplete".to_string(), b"payload-data".to_vec()).await?; @@ -114,6 +118,10 @@ Generates a TDX quote with a custom 64-byte payload. - `event_log`: Serialized list of events - `replay_rtmrs()`: Reconstructs RTMR values from the event log +#### `attest(report_data: Vec) -> AttestResponse` +Generates a versioned attestation with a custom 64-byte payload. +- `attestation`: Hex-encoded attestation + #### `emit_event(event: String, payload: Vec)` Sends an event log with associated binary payload to the runtime. diff --git a/verifier/README.md b/verifier/README.md index 4d3d86186..d3ae2767e 100644 --- a/verifier/README.md +++ b/verifier/README.md @@ -6,9 +6,11 @@ A HTTP server that provides dstack quote verification services using the same ve ### POST /verify -Verifies a dstack quote with the provided quote and VM configuration. The body can be grabbed via [getQuote](https://github.com/Dstack-TEE/dstack/blob/master/sdk/curl/api.md#3-get-quote). +Verifies a dstack attestation or quote with the provided data and VM configuration. The body can be grabbed via [getQuote](https://github.com/Dstack-TEE/dstack/blob/master/sdk/curl/api.md#3-get-quote) or [attest](https://github.com/Dstack-TEE/dstack/blob/master/sdk/curl/api.md#8-attest). **Request Body:** +Provide either `attestation` or (`quote` + `event_log` + `vm_config`). + ```json { "quote": "hex-encoded-quote", @@ -16,6 +18,12 @@ Verifies a dstack quote with the provided quote and VM configuration. The body c "vm_config": "json-vm-config-string", } ``` +or +```json +{ + "attestation": "hex-encoded-attestation", +} +``` **Response:** ```json @@ -26,7 +34,7 @@ Verifies a dstack quote with the provided quote and VM configuration. The body c "event_log_verified": true, "os_image_hash_verified": true, "report_data": "hex-encoded-64-byte-report-data", - "tcb_status": "OK", + "tcb_status": "UpToDate", "advisory_ids": [], "app_info": { "app_id": "hex-string", @@ -113,7 +121,18 @@ Save the docker compose file as `docker-compose.yml` and run `docker compose up ### Request verification -Grab a quote from your app. It's depends on your app how to grab a quote. +Grab an attestation or quote from your app. It's depends on your app how to grab it. + +```bash +# Grab an attestation from the demo app +curl https://712eab2f507b963e11144ae67218177e93ac2a24-3000.test0.dstack.org:12004/Attest?report_data=0x1234 -o attest.json +``` + +Send the attestation to the verifier. + +```bash +$ curl -s -d @attest.json localhost:8080/verify | jq +``` ```bash # Grab a quote from the demo app From 709339c7420d7a21067fb72149ba336c27825742 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 15 Jan 2026 08:00:35 +0000 Subject: [PATCH 083/264] Fix unit tests --- guest-agent/fixtures/attestation.bin | Bin 39090 -> 9504 bytes nsm-qvl/tests/verify_test.rs | 6 +++++- ra-tls/src/cert.rs | 15 +++++++-------- sdk/go/dstack/client_test.go | 2 +- sdk/simulator/attestation.bin | Bin 39090 -> 9504 bytes 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/guest-agent/fixtures/attestation.bin b/guest-agent/fixtures/attestation.bin index 2f7a9c35efad571c2e964cf40f8b48d87d345d51..2c4aef253944d9ad266d0fda37a558fd559dd74e 100644 GIT binary patch delta 4749 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 HbIbcT@gMJ# literal 39090 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 HbIbcT@gMJ# literal 39090 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 Date: Thu, 15 Jan 2026 12:40:38 +0000 Subject: [PATCH 084/264] dstack-util: Add --root-ca to get-key --- dstack-util/src/main.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index 0caf5f648..88ef12cb2 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -351,6 +351,11 @@ struct GetKeysArgs { /// Output file path (default: stdout as JSON) #[arg(short, long)] output: Option, + + /// Root CA certificate (PEM format) to pin for TLS verification. + /// If not provided, TLS certificate verification is skipped for the initial connection. + #[arg(long)] + root_ca: Option, } fn pad64(data: &[u8]) -> Result<[u8; 64]> { @@ -533,7 +538,7 @@ fn cmd_attest_strip(args: AttestStripArgs) -> Result<()> { async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { use dstack_kms_rpc::kms_client::KmsClient; - use ra_rpc::client::{RaClient, RaClientConfig}; + use ra_rpc::client::RaClientConfig; let kms_url = if args.kms_url.ends_with("/prpc") { args.kms_url.clone() @@ -541,10 +546,30 @@ async fn cmd_get_keys(args: GetKeysArgs) -> Result<()> { format!("{}/prpc", args.kms_url.trim_end_matches('/')) }; + // Load root CA if provided for TLS pinning + let root_ca_pem = if let Some(root_ca_path) = &args.root_ca { + let pem = fs::read_to_string(root_ca_path) + .with_context(|| format!("failed to read root CA from {}", root_ca_path.display()))?; + Some(pem) + } else { + None + }; + // Step 1: Get temporary CA certificate eprintln!("Connecting to KMS: {kms_url}"); + let tls_no_check = root_ca_pem.is_none(); + if tls_no_check { + eprintln!("Warning: no --root-ca provided, TLS certificate verification is disabled for initial connection"); + } let tmp_ca = { - let client = RaClient::new(kms_url.clone(), true)?; + let client = RaClientConfig::builder() + .remote_uri(kms_url.clone()) + .tls_no_check(tls_no_check) + .tls_built_in_root_certs(false) + .maybe_tls_ca_cert(root_ca_pem.clone()) + .build() + .into_client() + .context("failed to create client")?; let kms_client = KmsClient::new(client); kms_client .get_temp_ca_cert() From 68558a9e378e25b8e3c8a2bd82649745971685a8 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 15 Jan 2026 12:41:08 +0000 Subject: [PATCH 085/264] kms: Fix the quote display in onboard page --- kms/src/www/onboard.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/kms/src/www/onboard.html b/kms/src/www/onboard.html index a8b674a77..7904b84dc 100644 --- a/kms/src/www/onboard.html +++ b/kms/src/www/onboard.html @@ -206,7 +206,7 @@

Onboard from an Existing KMS Instance

methods: { async handleBootstrap() { try { - const { ca_pubkey, k256_pubkey, quote, eventlog, error } = await rpcCall('Bootstrap', { + const { ca_pubkey, k256_pubkey, attestation, error } = await rpcCall('Bootstrap', { domain: this.bootstrapDomain }); @@ -216,8 +216,7 @@

Onboard from an Existing KMS Instance

this.result = JSON.stringify({ caPubkey: '0x' + ca_pubkey, k256Pubkey: '0x' + k256_pubkey, - quote: '0x' + quote, - eventlog: '0x' + eventlog + attestation: '0x' + attestation }, null, 2); this.error = ''; } catch (err) { From 425ae9c189a051bf418a60c0ea3bef8e8c9d3f4c Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 19 Jan 2026 01:24:41 +0000 Subject: [PATCH 086/264] Switch LICENSE to BUSL-1.1 --- .github/workflows/gateway-release.yml | 2 +- .github/workflows/kms-release.yml | 2 +- .github/workflows/rust-sdk-release.yml | 2 +- .github/workflows/rust.yml | 2 +- .github/workflows/sdk.yaml | 2 +- .github/workflows/spdx-check.yml | 2 +- .github/workflows/verifier-release.yml | 2 +- .github/workflows/vmm-ui.yml | 2 +- Cargo.toml | 2 +- LICENSE | 289 +-- LICENSES/BUSL-1.1.txt | 88 + Makefile | 2 +- README.md | 8 +- REUSE.toml | 22 +- basefiles/app-compose.sh | 2 +- .../containerd.service.d/dstack-prepare.conf | 2 +- .../docker.service.d/dstack-guest-agent.conf | 2 +- .../docker.service.d/dstack-prepare.conf | 2 +- basefiles/dstack-prepare.sh | 2 +- basefiles/wg-checker.sh | 2 +- cargo-check-all.sh | 2 +- cc-eventlog/Cargo.toml | 2 +- cc-eventlog/src/codecs.rs | 2 +- cc-eventlog/src/lib.rs | 2 +- cc-eventlog/src/runtime_events.rs | 2 +- cc-eventlog/src/tcg.rs | 2 +- cc-eventlog/src/tdx.rs | 2 +- cc-eventlog/src/tpm.rs | 2 +- cert-client/Cargo.toml | 2 +- cert-client/src/lib.rs | 2 +- certbot/Cargo.toml | 2 +- certbot/cli/Cargo.toml | 2 +- certbot/cli/src/main.rs | 2 +- certbot/src/acme_client.rs | 2 +- certbot/src/acme_client/tests.rs | 2 +- certbot/src/bot.rs | 2 +- certbot/src/bot/tests.rs | 2 +- certbot/src/dns01_client.rs | 2 +- certbot/src/dns01_client/cloudflare.rs | 2 +- certbot/src/lib.rs | 2 +- certbot/src/workdir.rs | 2 +- cliff.toml | 2 +- ct_monitor/Cargo.toml | 2 +- ct_monitor/src/main.rs | 2 +- dstack-attest/Cargo.toml | 2 +- dstack-attest/src/attestation.rs | 2 +- dstack-attest/src/lib.rs | 2 +- dstack-attest/tests/nitro_verify.rs | 2 +- dstack-mr/Cargo.toml | 2 +- dstack-mr/cli/Cargo.toml | 2 +- dstack-mr/cli/src/main.rs | 2 +- dstack-mr/src/acpi.rs | 2 +- dstack-mr/src/kernel.rs | 2 +- dstack-mr/src/lib.rs | 2 +- dstack-mr/src/machine.rs | 2 +- dstack-mr/src/num.rs | 2 +- dstack-mr/src/tdvf.rs | 2 +- dstack-mr/src/util.rs | 2 +- dstack-mr/tests/tdvf_parse.rs | 2 +- dstack-types/Cargo.toml | 2 +- dstack-types/src/lib.rs | 2 +- dstack-types/src/mr_config.rs | 2 +- dstack-types/src/shared_filenames.rs | 2 +- dstack-util/Cargo.toml | 2 +- dstack-util/src/crypto.rs | 2 +- dstack-util/src/docker_compose.rs | 2 +- dstack-util/src/host_api.rs | 2 +- dstack-util/src/main.rs | 2 +- dstack-util/src/parse_env_file.rs | 2 +- dstack-util/src/system_setup.rs | 2 +- .../src/system_setup/config_id_verifier.rs | 2 +- dstack-util/src/utils.rs | 2 +- .../fixtures/key-provider-docker-compose.yaml | 2 +- .../fixtures/luks_header_cipher_null.license | 2 +- .../tests/fixtures/luks_header_good.license | 2 +- dstack-util/tests/test_remove_orphans.sh | 2 +- gateway/Cargo.toml | 2 +- gateway/dstack-app/builder/Dockerfile | 2 +- gateway/dstack-app/builder/build-image.sh | 2 +- gateway/dstack-app/builder/entrypoint.sh | 2 +- .../dstack-app/builder/shared/pin-packages.sh | 2 +- gateway/dstack-app/deploy-to-vmm.sh | 2 +- gateway/dstack-app/docker-compose.yaml | 2 +- gateway/gateway.toml | 2 +- gateway/rpc/Cargo.toml | 2 +- gateway/rpc/build.rs | 2 +- gateway/rpc/proto/gateway_rpc.proto | 2 +- gateway/rpc/src/lib.rs | 2 +- gateway/src/admin_service.rs | 2 +- gateway/src/config.rs | 2 +- gateway/src/main.rs | 2 +- gateway/src/main_service.rs | 2 +- gateway/src/main_service/auth_client.rs | 2 +- gateway/src/main_service/sync_client.rs | 2 +- gateway/src/main_service/tests.rs | 2 +- gateway/src/models.rs | 2 +- gateway/src/proxy.rs | 2 +- gateway/src/proxy/io_bridge.rs | 2 +- gateway/src/proxy/sni.rs | 2 +- gateway/src/proxy/tls_passthough.rs | 2 +- gateway/src/proxy/tls_terminate.rs | 2 +- gateway/src/web_routes.rs | 2 +- gateway/src/web_routes/route_index.rs | 2 +- gateway/templates/dashboard.html | 2 +- gateway/templates/rproxy.yaml | 2 +- guest-agent/Cargo.toml | 2 +- guest-agent/dstack.toml | 2 +- guest-agent/rpc/Cargo.toml | 2 +- guest-agent/rpc/build.rs | 2 +- guest-agent/rpc/proto/agent_rpc.proto | 2 +- guest-agent/rpc/src/lib.rs | 2 +- guest-agent/src/config.rs | 2 +- guest-agent/src/guest_api_service.rs | 2 +- guest-agent/src/http_routes.rs | 2 +- guest-agent/src/main.rs | 2 +- guest-agent/src/models.rs | 2 +- guest-agent/src/rpc_service.rs | 2 +- guest-agent/templates/dashboard.html | 2 +- guest-api/Cargo.toml | 2 +- guest-api/build.rs | 2 +- guest-api/proto/guest_api.proto | 2 +- guest-api/src/client.rs | 2 +- guest-api/src/lib.rs | 2 +- host-api/Cargo.toml | 2 +- host-api/build.rs | 2 +- host-api/proto/host_api.proto | 2 +- host-api/src/client.rs | 2 +- host-api/src/lib.rs | 2 +- http-client/Cargo.toml | 2 +- http-client/src/hyper_vsock.rs | 2 +- http-client/src/lib.rs | 2 +- http-client/src/prpc.rs | 2 +- iohash/Cargo.toml | 2 +- iohash/src/main.rs | 2 +- key-provider-build/Dockerfile.aesmd | 2 +- key-provider-build/Dockerfile.key-provider | 2 +- key-provider-build/docker-compose.yaml | 2 +- key-provider-build/entrypoint-aesmd.sh | 2 +- key-provider-build/entrypoint-key-provider.sh | 2 +- key-provider-build/run.sh | 2 +- key-provider-client/Cargo.toml | 2 +- key-provider-client/src/host.rs | 2 +- key-provider-client/src/lib.rs | 2 +- kms/Cargo.toml | 2 +- kms/auth-eth-bun/index.test.ts | 2 +- kms/auth-eth-bun/index.ts | 2 +- kms/auth-eth-bun/vitest.config.ts | 2 +- kms/auth-eth/contracts/DstackApp.sol | 2 +- kms/auth-eth/contracts/DstackKms.sol | 2 +- kms/auth-eth/contracts/IAppAuth.sol | 2 +- .../contracts/IAppAuthBasicManagement.sol | 2 +- kms/auth-eth/hardhat.config.ts | 2 +- kms/auth-eth/jest.config.js | 2 +- kms/auth-eth/jest.integration.config.js | 2 +- kms/auth-eth/lib/deployment-helpers.ts | 2 +- kms/auth-eth/run-tests.sh | 2 +- kms/auth-eth/scripts/deploy.ts | 2 +- kms/auth-eth/scripts/upgrade.ts | 2 +- kms/auth-eth/scripts/verify.ts | 2 +- kms/auth-eth/src/ethereum.ts | 2 +- kms/auth-eth/src/main.ts | 2 +- kms/auth-eth/src/server.ts | 2 +- kms/auth-eth/src/types.ts | 2 +- kms/auth-eth/test/DstackApp.test.ts | 2 +- .../test/ethereum.integration.test.ts | 2 +- kms/auth-eth/test/ethereum.test.ts | 2 +- kms/auth-eth/test/main.test.ts | 2 +- kms/auth-eth/test/setup.ts | 2 +- kms/auth-mock/index.test.ts | 2 +- kms/auth-mock/index.ts | 2 +- kms/auth-mock/vitest.config.ts | 2 +- kms/auth-simple/index.test.ts | 2 +- kms/auth-simple/index.ts | 2 +- kms/auth-simple/vitest.config.ts | 2 +- kms/dstack-app/builder/Dockerfile | 2 +- kms/dstack-app/builder/build-image.sh | 2 +- kms/dstack-app/builder/shared/config-qemu.sh | 2 +- kms/dstack-app/builder/shared/pin-packages.sh | 2 +- kms/dstack-app/compose-dev.yaml | 2 +- kms/dstack-app/compose-simple.yaml | 2 +- kms/dstack-app/deploy-simple.sh | 2 +- kms/dstack-app/deploy-to-vmm.sh | 2 +- kms/dstack-app/docker-compose.yaml | 2 +- kms/dstack-app/entrypoint.sh | 2 +- kms/kms.toml | 2 +- kms/rpc/Cargo.toml | 2 +- kms/rpc/build.rs | 2 +- kms/rpc/proto/kms_rpc.proto | 2 +- kms/rpc/src/lib.rs | 2 +- kms/src/config.rs | 2 +- kms/src/crypto.rs | 2 +- kms/src/ct_log.rs | 2 +- kms/src/main.rs | 2 +- kms/src/main_service.rs | 2 +- kms/src/main_service/upgrade_authority.rs | 2 +- kms/src/onboard_service.rs | 2 +- kms/src/www/onboard.html | 2 +- load_config/Cargo.toml | 2 +- load_config/src/lib.rs | 2 +- lspci/Cargo.toml | 2 +- lspci/src/lib.rs | 2 +- no_std_check/Cargo.toml | 2 +- no_std_check/src/lib.rs | 2 +- nsm-attest/Cargo.toml | 2 +- nsm-attest/src/lib.rs | 2 +- nsm-attest/src/types.rs | 2 +- nsm-attest/tests/attestation_test.rs | 3 +- nsm-qvl/Cargo.toml | 2 +- nsm-qvl/src/collateral.rs | 2 +- nsm-qvl/src/lib.rs | 2 +- nsm-qvl/src/verify.rs | 2 +- nsm-qvl/tests/verify_test.rs | 2 +- python/ct_monitor/ct_monitor.py | 2 +- python/ct_monitor/pyproject.toml | 2 +- ra-rpc/Cargo.toml | 2 +- ra-rpc/src/client.rs | 2 +- ra-rpc/src/lib.rs | 2 +- ra-rpc/src/openapi.rs | 2 +- ra-rpc/src/rocket_helper.rs | 2 +- ra-tls/Cargo.toml | 2 +- ra-tls/src/attestation.rs | 2 +- ra-tls/src/cert.rs | 2 +- ra-tls/src/kdf.rs | 2 +- ra-tls/src/lib.rs | 2 +- ra-tls/src/oids.rs | 2 +- ra-tls/src/traits.rs | 2 +- rocket-vsock-listener/Cargo.toml | 2 +- rocket-vsock-listener/src/lib.rs | 2 +- run-tests.sh | 2 +- run.sh | 2 +- scripts/add-spdx-attribution.py | 2 +- scripts/config-fw.sh | 2 +- serde-duration/Cargo.toml | 2 +- serde-duration/src/lib.rs | 2 +- size-parser/Cargo.toml | 2 +- size-parser/src/lib.rs | 2 +- sodiumbox/Cargo.toml | 2 +- sodiumbox/src/lib.rs | 2 +- supervisor/Cargo.toml | 2 +- supervisor/client/Cargo.toml | 2 +- supervisor/client/src/lib.rs | 2 +- supervisor/client/src/main.rs | 2 +- supervisor/src/lib.rs | 2 +- supervisor/src/main.rs | 2 +- supervisor/src/process.rs | 2 +- supervisor/src/supervisor.rs | 2 +- supervisor/src/web_api.rs | 2 +- supervisor/supervisor.toml | 2 +- supervisor/tests/test-cli.sh | 2 +- supervisor/tests/test.sh | 2 +- tdx-attest/Cargo.toml | 2 +- tdx-attest/src/dummy.rs | 2 +- tdx-attest/src/lib.rs | 2 +- tdx-attest/src/linux.rs | 2 +- tdx-attest/src/linux/configfs.rs | 2 +- test-scripts/get-app-key.sh | 2 +- test-scripts/inspect-cert.sh | 2 +- tpm-attest/Cargo.toml | 2 +- tpm-attest/src/esapi.rs | 2 +- tpm-attest/src/gcp_ak.rs | 2 +- tpm-attest/src/lib.rs | 2 +- tpm-qvl/Cargo.toml | 2 +- tpm-qvl/src/collateral.rs | 2 +- tpm-qvl/src/lib.rs | 2 +- tpm-qvl/src/verify.rs | 2 +- tpm-types/Cargo.toml | 2 +- tpm-types/src/lib.rs | 2 +- tpm2/Cargo.toml | 2 +- tpm2/src/bin/tpm2-test.rs | 2 +- tpm2/src/commands.rs | 2 +- tpm2/src/constants.rs | 2 +- tpm2/src/device.rs | 2 +- tpm2/src/lib.rs | 2 +- tpm2/src/marshal.rs | 2 +- tpm2/src/session.rs | 2 +- tpm2/src/types.rs | 2 +- verifier/Cargo.toml | 2 +- verifier/builder/Dockerfile | 2 +- verifier/builder/build-image.sh | 2 +- verifier/builder/shared/config-qemu.sh | 2 +- verifier/builder/shared/pin-packages.sh | 2 +- verifier/dstack-verifier.toml | 2 +- verifier/src/lib.rs | 2 +- verifier/src/main.rs | 2 +- verifier/src/types.rs | 2 +- verifier/src/verification.rs | 2 +- verifier/test.sh | 2 +- vmm/Cargo.toml | 2 +- vmm/rpc/Cargo.toml | 2 +- vmm/rpc/build.rs | 2 +- vmm/rpc/proto/prpc.proto | 2 +- vmm/rpc/proto/vmm_rpc.proto | 2 +- vmm/rpc/src/lib.rs | 2 +- vmm/src/app.rs | 2 +- vmm/src/app/id_pool.rs | 2 +- vmm/src/app/image.rs | 2 +- vmm/src/app/qemu.rs | 2 +- vmm/src/config.rs | 2 +- vmm/src/console_v0.html | 2 +- vmm/src/console_v1.html | 1568 ++++++++--------- vmm/src/guest_api_service.rs | 2 +- vmm/src/host_api_service.rs | 2 +- vmm/src/main.rs | 2 +- vmm/src/main_routes.rs | 2 +- vmm/src/main_service.rs | 2 +- vmm/src/one_shot.rs | 2 +- vmm/src/openapi.rs | 2 +- vmm/src/setup-user.sh | 2 +- vmm/src/tests/test-compose.sh | 2 +- vmm/src/tests/test-deployment.sh | 2 +- vmm/src/vmm-cli.py | 2 +- vmm/ui/build.mjs | 4 +- vmm/ui/scripts/build_proto.sh | 2 +- vmm/ui/src/App.ts | 2 +- vmm/ui/src/components/CreateVmDialog.ts | 2 +- vmm/ui/src/components/EncryptedEnvEditor.ts | 2 +- vmm/ui/src/components/ForkVmDialog.ts | 2 +- vmm/ui/src/components/GpuConfigEditor.ts | 2 +- vmm/ui/src/components/PortMappingEditor.ts | 2 +- vmm/ui/src/components/UpdateVmDialog.ts | 2 +- vmm/ui/src/composables/useVmManager.ts | 2 +- vmm/ui/src/index.html | 2 +- vmm/ui/src/lib/vmmRpcClient.ts | 2 +- vmm/ui/src/main.ts | 2 +- vmm/ui/src/styles/main.css | 2 +- vmm/ui/src/templates/app.html | 2 +- vmm/venv.sh | 2 +- vmm/vmm.toml | 2 +- 328 files changed, 1303 insertions(+), 1321 deletions(-) create mode 100644 LICENSES/BUSL-1.1.txt diff --git a/.github/workflows/gateway-release.yml b/.github/workflows/gateway-release.yml index e983b89a0..188e2428a 100644 --- a/.github/workflows/gateway-release.yml +++ b/.github/workflows/gateway-release.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: Gateway Release diff --git a/.github/workflows/kms-release.yml b/.github/workflows/kms-release.yml index b53843720..1043b6002 100644 --- a/.github/workflows/kms-release.yml +++ b/.github/workflows/kms-release.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: KMS Release diff --git a/.github/workflows/rust-sdk-release.yml b/.github/workflows/rust-sdk-release.yml index 9e85c6b2a..c9ae3f85b 100644 --- a/.github/workflows/rust-sdk-release.yml +++ b/.github/workflows/rust-sdk-release.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: Publish SDK to crates.io on: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 58e709e2b..be84b6097 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: Rust checks diff --git a/.github/workflows/sdk.yaml b/.github/workflows/sdk.yaml index e8abf7afe..ec015a7ca 100644 --- a/.github/workflows/sdk.yaml +++ b/.github/workflows/sdk.yaml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: SDK tests permissions: diff --git a/.github/workflows/spdx-check.yml b/.github/workflows/spdx-check.yml index 2839a74bb..9660635ea 100644 --- a/.github/workflows/spdx-check.yml +++ b/.github/workflows/spdx-check.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: SPDX License Check diff --git a/.github/workflows/verifier-release.yml b/.github/workflows/verifier-release.yml index a7a4d28dc..e3101529e 100644 --- a/.github/workflows/verifier-release.yml +++ b/.github/workflows/verifier-release.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: Verifier Release diff --git a/.github/workflows/vmm-ui.yml b/.github/workflows/vmm-ui.yml index 6b2c5c626..4b1fabfd2 100644 --- a/.github/workflows/vmm-ui.yml +++ b/.github/workflows/vmm-ui.yml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 name: VMM UI build diff --git a/Cargo.toml b/Cargo.toml index 24f3b75fe..79ecf41cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Created-for-a-purpose # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [workspace.package] version = "0.5.5" diff --git a/LICENSE b/LICENSE index 261eeb9e9..fd94e8eaf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,88 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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 - - http://www.apache.org/licenses/LICENSE-2.0 - - 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. +Business Source License 1.1 + +Parameters + +Licensor: Phala Network +Licensed Work: dstack-cloud + The Licensed Work is (c) Phala Network +Additional Use Grant: None + +Change Date: Four years from the date a MINOR version (SemVer) is published. + +Change License: GNU Affero General Public License Version 3 (AGPL-3.0-only) + +Notice + +License text copyright (c) 2023 MariaDB plc, All Rights Reserved. +“Business Source License” is a trademark of MariaDB plc. + +----------------------------------------------------------------------------- + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. \ No newline at end of file diff --git a/LICENSES/BUSL-1.1.txt b/LICENSES/BUSL-1.1.txt new file mode 100644 index 000000000..fd94e8eaf --- /dev/null +++ b/LICENSES/BUSL-1.1.txt @@ -0,0 +1,88 @@ +Business Source License 1.1 + +Parameters + +Licensor: Phala Network +Licensed Work: dstack-cloud + The Licensed Work is (c) Phala Network +Additional Use Grant: None + +Change Date: Four years from the date a MINOR version (SemVer) is published. + +Change License: GNU Affero General Public License Version 3 (AGPL-3.0-only) + +Notice + +License text copyright (c) 2023 MariaDB plc, All Rights Reserved. +“Business Source License” is a trademark of MariaDB plc. + +----------------------------------------------------------------------------- + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. \ No newline at end of file diff --git a/Makefile b/Makefile index 0d04c03a9..3100a4fc2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 DOMAIN := local TO := ./certs diff --git a/README.md b/README.md index 2ed0d5510..246cd5959 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ dstack is the open framework for confidential AI - deploy AI applications with c 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. +### What is dstack-cloud? + +`dstack-cloud` is a commercial distribution of dstack. It primarily extends the open framework with additional support for cloud platforms such as Google Cloud (GCP) and AWS Nitro Enclaves. + ### Features **Zero friction onboarding** @@ -206,4 +210,6 @@ Logo and branding assets: [dstack-logo-kit](./docs/assets/dstack-logo-kit/) ## License -Apache 2.0 +This repository is licensed under the Business Source License 1.1 (BUSL-1.1). Per the terms in [LICENSE](./LICENSE), the Licensed Work is `dstack-cloud`. + +By default, BUSL-1.1 permits copying, modification, redistribution, and **non-production** use. The license does not grant production use unless an Additional Use Grant is provided (this project specifies: None). If your intended use does not comply with BUSL-1.1, you must obtain a commercial license from Phala Network (or authorized resellers). diff --git a/REUSE.toml b/REUSE.toml index bbe22a71c..fbf21f15f 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -8,12 +8,12 @@ SPDX-PackageDownloadLocation = "https://github.com/Dstack-TEE/dstack" [[annotations]] path = "**/*.md" SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" [[annotations]] path = "**/requirements.txt" SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" [[annotations]] path = [ @@ -37,7 +37,7 @@ path = [ "gateway/dstack-app/builder/shared/pinned-packages.txt", ] SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" [[annotations]] path = [ @@ -46,7 +46,7 @@ path = [ "key-provider-build/sgx_default_qcnl.conf", ] SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" [[annotations]] path = [ @@ -56,12 +56,12 @@ path = [ "**/.npmignore", ] SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" [[annotations]] path = "basefiles/*" SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" [[annotations]] path = [ @@ -94,14 +94,14 @@ path = [ "dstack-logo.svg", ] SPDX-FileCopyrightText = "Copyright (c) 2024-2025 The Project Contributors" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" # 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" +SPDX-License-Identifier = "BUSL-1.1" precedence = "override" # Vendor code @@ -109,19 +109,19 @@ precedence = "override" [[annotations]] path = "kms/auth-eth/lib/openzeppelin-contracts-upgradeable/**" SPDX-FileCopyrightText = "Copyright (c) 2016-2025 Zeppelin Group Ltd" -SPDX-License-Identifier = "MIT" +SPDX-License-Identifier = "BUSL-1.1" precedence = "override" [[annotations]] path = "kms/auth-eth/lib/openzeppelin-foundry-upgrades/**" SPDX-FileCopyrightText = "NONE" -SPDX-License-Identifier = "MIT" +SPDX-License-Identifier = "BUSL-1.1" precedence = "override" [[annotations]] path = "kms/auth-eth/lib/forge-std/**" SPDX-FileCopyrightText = "NONE" -SPDX-License-Identifier = "Apache-2.0" +SPDX-License-Identifier = "BUSL-1.1" precedence = "override" # Generated files diff --git a/basefiles/app-compose.sh b/basefiles/app-compose.sh index 0387ed348..417255658 100644 --- a/basefiles/app-compose.sh +++ b/basefiles/app-compose.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 HOST_SHARED_DIR="/dstack/.host-shared" SYS_CONFIG_FILE="$HOST_SHARED_DIR/.sys-config.json" diff --git a/basefiles/containerd.service.d/dstack-prepare.conf b/basefiles/containerd.service.d/dstack-prepare.conf index 6bbda8246..ca2bace02 100644 --- a/basefiles/containerd.service.d/dstack-prepare.conf +++ b/basefiles/containerd.service.d/dstack-prepare.conf @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [Unit] Wants=dstack-prepare.service diff --git a/basefiles/docker.service.d/dstack-guest-agent.conf b/basefiles/docker.service.d/dstack-guest-agent.conf index 2ca01dce3..778580f8d 100644 --- a/basefiles/docker.service.d/dstack-guest-agent.conf +++ b/basefiles/docker.service.d/dstack-guest-agent.conf @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [Unit] Wants=dstack-guest-agent.service diff --git a/basefiles/docker.service.d/dstack-prepare.conf b/basefiles/docker.service.d/dstack-prepare.conf index 6bbda8246..ca2bace02 100644 --- a/basefiles/docker.service.d/dstack-prepare.conf +++ b/basefiles/docker.service.d/dstack-prepare.conf @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [Unit] Wants=dstack-prepare.service diff --git a/basefiles/dstack-prepare.sh b/basefiles/dstack-prepare.sh index fc863a86b..061c2c887 100755 --- a/basefiles/dstack-prepare.sh +++ b/basefiles/dstack-prepare.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/basefiles/wg-checker.sh b/basefiles/wg-checker.sh index d97635280..dab16aa43 100755 --- a/basefiles/wg-checker.sh +++ b/basefiles/wg-checker.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 HANDSHAKE_TIMEOUT=180 LAST_REFRESH=0 diff --git a/cargo-check-all.sh b/cargo-check-all.sh index 11adab7b3..788993760 100755 --- a/cargo-check-all.sh +++ b/cargo-check-all.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 find . -name Cargo.toml -exec dirname {} \; | while read dir; do echo "Checking $dir..." diff --git a/cc-eventlog/Cargo.toml b/cc-eventlog/Cargo.toml index 2863760f7..7c42c2e61 100644 --- a/cc-eventlog/Cargo.toml +++ b/cc-eventlog/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "cc-eventlog" diff --git a/cc-eventlog/src/codecs.rs b/cc-eventlog/src/codecs.rs index c94edda3c..73c431478 100644 --- a/cc-eventlog/src/codecs.rs +++ b/cc-eventlog/src/codecs.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::ops::Deref; diff --git a/cc-eventlog/src/lib.rs b/cc-eventlog/src/lib.rs index 93bbc77ff..d53c5967f 100644 --- a/cc-eventlog/src/lib.rs +++ b/cc-eventlog/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 pub use runtime_events::{replay_events, RuntimeEvent}; pub use tdx::TdxEvent; diff --git a/cc-eventlog/src/runtime_events.rs b/cc-eventlog/src/runtime_events.rs index ace6cd970..fc6efae79 100644 --- a/cc-eventlog/src/runtime_events.rs +++ b/cc-eventlog/src/runtime_events.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use fs_err as fs; diff --git a/cc-eventlog/src/tcg.rs b/cc-eventlog/src/tcg.rs index ff0aadc32..9c40696e8 100644 --- a/cc-eventlog/src/tcg.rs +++ b/cc-eventlog/src/tcg.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::{codecs::VecOf, tdx::TdxEvent}; use anyhow::{Context, Result}; diff --git a/cc-eventlog/src/tdx.rs b/cc-eventlog/src/tdx.rs index bf7d677c0..65cc1154e 100644 --- a/cc-eventlog/src/tdx.rs +++ b/cc-eventlog/src/tdx.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Result; use scale::{Decode, Encode}; diff --git a/cc-eventlog/src/tpm.rs b/cc-eventlog/src/tpm.rs index 2d70bd01a..696263d3b 100644 --- a/cc-eventlog/src/tpm.rs +++ b/cc-eventlog/src/tpm.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM Event Log parsing (binary_bios_measurements format) diff --git a/cert-client/Cargo.toml b/cert-client/Cargo.toml index 089b927bf..e0f49fbf5 100644 --- a/cert-client/Cargo.toml +++ b/cert-client/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "cert-client" diff --git a/cert-client/src/lib.rs b/cert-client/src/lib.rs index 8328cd683..553bcfa41 100644 --- a/cert-client/src/lib.rs +++ b/cert-client/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use dstack_kms_rpc::{kms_client::KmsClient, SignCertRequest}; diff --git a/certbot/Cargo.toml b/certbot/Cargo.toml index 52c49bea0..94767b208 100644 --- a/certbot/Cargo.toml +++ b/certbot/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "certbot" diff --git a/certbot/cli/Cargo.toml b/certbot/cli/Cargo.toml index cd415c084..d3677fd40 100644 --- a/certbot/cli/Cargo.toml +++ b/certbot/cli/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "certbot-cli" diff --git a/certbot/cli/src/main.rs b/certbot/cli/src/main.rs index b22d32462..948a0c7bc 100644 --- a/certbot/cli/src/main.rs +++ b/certbot/cli/src/main.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // SPDX-FileCopyrightText: © 2025 Test in Prod // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{path::PathBuf, time::Duration}; diff --git a/certbot/src/acme_client.rs b/certbot/src/acme_client.rs index d4ebcf514..749f1f21e 100644 --- a/certbot/src/acme_client.rs +++ b/certbot/src/acme_client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Context, Result}; use fs_err as fs; diff --git a/certbot/src/acme_client/tests.rs b/certbot/src/acme_client/tests.rs index d77504a6d..4f2ac5e44 100644 --- a/certbot/src/acme_client/tests.rs +++ b/certbot/src/acme_client/tests.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use super::*; diff --git a/certbot/src/bot.rs b/certbot/src/bot.rs index 5a9c775fd..db2f8f63e 100644 --- a/certbot/src/bot.rs +++ b/certbot/src/bot.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{ collections::BTreeSet, diff --git a/certbot/src/bot/tests.rs b/certbot/src/bot/tests.rs index ce8b16f93..1591d74e2 100644 --- a/certbot/src/bot/tests.rs +++ b/certbot/src/bot/tests.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use instant_acme::LetsEncrypt; diff --git a/certbot/src/dns01_client.rs b/certbot/src/dns01_client.rs index 701d5ba9c..455c505a8 100644 --- a/certbot/src/dns01_client.rs +++ b/certbot/src/dns01_client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Result; use cloudflare::CloudflareClient; diff --git a/certbot/src/dns01_client/cloudflare.rs b/certbot/src/dns01_client/cloudflare.rs index 222028da9..caf085822 100644 --- a/certbot/src/dns01_client/cloudflare.rs +++ b/certbot/src/dns01_client/cloudflare.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::collections::HashMap; diff --git a/certbot/src/lib.rs b/certbot/src/lib.rs index 20cf8ed17..5951b2cf5 100644 --- a/certbot/src/lib.rs +++ b/certbot/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! A CertBot client for requesting certificates from Let's Encrypt. //! diff --git a/certbot/src/workdir.rs b/certbot/src/workdir.rs index 9265834d1..95c4dcb26 100644 --- a/certbot/src/workdir.rs +++ b/certbot/src/workdir.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Result; use fs_err as fs; diff --git a/cliff.toml b/cliff.toml index 167ef9f5b..dd09eaca9 100644 --- a/cliff.toml +++ b/cliff.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # git-cliff ~ configuration file # https://git-cliff.org/docs/configuration diff --git a/ct_monitor/Cargo.toml b/ct_monitor/Cargo.toml index 82c1dccdd..4f214d7fd 100644 --- a/ct_monitor/Cargo.toml +++ b/ct_monitor/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "ct_monitor" diff --git a/ct_monitor/src/main.rs b/ct_monitor/src/main.rs index bfa0565c8..721e5a392 100644 --- a/ct_monitor/src/main.rs +++ b/ct_monitor/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Context, Result}; use clap::Parser; diff --git a/dstack-attest/Cargo.toml b/dstack-attest/Cargo.toml index 9cfa26b44..f4ce8c312 100644 --- a/dstack-attest/Cargo.toml +++ b/dstack-attest/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-attest" diff --git a/dstack-attest/src/attestation.rs b/dstack-attest/src/attestation.rs index b27a5f506..aaccd758e 100644 --- a/dstack-attest/src/attestation.rs +++ b/dstack-attest/src/attestation.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Attestation functions diff --git a/dstack-attest/src/lib.rs b/dstack-attest/src/lib.rs index b8577d007..a854d0dbe 100644 --- a/dstack-attest/src/lib.rs +++ b/dstack-attest/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Context; use cc_eventlog::RuntimeEvent; diff --git a/dstack-attest/tests/nitro_verify.rs b/dstack-attest/tests/nitro_verify.rs index b5f8a07f1..075c3c660 100644 --- a/dstack-attest/tests/nitro_verify.rs +++ b/dstack-attest/tests/nitro_verify.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Integration test: verify Nitro Enclave attestation end-to-end diff --git a/dstack-mr/Cargo.toml b/dstack-mr/Cargo.toml index 12ca50091..b465795ba 100644 --- a/dstack-mr/Cargo.toml +++ b/dstack-mr/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-mr" diff --git a/dstack-mr/cli/Cargo.toml b/dstack-mr/cli/Cargo.toml index 336b0a791..ee819852d 100644 --- a/dstack-mr/cli/Cargo.toml +++ b/dstack-mr/cli/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-mr-cli" diff --git a/dstack-mr/cli/src/main.rs b/dstack-mr/cli/src/main.rs index c461a998c..83c68d983 100644 --- a/dstack-mr/cli/src/main.rs +++ b/dstack-mr/cli/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; diff --git a/dstack-mr/src/acpi.rs b/dstack-mr/src/acpi.rs index a39759e83..1d1cd66e1 100644 --- a/dstack-mr/src/acpi.rs +++ b/dstack-mr/src/acpi.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! This module provides functionality to generate ACPI tables for QEMU, //! translated from an original Go implementation. diff --git a/dstack-mr/src/kernel.rs b/dstack-mr/src/kernel.rs index 878a2b012..5f9f43e3a 100644 --- a/dstack-mr/src/kernel.rs +++ b/dstack-mr/src/kernel.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::{measure_sha384, num::read_le, utf16_encode}; use anyhow::{bail, Context, Result}; diff --git a/dstack-mr/src/lib.rs b/dstack-mr/src/lib.rs index a8d5825e5..144ce9dff 100644 --- a/dstack-mr/src/lib.rs +++ b/dstack-mr/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use serde::{Deserialize, Serialize}; use serde_human_bytes as hex_bytes; diff --git a/dstack-mr/src/machine.rs b/dstack-mr/src/machine.rs index b664d2c40..e6e526a11 100644 --- a/dstack-mr/src/machine.rs +++ b/dstack-mr/src/machine.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::acpi::Tables; use crate::tdvf::Tdvf; diff --git a/dstack-mr/src/num.rs b/dstack-mr/src/num.rs index 56a7bd797..1d16e1fcd 100644 --- a/dstack-mr/src/num.rs +++ b/dstack-mr/src/num.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; diff --git a/dstack-mr/src/tdvf.rs b/dstack-mr/src/tdvf.rs index b41dee76d..382a429af 100644 --- a/dstack-mr/src/tdvf.rs +++ b/dstack-mr/src/tdvf.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{anyhow, bail, Context, Result}; use hex_literal::hex; diff --git a/dstack-mr/src/util.rs b/dstack-mr/src/util.rs index 9b394944f..a40cc2ce9 100644 --- a/dstack-mr/src/util.rs +++ b/dstack-mr/src/util.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use log::debug; use sha2::{Digest, Sha384}; diff --git a/dstack-mr/tests/tdvf_parse.rs b/dstack-mr/tests/tdvf_parse.rs index 6c7e93827..692936a78 100644 --- a/dstack-mr/tests/tdvf_parse.rs +++ b/dstack-mr/tests/tdvf_parse.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Integration test to verify TDVF firmware parsing correctness //! diff --git a/dstack-types/Cargo.toml b/dstack-types/Cargo.toml index 997b0e43f..badce5d1f 100644 --- a/dstack-types/Cargo.toml +++ b/dstack-types/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-types" diff --git a/dstack-types/src/lib.rs b/dstack-types/src/lib.rs index 4af37cfc2..1a1e96851 100644 --- a/dstack-types/src/lib.rs +++ b/dstack-types/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::path::Path; diff --git a/dstack-types/src/mr_config.rs b/dstack-types/src/mr_config.rs index b4766ecbe..30a1b9976 100644 --- a/dstack-types/src/mr_config.rs +++ b/dstack-types/src/mr_config.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use sha3::{Digest, Keccak256}; diff --git a/dstack-types/src/shared_filenames.rs b/dstack-types/src/shared_filenames.rs index 5c3ef8282..3b9ed7893 100644 --- a/dstack-types/src/shared_filenames.rs +++ b/dstack-types/src/shared_filenames.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 pub const APP_COMPOSE: &str = "app-compose.json"; pub const APP_KEYS: &str = ".appkeys.json"; diff --git a/dstack-util/Cargo.toml b/dstack-util/Cargo.toml index 71bf295fe..5afdc21ee 100644 --- a/dstack-util/Cargo.toml +++ b/dstack-util/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-util" diff --git a/dstack-util/src/crypto.rs b/dstack-util/src/crypto.rs index ee1dc555f..12b848d00 100644 --- a/dstack-util/src/crypto.rs +++ b/dstack-util/src/crypto.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use aes_gcm::{ aead::{Aead, Nonce}, diff --git a/dstack-util/src/docker_compose.rs b/dstack-util/src/docker_compose.rs index b5f02eb78..b5d0f1e60 100644 --- a/dstack-util/src/docker_compose.rs +++ b/dstack-util/src/docker_compose.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use bollard::container::{ListContainersOptions, RemoveContainerOptions}; diff --git a/dstack-util/src/host_api.rs b/dstack-util/src/host_api.rs index 6bcc270a6..4150d9b29 100644 --- a/dstack-util/src/host_api.rs +++ b/dstack-util/src/host_api.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::utils::{deserialize_json_file, sha256, SysConfig}; use anyhow::{anyhow, bail, Context, Result}; diff --git a/dstack-util/src/main.rs b/dstack-util/src/main.rs index 88ef12cb2..6eaf91802 100644 --- a/dstack-util/src/main.rs +++ b/dstack-util/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; diff --git a/dstack-util/src/parse_env_file.rs b/dstack-util/src/parse_env_file.rs index 1c93dee62..db949d500 100644 --- a/dstack-util/src/parse_env_file.rs +++ b/dstack-util/src/parse_env_file.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Context, Result}; use serde::Deserialize; diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs index 748444a78..52307fee6 100644 --- a/dstack-util/src/system_setup.rs +++ b/dstack-util/src/system_setup.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{ collections::{BTreeMap, BTreeSet}, diff --git a/dstack-util/src/system_setup/config_id_verifier.rs b/dstack-util/src/system_setup/config_id_verifier.rs index c62f665c3..42dfee7a3 100644 --- a/dstack-util/src/system_setup/config_id_verifier.rs +++ b/dstack-util/src/system_setup/config_id_verifier.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Context, Result}; use dstack_types::{mr_config::MrConfig, KeyProviderKind}; diff --git a/dstack-util/src/utils.rs b/dstack-util/src/utils.rs index 510f93b25..974a33961 100644 --- a/dstack-util/src/utils.rs +++ b/dstack-util/src/utils.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::path::Path; diff --git a/dstack-util/tests/fixtures/key-provider-docker-compose.yaml b/dstack-util/tests/fixtures/key-provider-docker-compose.yaml index 25e7e4ef1..d163a23b2 100644 --- a/dstack-util/tests/fixtures/key-provider-docker-compose.yaml +++ b/dstack-util/tests/fixtures/key-provider-docker-compose.yaml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 x-common: &common-config restart: always diff --git a/dstack-util/tests/fixtures/luks_header_cipher_null.license b/dstack-util/tests/fixtures/luks_header_cipher_null.license index 8f861f09b..1b2524d9e 100644 --- a/dstack-util/tests/fixtures/luks_header_cipher_null.license +++ b/dstack-util/tests/fixtures/luks_header_cipher_null.license @@ -1,3 +1,3 @@ SPDX-FileCopyrightText: © 2025 Phala Network -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: BUSL-1.1 \ No newline at end of file diff --git a/dstack-util/tests/fixtures/luks_header_good.license b/dstack-util/tests/fixtures/luks_header_good.license index 8f861f09b..1b2524d9e 100644 --- a/dstack-util/tests/fixtures/luks_header_good.license +++ b/dstack-util/tests/fixtures/luks_header_good.license @@ -1,3 +1,3 @@ SPDX-FileCopyrightText: © 2025 Phala Network -SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +SPDX-License-Identifier: BUSL-1.1 \ No newline at end of file diff --git a/dstack-util/tests/test_remove_orphans.sh b/dstack-util/tests/test_remove_orphans.sh index 0400693b8..69794dd9d 100755 --- a/dstack-util/tests/test_remove_orphans.sh +++ b/dstack-util/tests/test_remove_orphans.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: © 2025 Phala Network -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # Test script for remove-orphans command (both online and offline modes) # Uses real docker compose to create containers for accurate testing diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 8a30c6da3..82102cde6 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-gateway" diff --git a/gateway/dstack-app/builder/Dockerfile b/gateway/dstack-app/builder/Dockerfile index 3f0076429..f7b6ba5b2 100644 --- a/gateway/dstack-app/builder/Dockerfile +++ b/gateway/dstack-app/builder/Dockerfile @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS gateway-builder COPY ./shared /build diff --git a/gateway/dstack-app/builder/build-image.sh b/gateway/dstack-app/builder/build-image.sh index 9c7a56c65..d2bfbbd01 100755 --- a/gateway/dstack-app/builder/build-image.sh +++ b/gateway/dstack-app/builder/build-image.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/gateway/dstack-app/builder/entrypoint.sh b/gateway/dstack-app/builder/entrypoint.sh index cd25da1f1..a2b92d660 100755 --- a/gateway/dstack-app/builder/entrypoint.sh +++ b/gateway/dstack-app/builder/entrypoint.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/gateway/dstack-app/builder/shared/pin-packages.sh b/gateway/dstack-app/builder/shared/pin-packages.sh index 3c750b1bb..e0f20386b 100755 --- a/gateway/dstack-app/builder/shared/pin-packages.sh +++ b/gateway/dstack-app/builder/shared/pin-packages.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e PKG_LIST=$1 diff --git a/gateway/dstack-app/deploy-to-vmm.sh b/gateway/dstack-app/deploy-to-vmm.sh index 2584d4503..d63d609ca 100755 --- a/gateway/dstack-app/deploy-to-vmm.sh +++ b/gateway/dstack-app/deploy-to-vmm.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 APP_COMPOSE_FILE="" diff --git a/gateway/dstack-app/docker-compose.yaml b/gateway/dstack-app/docker-compose.yaml index 6fdc1d8bb..fa6c0945c 100644 --- a/gateway/dstack-app/docker-compose.yaml +++ b/gateway/dstack-app/docker-compose.yaml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 services: gateway: diff --git a/gateway/gateway.toml b/gateway/gateway.toml index 78446b0e8..9bbf09835 100644 --- a/gateway/gateway.toml +++ b/gateway/gateway.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 workers = 8 max_blocking = 64 diff --git a/gateway/rpc/Cargo.toml b/gateway/rpc/Cargo.toml index 3a38ad25b..4802057bb 100644 --- a/gateway/rpc/Cargo.toml +++ b/gateway/rpc/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-gateway-rpc" diff --git a/gateway/rpc/build.rs b/gateway/rpc/build.rs index fe19530a5..5e0d5ddec 100644 --- a/gateway/rpc/build.rs +++ b/gateway/rpc/build.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #![allow(clippy::expect_used)] diff --git a/gateway/rpc/proto/gateway_rpc.proto b/gateway/rpc/proto/gateway_rpc.proto index 2c7aa5528..d41f6890d 100644 --- a/gateway/rpc/proto/gateway_rpc.proto +++ b/gateway/rpc/proto/gateway_rpc.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/gateway/rpc/src/lib.rs b/gateway/rpc/src/lib.rs index 089da7c60..a1218ef3a 100644 --- a/gateway/rpc/src/lib.rs +++ b/gateway/rpc/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 extern crate alloc; diff --git a/gateway/src/admin_service.rs b/gateway/src/admin_service.rs index 541dee0d0..5e4c211c3 100644 --- a/gateway/src/admin_service.rs +++ b/gateway/src/admin_service.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::sync::atomic::Ordering; use std::time::{SystemTime, UNIX_EPOCH}; diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 4809aef8b..634b13da7 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Context, Result}; use cmd_lib::run_cmd as cmd; diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 5d86e84f6..e18ceefe5 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{anyhow, Context, Result}; use clap::Parser; diff --git a/gateway/src/main_service.rs b/gateway/src/main_service.rs index e6bc67754..069f66dd0 100644 --- a/gateway/src/main_service.rs +++ b/gateway/src/main_service.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{ collections::{BTreeMap, BTreeSet}, diff --git a/gateway/src/main_service/auth_client.rs b/gateway/src/main_service/auth_client.rs index f729cf7fb..cd28fa8dc 100644 --- a/gateway/src/main_service/auth_client.rs +++ b/gateway/src/main_service/auth_client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::config::AuthConfig; use anyhow::{Context, Result}; diff --git a/gateway/src/main_service/sync_client.rs b/gateway/src/main_service/sync_client.rs index 7feba2a02..aaffab67c 100644 --- a/gateway/src/main_service/sync_client.rs +++ b/gateway/src/main_service/sync_client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::time::{Duration, Instant}; diff --git a/gateway/src/main_service/tests.rs b/gateway/src/main_service/tests.rs index d98c0131b..f7e15563f 100644 --- a/gateway/src/main_service/tests.rs +++ b/gateway/src/main_service/tests.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use super::*; use crate::config::{load_config_figment, Config}; diff --git a/gateway/src/models.rs b/gateway/src/models.rs index ec476cff0..0b0a0fdbe 100644 --- a/gateway/src/models.rs +++ b/gateway/src/models.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use dstack_gateway_rpc::{AcmeInfoResponse, StatusResponse}; use rinja::Template; diff --git a/gateway/src/proxy.rs b/gateway/src/proxy.rs index 73b947cca..671b58810 100644 --- a/gateway/src/proxy.rs +++ b/gateway/src/proxy.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{ net::Ipv4Addr, diff --git a/gateway/src/proxy/io_bridge.rs b/gateway/src/proxy/io_bridge.rs index 8c5dd7662..b396671a6 100644 --- a/gateway/src/proxy/io_bridge.rs +++ b/gateway/src/proxy/io_bridge.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::config::ProxyConfig; use anyhow::{Context, Result}; diff --git a/gateway/src/proxy/sni.rs b/gateway/src/proxy/sni.rs index 4901cc45f..ec7594111 100644 --- a/gateway/src/proxy/sni.rs +++ b/gateway/src/proxy/sni.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use parcelona::parser_combinators::{Msg, PErr}; use parcelona::u8::*; diff --git a/gateway/src/proxy/tls_passthough.rs b/gateway/src/proxy/tls_passthough.rs index e2cea9d08..ee72de4da 100644 --- a/gateway/src/proxy/tls_passthough.rs +++ b/gateway/src/proxy/tls_passthough.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use std::fmt::Debug; diff --git a/gateway/src/proxy/tls_terminate.rs b/gateway/src/proxy/tls_terminate.rs index ad19ebf4c..151e456ea 100644 --- a/gateway/src/proxy/tls_terminate.rs +++ b/gateway/src/proxy/tls_terminate.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::io; use std::pin::Pin; diff --git a/gateway/src/web_routes.rs b/gateway/src/web_routes.rs index 1bd57f2b6..9fec788ce 100644 --- a/gateway/src/web_routes.rs +++ b/gateway/src/web_routes.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::main_service::Proxy; use anyhow::Result; diff --git a/gateway/src/web_routes/route_index.rs b/gateway/src/web_routes/route_index.rs index db414391f..eb044dff5 100644 --- a/gateway/src/web_routes/route_index.rs +++ b/gateway/src/web_routes/route_index.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::{ admin_service::AdminRpcHandler, diff --git a/gateway/templates/dashboard.html b/gateway/templates/dashboard.html index 56750204d..b7ac91778 100644 --- a/gateway/templates/dashboard.html +++ b/gateway/templates/dashboard.html @@ -1,7 +1,7 @@ diff --git a/gateway/templates/rproxy.yaml b/gateway/templates/rproxy.yaml index 69d03792b..788978af1 100644 --- a/gateway/templates/rproxy.yaml +++ b/gateway/templates/rproxy.yaml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 servers: {%- for p in portmap %} diff --git a/guest-agent/Cargo.toml b/guest-agent/Cargo.toml index fe8813bab..0d09b2435 100644 --- a/guest-agent/Cargo.toml +++ b/guest-agent/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-guest-agent" diff --git a/guest-agent/dstack.toml b/guest-agent/dstack.toml index 63b71bc9f..0c94d07ca 100644 --- a/guest-agent/dstack.toml +++ b/guest-agent/dstack.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [default] workers = 8 diff --git a/guest-agent/rpc/Cargo.toml b/guest-agent/rpc/Cargo.toml index a7e573c64..e0dc9fe36 100644 --- a/guest-agent/rpc/Cargo.toml +++ b/guest-agent/rpc/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-guest-agent-rpc" diff --git a/guest-agent/rpc/build.rs b/guest-agent/rpc/build.rs index fe19530a5..5e0d5ddec 100644 --- a/guest-agent/rpc/build.rs +++ b/guest-agent/rpc/build.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #![allow(clippy::expect_used)] diff --git a/guest-agent/rpc/proto/agent_rpc.proto b/guest-agent/rpc/proto/agent_rpc.proto index f12b161de..b18406fd1 100644 --- a/guest-agent/rpc/proto/agent_rpc.proto +++ b/guest-agent/rpc/proto/agent_rpc.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/guest-agent/rpc/src/lib.rs b/guest-agent/rpc/src/lib.rs index 089da7c60..a1218ef3a 100644 --- a/guest-agent/rpc/src/lib.rs +++ b/guest-agent/rpc/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 extern crate alloc; diff --git a/guest-agent/src/config.rs b/guest-agent/src/config.rs index 6276a4daa..e6f2c95dd 100644 --- a/guest-agent/src/config.rs +++ b/guest-agent/src/config.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{collections::HashSet, ops::Deref, path::PathBuf}; diff --git a/guest-agent/src/guest_api_service.rs b/guest-agent/src/guest_api_service.rs index 2f905778e..f3ae9bb9c 100644 --- a/guest-agent/src/guest_api_service.rs +++ b/guest-agent/src/guest_api_service.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::fmt::Debug; diff --git a/guest-agent/src/http_routes.rs b/guest-agent/src/http_routes.rs index c8fa44df2..e3da62465 100644 --- a/guest-agent/src/http_routes.rs +++ b/guest-agent/src/http_routes.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::time::Duration; diff --git a/guest-agent/src/main.rs b/guest-agent/src/main.rs index 3183e61f9..03aa4de6c 100644 --- a/guest-agent/src/main.rs +++ b/guest-agent/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{fs::Permissions, future::pending, os::unix::fs::PermissionsExt}; diff --git a/guest-agent/src/models.rs b/guest-agent/src/models.rs index a50cf340d..4c3f3e016 100644 --- a/guest-agent/src/models.rs +++ b/guest-agent/src/models.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use guest_api::{Container, SystemInfo}; use rinja::Template; diff --git a/guest-agent/src/rpc_service.rs b/guest-agent/src/rpc_service.rs index 03967547c..db9608d11 100644 --- a/guest-agent/src/rpc_service.rs +++ b/guest-agent/src/rpc_service.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::sync::{Arc, RwLock}; diff --git a/guest-agent/templates/dashboard.html b/guest-agent/templates/dashboard.html index 212df06ac..78bf3dbd9 100644 --- a/guest-agent/templates/dashboard.html +++ b/guest-agent/templates/dashboard.html @@ -1,7 +1,7 @@ diff --git a/guest-api/Cargo.toml b/guest-api/Cargo.toml index 01b6c8909..a76f80891 100644 --- a/guest-api/Cargo.toml +++ b/guest-api/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "guest-api" diff --git a/guest-api/build.rs b/guest-api/build.rs index dc3e9d962..55a1b89a6 100644 --- a/guest-api/build.rs +++ b/guest-api/build.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #![allow(clippy::expect_used)] diff --git a/guest-api/proto/guest_api.proto b/guest-api/proto/guest_api.proto index 5d5868cb9..12efb917c 100644 --- a/guest-api/proto/guest_api.proto +++ b/guest-api/proto/guest_api.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/guest-api/src/client.rs b/guest-api/src/client.rs index 68fa36e44..64316f7d0 100644 --- a/guest-api/src/client.rs +++ b/guest-api/src/client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::guest_api_client::GuestApiClient; use http_client::prpc::PrpcClient; diff --git a/guest-api/src/lib.rs b/guest-api/src/lib.rs index eb5524134..b63a6b1df 100644 --- a/guest-api/src/lib.rs +++ b/guest-api/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 extern crate alloc; diff --git a/host-api/Cargo.toml b/host-api/Cargo.toml index 8c43a217f..32c3f7b9a 100644 --- a/host-api/Cargo.toml +++ b/host-api/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "host-api" diff --git a/host-api/build.rs b/host-api/build.rs index dc3e9d962..55a1b89a6 100644 --- a/host-api/build.rs +++ b/host-api/build.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #![allow(clippy::expect_used)] diff --git a/host-api/proto/host_api.proto b/host-api/proto/host_api.proto index f8864f516..3c9e3e383 100644 --- a/host-api/proto/host_api.proto +++ b/host-api/proto/host_api.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/host-api/src/client.rs b/host-api/src/client.rs index 5a0606009..4f0dfc9cd 100644 --- a/host-api/src/client.rs +++ b/host-api/src/client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::host_api_client::HostApiClient; use http_client::prpc::PrpcClient; diff --git a/host-api/src/lib.rs b/host-api/src/lib.rs index eb5524134..b63a6b1df 100644 --- a/host-api/src/lib.rs +++ b/host-api/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 extern crate alloc; diff --git a/http-client/Cargo.toml b/http-client/Cargo.toml index fffe277ab..a8c32181c 100644 --- a/http-client/Cargo.toml +++ b/http-client/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "http-client" diff --git a/http-client/src/hyper_vsock.rs b/http-client/src/hyper_vsock.rs index df280f80b..d5fc641a2 100644 --- a/http-client/src/hyper_vsock.rs +++ b/http-client/src/hyper_vsock.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use hyper::{body::Body, rt::ReadBufCursor, Uri}; use hyper_util::{ diff --git a/http-client/src/lib.rs b/http-client/src/lib.rs index ee5c7f9f9..6f0014be7 100644 --- a/http-client/src/lib.rs +++ b/http-client/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Result; use http_body_util::{BodyExt, Full}; diff --git a/http-client/src/prpc.rs b/http-client/src/prpc.rs index eacc03b1e..54611217e 100644 --- a/http-client/src/prpc.rs +++ b/http-client/src/prpc.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Context; use prpc::{ diff --git a/iohash/Cargo.toml b/iohash/Cargo.toml index 2e7ac0e06..0bd6245df 100644 --- a/iohash/Cargo.toml +++ b/iohash/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "iohash" diff --git a/iohash/src/main.rs b/iohash/src/main.rs index 2bb32ce2d..5a5d83c7d 100644 --- a/iohash/src/main.rs +++ b/iohash/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Stream compute hash digest of stdin and output the original data to stdout. The digest is output to stderr. use std::io::{Read, Write}; diff --git a/key-provider-build/Dockerfile.aesmd b/key-provider-build/Dockerfile.aesmd index c5d032507..bb00ceee9 100644 --- a/key-provider-build/Dockerfile.aesmd +++ b/key-provider-build/Dockerfile.aesmd @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 FROM ubuntu:20.04 # Prevent timezone prompt diff --git a/key-provider-build/Dockerfile.key-provider b/key-provider-build/Dockerfile.key-provider index 5929d156f..b7476a19f 100644 --- a/key-provider-build/Dockerfile.key-provider +++ b/key-provider-build/Dockerfile.key-provider @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 FROM gramineproject/gramine:v1.5@sha256:615849089db84477f03cd13209c08eaf6b6a3a68b4df733e097db56781935589 RUN apt-get update && apt-get install -y \ diff --git a/key-provider-build/docker-compose.yaml b/key-provider-build/docker-compose.yaml index 25e7e4ef1..d163a23b2 100644 --- a/key-provider-build/docker-compose.yaml +++ b/key-provider-build/docker-compose.yaml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 x-common: &common-config restart: always diff --git a/key-provider-build/entrypoint-aesmd.sh b/key-provider-build/entrypoint-aesmd.sh index 020591aac..46393831d 100755 --- a/key-provider-build/entrypoint-aesmd.sh +++ b/key-provider-build/entrypoint-aesmd.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 echo "Starting AESM service..." # Make sure the AESM directory exists with proper permissions diff --git a/key-provider-build/entrypoint-key-provider.sh b/key-provider-build/entrypoint-key-provider.sh index 893e4686b..3017f8f3f 100755 --- a/key-provider-build/entrypoint-key-provider.sh +++ b/key-provider-build/entrypoint-key-provider.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 echo "Waiting for AESM socket to be available..." AESM_SOCKET="/var/run/aesmd/aesm.socket" diff --git a/key-provider-build/run.sh b/key-provider-build/run.sh index 42624b8e8..e9ccb447b 100755 --- a/key-provider-build/run.sh +++ b/key-provider-build/run.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 echo "Starting all SGX services using docker-compose..." docker compose up --build -d diff --git a/key-provider-client/Cargo.toml b/key-provider-client/Cargo.toml index 4074fa39c..e165cbeee 100644 --- a/key-provider-client/Cargo.toml +++ b/key-provider-client/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "key-provider-client" diff --git a/key-provider-client/src/host.rs b/key-provider-client/src/host.rs index e6ade400d..49f725106 100644 --- a/key-provider-client/src/host.rs +++ b/key-provider-client/src/host.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; diff --git a/key-provider-client/src/lib.rs b/key-provider-client/src/lib.rs index ada18a6e8..a24195157 100644 --- a/key-provider-client/src/lib.rs +++ b/key-provider-client/src/lib.rs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 pub mod host; diff --git a/kms/Cargo.toml b/kms/Cargo.toml index bc33bc6a7..a29299241 100644 --- a/kms/Cargo.toml +++ b/kms/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-kms" diff --git a/kms/auth-eth-bun/index.test.ts b/kms/auth-eth-bun/index.test.ts index 4bc271b8b..9b3cd7183 100644 --- a/kms/auth-eth-bun/index.test.ts +++ b/kms/auth-eth-bun/index.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; import openApiSpec from './openapi.json'; diff --git a/kms/auth-eth-bun/index.ts b/kms/auth-eth-bun/index.ts index 2432d3d13..7c6fd71d7 100644 --- a/kms/auth-eth-bun/index.ts +++ b/kms/auth-eth-bun/index.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; diff --git a/kms/auth-eth-bun/vitest.config.ts b/kms/auth-eth-bun/vitest.config.ts index 990992495..df38dc19e 100644 --- a/kms/auth-eth-bun/vitest.config.ts +++ b/kms/auth-eth-bun/vitest.config.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { defineConfig } from 'vitest/config'; diff --git a/kms/auth-eth/contracts/DstackApp.sol b/kms/auth-eth/contracts/DstackApp.sol index a0093b0a9..02142dcff 100644 --- a/kms/auth-eth/contracts/DstackApp.sol +++ b/kms/auth-eth/contracts/DstackApp.sol @@ -1,7 +1,7 @@ /* * SPDX-FileCopyrightText: © 2025 Phala Network * - * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: BUSL-1.1 */ pragma solidity ^0.8.22; diff --git a/kms/auth-eth/contracts/DstackKms.sol b/kms/auth-eth/contracts/DstackKms.sol index 9a26ef4ff..c5bc77fb9 100644 --- a/kms/auth-eth/contracts/DstackKms.sol +++ b/kms/auth-eth/contracts/DstackKms.sol @@ -1,7 +1,7 @@ /* * SPDX-FileCopyrightText: © 2025 Phala Network * - * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: BUSL-1.1 */ pragma solidity ^0.8.22; diff --git a/kms/auth-eth/contracts/IAppAuth.sol b/kms/auth-eth/contracts/IAppAuth.sol index 29eb6737a..19e07d4af 100644 --- a/kms/auth-eth/contracts/IAppAuth.sol +++ b/kms/auth-eth/contracts/IAppAuth.sol @@ -1,7 +1,7 @@ /* * SPDX-FileCopyrightText: © 2025 Phala Network * - * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: BUSL-1.1 */ pragma solidity ^0.8.0; diff --git a/kms/auth-eth/contracts/IAppAuthBasicManagement.sol b/kms/auth-eth/contracts/IAppAuthBasicManagement.sol index 0396b1d47..fdcaaacf3 100644 --- a/kms/auth-eth/contracts/IAppAuthBasicManagement.sol +++ b/kms/auth-eth/contracts/IAppAuthBasicManagement.sol @@ -1,7 +1,7 @@ /* * SPDX-FileCopyrightText: © 2025 Phala Network * - * SPDX-License-Identifier: Apache-2.0 + * SPDX-License-Identifier: BUSL-1.1 */ pragma solidity ^0.8.0; diff --git a/kms/auth-eth/hardhat.config.ts b/kms/auth-eth/hardhat.config.ts index dd7c660c5..2e3fe80c1 100644 --- a/kms/auth-eth/hardhat.config.ts +++ b/kms/auth-eth/hardhat.config.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import "@openzeppelin/hardhat-upgrades"; import { HardhatUserConfig, task, types } from "hardhat/config"; diff --git a/kms/auth-eth/jest.config.js b/kms/auth-eth/jest.config.js index ae7a06b74..791505b15 100644 --- a/kms/auth-eth/jest.config.js +++ b/kms/auth-eth/jest.config.js @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 module.exports = { preset: 'ts-jest', diff --git a/kms/auth-eth/jest.integration.config.js b/kms/auth-eth/jest.integration.config.js index f6cc831be..17c3ba2de 100644 --- a/kms/auth-eth/jest.integration.config.js +++ b/kms/auth-eth/jest.integration.config.js @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { diff --git a/kms/auth-eth/lib/deployment-helpers.ts b/kms/auth-eth/lib/deployment-helpers.ts index dbcb2b66f..5472ec097 100644 --- a/kms/auth-eth/lib/deployment-helpers.ts +++ b/kms/auth-eth/lib/deployment-helpers.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { HardhatRuntimeEnvironment } from "hardhat/types"; import * as readline from 'readline'; diff --git a/kms/auth-eth/run-tests.sh b/kms/auth-eth/run-tests.sh index e8dd6ea0f..ddb2b333e 100644 --- a/kms/auth-eth/run-tests.sh +++ b/kms/auth-eth/run-tests.sh @@ -1,5 +1,5 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 npm run test diff --git a/kms/auth-eth/scripts/deploy.ts b/kms/auth-eth/scripts/deploy.ts index 95b5587ac..888ee8300 100644 --- a/kms/auth-eth/scripts/deploy.ts +++ b/kms/auth-eth/scripts/deploy.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { HardhatRuntimeEnvironment } from "hardhat/types"; import * as helpers from "../lib/deployment-helpers"; diff --git a/kms/auth-eth/scripts/upgrade.ts b/kms/auth-eth/scripts/upgrade.ts index 01f252314..206fcc097 100644 --- a/kms/auth-eth/scripts/upgrade.ts +++ b/kms/auth-eth/scripts/upgrade.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { HardhatRuntimeEnvironment } from "hardhat/types"; import * as helpers from "../lib/deployment-helpers"; diff --git a/kms/auth-eth/scripts/verify.ts b/kms/auth-eth/scripts/verify.ts index 73904db4e..47c717256 100644 --- a/kms/auth-eth/scripts/verify.ts +++ b/kms/auth-eth/scripts/verify.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { run } from "hardhat"; diff --git a/kms/auth-eth/src/ethereum.ts b/kms/auth-eth/src/ethereum.ts index 5b1385d98..50f24f994 100644 --- a/kms/auth-eth/src/ethereum.ts +++ b/kms/auth-eth/src/ethereum.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { ethers } from 'ethers'; import { BootInfo, BootResponse } from './types'; diff --git a/kms/auth-eth/src/main.ts b/kms/auth-eth/src/main.ts index cdbf2e7c5..f634a9f62 100644 --- a/kms/auth-eth/src/main.ts +++ b/kms/auth-eth/src/main.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { config } from 'dotenv'; import { build } from './server'; diff --git a/kms/auth-eth/src/server.ts b/kms/auth-eth/src/server.ts index 16f8cbc87..2891ebfae 100644 --- a/kms/auth-eth/src/server.ts +++ b/kms/auth-eth/src/server.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import fastify, { FastifyInstance } from 'fastify'; import { EthereumBackend } from './ethereum'; diff --git a/kms/auth-eth/src/types.ts b/kms/auth-eth/src/types.ts index fd1a89143..16715914b 100644 --- a/kms/auth-eth/src/types.ts +++ b/kms/auth-eth/src/types.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 export interface BootInfo { tcbStatus: string; diff --git a/kms/auth-eth/test/DstackApp.test.ts b/kms/auth-eth/test/DstackApp.test.ts index 6421402e3..830a552c9 100644 --- a/kms/auth-eth/test/DstackApp.test.ts +++ b/kms/auth-eth/test/DstackApp.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { expect } from "chai"; import { ethers } from "hardhat"; diff --git a/kms/auth-eth/test/ethereum.integration.test.ts b/kms/auth-eth/test/ethereum.integration.test.ts index 51a0783b0..2b81da359 100644 --- a/kms/auth-eth/test/ethereum.integration.test.ts +++ b/kms/auth-eth/test/ethereum.integration.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { ethers } from "hardhat"; diff --git a/kms/auth-eth/test/ethereum.test.ts b/kms/auth-eth/test/ethereum.test.ts index 83c0561f3..17e4d268d 100644 --- a/kms/auth-eth/test/ethereum.test.ts +++ b/kms/auth-eth/test/ethereum.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { ethers } from "hardhat"; diff --git a/kms/auth-eth/test/main.test.ts b/kms/auth-eth/test/main.test.ts index 799b50302..bf03e41ef 100644 --- a/kms/auth-eth/test/main.test.ts +++ b/kms/auth-eth/test/main.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { FastifyInstance } from 'fastify'; import { build } from '../src/server'; diff --git a/kms/auth-eth/test/setup.ts b/kms/auth-eth/test/setup.ts index 9e5547b2a..8af8ae725 100644 --- a/kms/auth-eth/test/setup.ts +++ b/kms/auth-eth/test/setup.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import hre from "hardhat"; diff --git a/kms/auth-mock/index.test.ts b/kms/auth-mock/index.test.ts index 177fa1b44..f34194bca 100644 --- a/kms/auth-mock/index.test.ts +++ b/kms/auth-mock/index.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; import openApiSpec from './openapi.json'; diff --git a/kms/auth-mock/index.ts b/kms/auth-mock/index.ts index cef8becb5..15c9ce8b9 100644 --- a/kms/auth-mock/index.ts +++ b/kms/auth-mock/index.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; diff --git a/kms/auth-mock/vitest.config.ts b/kms/auth-mock/vitest.config.ts index 990992495..df38dc19e 100644 --- a/kms/auth-mock/vitest.config.ts +++ b/kms/auth-mock/vitest.config.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { defineConfig } from 'vitest/config'; diff --git a/kms/auth-simple/index.test.ts b/kms/auth-simple/index.test.ts index cd931431e..2b9e5a390 100644 --- a/kms/auth-simple/index.test.ts +++ b/kms/auth-simple/index.test.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { writeFileSync, unlinkSync, existsSync } from 'fs'; diff --git a/kms/auth-simple/index.ts b/kms/auth-simple/index.ts index bda04b110..005fd7906 100644 --- a/kms/auth-simple/index.ts +++ b/kms/auth-simple/index.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; diff --git a/kms/auth-simple/vitest.config.ts b/kms/auth-simple/vitest.config.ts index 17597daca..24cec8536 100644 --- a/kms/auth-simple/vitest.config.ts +++ b/kms/auth-simple/vitest.config.ts @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 import { defineConfig } from 'vitest/config'; diff --git a/kms/dstack-app/builder/Dockerfile b/kms/dstack-app/builder/Dockerfile index d62015359..29ee66ed5 100644 --- a/kms/dstack-app/builder/Dockerfile +++ b/kms/dstack-app/builder/Dockerfile @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS kms-builder COPY ./shared /build diff --git a/kms/dstack-app/builder/build-image.sh b/kms/dstack-app/builder/build-image.sh index 7290cd204..1b2645740 100755 --- a/kms/dstack-app/builder/build-image.sh +++ b/kms/dstack-app/builder/build-image.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/kms/dstack-app/builder/shared/config-qemu.sh b/kms/dstack-app/builder/shared/config-qemu.sh index 94174a585..651fa7606 100755 --- a/kms/dstack-app/builder/shared/config-qemu.sh +++ b/kms/dstack-app/builder/shared/config-qemu.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 BUILD_DIR="$1" PREFIX="$2" diff --git a/kms/dstack-app/builder/shared/pin-packages.sh b/kms/dstack-app/builder/shared/pin-packages.sh index 3c750b1bb..e0f20386b 100755 --- a/kms/dstack-app/builder/shared/pin-packages.sh +++ b/kms/dstack-app/builder/shared/pin-packages.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e PKG_LIST=$1 diff --git a/kms/dstack-app/compose-dev.yaml b/kms/dstack-app/compose-dev.yaml index aacb0e817..04a4b4b95 100644 --- a/kms/dstack-app/compose-dev.yaml +++ b/kms/dstack-app/compose-dev.yaml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # SPDX-FileCopyrightText: © 2025 Test in Prod # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 services: auth-api: diff --git a/kms/dstack-app/compose-simple.yaml b/kms/dstack-app/compose-simple.yaml index f40e58d36..98b2bdde1 100644 --- a/kms/dstack-app/compose-simple.yaml +++ b/kms/dstack-app/compose-simple.yaml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # KMS deployment with external auth-simple webhook # auth-simple runs outside the CVM (operator infrastructure) diff --git a/kms/dstack-app/deploy-simple.sh b/kms/dstack-app/deploy-simple.sh index f027f24aa..4c86b515f 100755 --- a/kms/dstack-app/deploy-simple.sh +++ b/kms/dstack-app/deploy-simple.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # Deploy KMS with external auth-simple webhook # auth-simple runs outside the CVM (operator infrastructure) diff --git a/kms/dstack-app/deploy-to-vmm.sh b/kms/dstack-app/deploy-to-vmm.sh index b8f6aeeeb..d96de743a 100755 --- a/kms/dstack-app/deploy-to-vmm.sh +++ b/kms/dstack-app/deploy-to-vmm.sh @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # SPDX-FileCopyrightText: © 2025 Test in Prod # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # Check if .env exists if [ -f ".env" ]; then diff --git a/kms/dstack-app/docker-compose.yaml b/kms/dstack-app/docker-compose.yaml index 3d8a18f5d..18a26e1f7 100644 --- a/kms/dstack-app/docker-compose.yaml +++ b/kms/dstack-app/docker-compose.yaml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 services: # Helios is a Ethereum light client diff --git a/kms/dstack-app/entrypoint.sh b/kms/dstack-app/entrypoint.sh index 781c31fa5..93562160b 100755 --- a/kms/dstack-app/entrypoint.sh +++ b/kms/dstack-app/entrypoint.sh @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # SPDX-FileCopyrightText: © 2025 Test in Prod # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/kms/kms.toml b/kms/kms.toml index 1f354066e..0e65b96b2 100644 --- a/kms/kms.toml +++ b/kms/kms.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [default] workers = 8 diff --git a/kms/rpc/Cargo.toml b/kms/rpc/Cargo.toml index 79a9b3478..d369d4dfe 100644 --- a/kms/rpc/Cargo.toml +++ b/kms/rpc/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-kms-rpc" diff --git a/kms/rpc/build.rs b/kms/rpc/build.rs index fe19530a5..5e0d5ddec 100644 --- a/kms/rpc/build.rs +++ b/kms/rpc/build.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #![allow(clippy::expect_used)] diff --git a/kms/rpc/proto/kms_rpc.proto b/kms/rpc/proto/kms_rpc.proto index 008151218..e89502574 100644 --- a/kms/rpc/proto/kms_rpc.proto +++ b/kms/rpc/proto/kms_rpc.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/kms/rpc/src/lib.rs b/kms/rpc/src/lib.rs index 089da7c60..a1218ef3a 100644 --- a/kms/rpc/src/lib.rs +++ b/kms/rpc/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 extern crate alloc; diff --git a/kms/src/config.rs b/kms/src/config.rs index 36874e1b9..e9b502ada 100644 --- a/kms/src/config.rs +++ b/kms/src/config.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use load_config::load_config; use rocket::figment::Figment; diff --git a/kms/src/crypto.rs b/kms/src/crypto.rs index 6400d7001..c3787cf21 100644 --- a/kms/src/crypto.rs +++ b/kms/src/crypto.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use k256::ecdsa::SigningKey; diff --git a/kms/src/ct_log.rs b/kms/src/ct_log.rs index de5c4b6bd..24e506795 100644 --- a/kms/src/ct_log.rs +++ b/kms/src/ct_log.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::path::{Path, PathBuf}; diff --git a/kms/src/main.rs b/kms/src/main.rs index 8584eec99..3c773acd5 100644 --- a/kms/src/main.rs +++ b/kms/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{anyhow, bail, Context, Result}; use clap::Parser; diff --git a/kms/src/main_service.rs b/kms/src/main_service.rs index 57fead253..f005cbb02 100644 --- a/kms/src/main_service.rs +++ b/kms/src/main_service.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{path::PathBuf, sync::Arc}; diff --git a/kms/src/main_service/upgrade_authority.rs b/kms/src/main_service/upgrade_authority.rs index 169fd495a..d37694a9b 100644 --- a/kms/src/main_service/upgrade_authority.rs +++ b/kms/src/main_service/upgrade_authority.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::config::AuthApi; use anyhow::{bail, Result}; diff --git a/kms/src/onboard_service.rs b/kms/src/onboard_service.rs index 4a4107fd5..5106609bd 100644 --- a/kms/src/onboard_service.rs +++ b/kms/src/onboard_service.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{Context, Result}; use dstack_guest_agent_rpc::{ diff --git a/kms/src/www/onboard.html b/kms/src/www/onboard.html index 7904b84dc..4d2a95afc 100644 --- a/kms/src/www/onboard.html +++ b/kms/src/www/onboard.html @@ -1,7 +1,7 @@ diff --git a/load_config/Cargo.toml b/load_config/Cargo.toml index 4ff4b140b..1a946d207 100644 --- a/load_config/Cargo.toml +++ b/load_config/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "load_config" diff --git a/load_config/src/lib.rs b/load_config/src/lib.rs index cd99b6cf7..25d2a6dc5 100644 --- a/load_config/src/lib.rs +++ b/load_config/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use figment::{ providers::{Data, Format, Json, Toml}, diff --git a/lspci/Cargo.toml b/lspci/Cargo.toml index bbb6f35cd..beea726d1 100644 --- a/lspci/Cargo.toml +++ b/lspci/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "lspci" diff --git a/lspci/src/lib.rs b/lspci/src/lib.rs index ef3565ede..44a9af86d 100644 --- a/lspci/src/lib.rs +++ b/lspci/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::process::Command; diff --git a/no_std_check/Cargo.toml b/no_std_check/Cargo.toml index 4a78beb80..6eee62cb3 100644 --- a/no_std_check/Cargo.toml +++ b/no_std_check/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "no_std_check" diff --git a/no_std_check/src/lib.rs b/no_std_check/src/lib.rs index 0f2f3803f..a7b1a3b63 100644 --- a/no_std_check/src/lib.rs +++ b/no_std_check/src/lib.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 // Import the types we want to test use dstack_sdk_types as _; diff --git a/nsm-attest/Cargo.toml b/nsm-attest/Cargo.toml index 09d3b3e4e..fb79bda43 100644 --- a/nsm-attest/Cargo.toml +++ b/nsm-attest/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "nsm-attest" diff --git a/nsm-attest/src/lib.rs b/nsm-attest/src/lib.rs index faf176459..2359d45dc 100644 --- a/nsm-attest/src/lib.rs +++ b/nsm-attest/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! AWS Nitro Enclave NSM (Nitro Security Module) Attestation Library //! diff --git a/nsm-attest/src/types.rs b/nsm-attest/src/types.rs index e33ab9be2..c0a6d743d 100644 --- a/nsm-attest/src/types.rs +++ b/nsm-attest/src/types.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! NSM types for attestation document parsing diff --git a/nsm-attest/tests/attestation_test.rs b/nsm-attest/tests/attestation_test.rs index 3e556855c..8dc692089 100644 --- a/nsm-attest/tests/attestation_test.rs +++ b/nsm-attest/tests/attestation_test.rs @@ -1,5 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 The Project Contributors -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 + // Test for NSM attestation document parsing use nsm_attest::AttestationDocument; diff --git a/nsm-qvl/Cargo.toml b/nsm-qvl/Cargo.toml index 007a1f613..efacc9fd5 100644 --- a/nsm-qvl/Cargo.toml +++ b/nsm-qvl/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "nsm-qvl" diff --git a/nsm-qvl/src/collateral.rs b/nsm-qvl/src/collateral.rs index a12d73563..f9b2ca4e1 100644 --- a/nsm-qvl/src/collateral.rs +++ b/nsm-qvl/src/collateral.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Collateral retrieval module //! diff --git a/nsm-qvl/src/lib.rs b/nsm-qvl/src/lib.rs index fd6290460..27ae8c3be 100644 --- a/nsm-qvl/src/lib.rs +++ b/nsm-qvl/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! AWS Nitro Enclave NSM Quote Verification Library (QVL) //! diff --git a/nsm-qvl/src/verify.rs b/nsm-qvl/src/verify.rs index 3844e754e..c4fb7f825 100644 --- a/nsm-qvl/src/verify.rs +++ b/nsm-qvl/src/verify.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! NSM Attestation Verification Module diff --git a/nsm-qvl/tests/verify_test.rs b/nsm-qvl/tests/verify_test.rs index cd9e766f1..0f232f608 100644 --- a/nsm-qvl/tests/verify_test.rs +++ b/nsm-qvl/tests/verify_test.rs @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: Copyright (c) 2024-2025 The Project Contributors -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 // Test for NSM attestation verification use nsm_qvl::{AttestationDocument, CoseSign1}; use std::io::Cursor; diff --git a/python/ct_monitor/ct_monitor.py b/python/ct_monitor/ct_monitor.py index 756636354..bab06db1f 100644 --- a/python/ct_monitor/ct_monitor.py +++ b/python/ct_monitor/ct_monitor.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 import sys import time diff --git a/python/ct_monitor/pyproject.toml b/python/ct_monitor/pyproject.toml index 0c201bec9..88361b972 100644 --- a/python/ct_monitor/pyproject.toml +++ b/python/ct_monitor/pyproject.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [project] name = "ct_monitor" diff --git a/ra-rpc/Cargo.toml b/ra-rpc/Cargo.toml index 2ec2b636b..4c9fb8735 100644 --- a/ra-rpc/Cargo.toml +++ b/ra-rpc/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "ra-rpc" diff --git a/ra-rpc/src/client.rs b/ra-rpc/src/client.rs index 1f68ad831..b66bad42c 100644 --- a/ra-rpc/src/client.rs +++ b/ra-rpc/src/client.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::time::Duration; diff --git a/ra-rpc/src/lib.rs b/ra-rpc/src/lib.rs index 43a5e257c..b01e2689f 100644 --- a/ra-rpc/src/lib.rs +++ b/ra-rpc/src/lib.rs @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{net::SocketAddr, path::PathBuf}; diff --git a/ra-rpc/src/openapi.rs b/ra-rpc/src/openapi.rs index 7eb0a0e92..7c6196eb3 100644 --- a/ra-rpc/src/openapi.rs +++ b/ra-rpc/src/openapi.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Utilities to derive OpenAPI documents for pRPC services from their compiled //! protobuf descriptors. The resulting spec can be served directly or embedded diff --git a/ra-rpc/src/rocket_helper.rs b/ra-rpc/src/rocket_helper.rs index 88e32060e..584930318 100644 --- a/ra-rpc/src/rocket_helper.rs +++ b/ra-rpc/src/rocket_helper.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::convert::Infallible; diff --git a/ra-tls/Cargo.toml b/ra-tls/Cargo.toml index aed0ee5d3..7ad781f6c 100644 --- a/ra-tls/Cargo.toml +++ b/ra-tls/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "ra-tls" diff --git a/ra-tls/src/attestation.rs b/ra-tls/src/attestation.rs index fcd6815a6..5c0ddd4b6 100644 --- a/ra-tls/src/attestation.rs +++ b/ra-tls/src/attestation.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Embedding and extracting attestation from/to TLS certificate diff --git a/ra-tls/src/cert.rs b/ra-tls/src/cert.rs index bf5d16d63..6eea4b1c5 100644 --- a/ra-tls/src/cert.rs +++ b/ra-tls/src/cert.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Certificate creation functions. diff --git a/ra-tls/src/kdf.rs b/ra-tls/src/kdf.rs index fbe16535b..96eed96f8 100644 --- a/ra-tls/src/kdf.rs +++ b/ra-tls/src/kdf.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Key derivation functions. use anyhow::{anyhow, Context, Result}; diff --git a/ra-tls/src/lib.rs b/ra-tls/src/lib.rs index 70bb712e8..347b6b467 100644 --- a/ra-tls/src/lib.rs +++ b/ra-tls/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! RATLS library for Phala #![deny(missing_docs)] diff --git a/ra-tls/src/oids.rs b/ra-tls/src/oids.rs index 38e71d1bc..bde9a300a 100644 --- a/ra-tls/src/oids.rs +++ b/ra-tls/src/oids.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! OIDs used by the RATLS protocol. diff --git a/ra-tls/src/traits.rs b/ra-tls/src/traits.rs index aaac91158..3b1e20350 100644 --- a/ra-tls/src/traits.rs +++ b/ra-tls/src/traits.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Traits for the crate diff --git a/rocket-vsock-listener/Cargo.toml b/rocket-vsock-listener/Cargo.toml index a0fe89de0..a108bf13e 100644 --- a/rocket-vsock-listener/Cargo.toml +++ b/rocket-vsock-listener/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "rocket-vsock-listener" diff --git a/rocket-vsock-listener/src/lib.rs b/rocket-vsock-listener/src/lib.rs index fb7014985..af38ea27c 100644 --- a/rocket-vsock-listener/src/lib.rs +++ b/rocket-vsock-listener/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use derive_more::Display; use rocket::listener::{Connection, Endpoint, Listener}; diff --git a/run-tests.sh b/run-tests.sh index e348d1bed..a06ea133d 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/run.sh b/run.sh index bf8a2af98..e0cf81154 100755 --- a/run.sh +++ b/run.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 VMDIR=$1 IMAGE_PATH=./images/$(jq -r '.image' ${VMDIR}/vm-manifest.json) diff --git a/scripts/add-spdx-attribution.py b/scripts/add-spdx-attribution.py index 67de938ba..1314e392f 100755 --- a/scripts/add-spdx-attribution.py +++ b/scripts/add-spdx-attribution.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 """ SPDX Header Attribution Script diff --git a/scripts/config-fw.sh b/scripts/config-fw.sh index c3442d791..feeb226bc 100755 --- a/scripts/config-fw.sh +++ b/scripts/config-fw.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: © 2025 Phala Network -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/serde-duration/Cargo.toml b/serde-duration/Cargo.toml index c8b9280c4..9a2383a88 100644 --- a/serde-duration/Cargo.toml +++ b/serde-duration/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "serde-duration" diff --git a/serde-duration/src/lib.rs b/serde-duration/src/lib.rs index 345cc4f6b..43d9abadc 100644 --- a/serde-duration/src/lib.rs +++ b/serde-duration/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use serde::{Deserialize, Deserializer, Serializer}; use std::time::Duration; diff --git a/size-parser/Cargo.toml b/size-parser/Cargo.toml index 1107dec75..44fb210f2 100644 --- a/size-parser/Cargo.toml +++ b/size-parser/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "size-parser" diff --git a/size-parser/src/lib.rs b/size-parser/src/lib.rs index b7cf39896..ca77632ad 100644 --- a/size-parser/src/lib.rs +++ b/size-parser/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! A utility crate for parsing and handling memory sizes with serde support. //! diff --git a/sodiumbox/Cargo.toml b/sodiumbox/Cargo.toml index 9fcd67029..1fabb3b2e 100644 --- a/sodiumbox/Cargo.toml +++ b/sodiumbox/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "sodiumbox" diff --git a/sodiumbox/src/lib.rs b/sodiumbox/src/lib.rs index 8375776c6..828d0f6aa 100644 --- a/sodiumbox/src/lib.rs +++ b/sodiumbox/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Pure Rust implementation of libsodium's sealed box encryption/decryption. //! diff --git a/supervisor/Cargo.toml b/supervisor/Cargo.toml index e9e91b559..4d9b0e932 100644 --- a/supervisor/Cargo.toml +++ b/supervisor/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "supervisor" diff --git a/supervisor/client/Cargo.toml b/supervisor/client/Cargo.toml index 99db01f7e..a76d2359f 100644 --- a/supervisor/client/Cargo.toml +++ b/supervisor/client/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "supervisor-client" diff --git a/supervisor/client/src/lib.rs b/supervisor/client/src/lib.rs index 53257e5c9..87e396860 100644 --- a/supervisor/client/src/lib.rs +++ b/supervisor/client/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{path::Path, sync::Arc, time::Duration}; diff --git a/supervisor/client/src/main.rs b/supervisor/client/src/main.rs index b993984c7..06662b62d 100644 --- a/supervisor/client/src/main.rs +++ b/supervisor/client/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::Result; use clap::{Parser, Subcommand}; diff --git a/supervisor/src/lib.rs b/supervisor/src/lib.rs index 131070819..e60f97b9d 100644 --- a/supervisor/src/lib.rs +++ b/supervisor/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 mod process; mod supervisor; diff --git a/supervisor/src/main.rs b/supervisor/src/main.rs index 752b511ca..eb17d2486 100644 --- a/supervisor/src/main.rs +++ b/supervisor/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::path::Path; diff --git a/supervisor/src/process.rs b/supervisor/src/process.rs index c424f9b51..d52407ff0 100644 --- a/supervisor/src/process.rs +++ b/supervisor/src/process.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{bail, Result}; use bon::Builder; diff --git a/supervisor/src/supervisor.rs b/supervisor/src/supervisor.rs index 378013c05..3a400ceb5 100644 --- a/supervisor/src/supervisor.rs +++ b/supervisor/src/supervisor.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::process::{Process, ProcessConfig, ProcessInfo}; use anyhow::{bail, Context, Result}; diff --git a/supervisor/src/web_api.rs b/supervisor/src/web_api.rs index 2521ee20b..fc791da00 100644 --- a/supervisor/src/web_api.rs +++ b/supervisor/src/web_api.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::{anyhow, Result}; use or_panic::ResultOrPanic; diff --git a/supervisor/supervisor.toml b/supervisor/supervisor.toml index b66db0ac6..0d41ee507 100644 --- a/supervisor/supervisor.toml +++ b/supervisor/supervisor.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [default] workers = 8 diff --git a/supervisor/tests/test-cli.sh b/supervisor/tests/test-cli.sh index 2985396aa..e9574dcbb 100755 --- a/supervisor/tests/test-cli.sh +++ b/supervisor/tests/test-cli.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # Colors for output GREEN='\033[0;32m' diff --git a/supervisor/tests/test.sh b/supervisor/tests/test.sh index b1ec27e47..5fef7125b 100755 --- a/supervisor/tests/test.sh +++ b/supervisor/tests/test.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # Base URL BASE_URL="http://localhost" diff --git a/tdx-attest/Cargo.toml b/tdx-attest/Cargo.toml index 3470519d1..7aa402d29 100644 --- a/tdx-attest/Cargo.toml +++ b/tdx-attest/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # SPDX-FileCopyrightText: © 2025 Daniel Sharifi # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "tdx-attest" diff --git a/tdx-attest/src/dummy.rs b/tdx-attest/src/dummy.rs index 58c1ec006..60ab3d5df 100644 --- a/tdx-attest/src/dummy.rs +++ b/tdx-attest/src/dummy.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use cc_eventlog::TdxEvent; use thiserror::Error; diff --git a/tdx-attest/src/lib.rs b/tdx-attest/src/lib.rs index b8bc5a94e..90e14f3ac 100644 --- a/tdx-attest/src/lib.rs +++ b/tdx-attest/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #[cfg(all(target_os = "linux", target_arch = "x86_64"))] pub use linux::*; diff --git a/tdx-attest/src/linux.rs b/tdx-attest/src/linux.rs index b8d87393f..04177e264 100644 --- a/tdx-attest/src/linux.rs +++ b/tdx-attest/src/linux.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use anyhow::bail; use std::path::PathBuf; diff --git a/tdx-attest/src/linux/configfs.rs b/tdx-attest/src/linux/configfs.rs index 3f37a7b98..228013106 100644 --- a/tdx-attest/src/linux/configfs.rs +++ b/tdx-attest/src/linux/configfs.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Pure Rust implementation of TDX quote generation using Linux TSM configfs interface //! diff --git a/test-scripts/get-app-key.sh b/test-scripts/get-app-key.sh index 7285a4fbe..db04b056e 100755 --- a/test-scripts/get-app-key.sh +++ b/test-scripts/get-app-key.sh @@ -1,7 +1,7 @@ #!/bin/bash # SPDX-FileCopyrightText: © 2025 Phala Network -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 CERT_FILE=${1} KEY_FILE=${CERT_FILE%.*}.key diff --git a/test-scripts/inspect-cert.sh b/test-scripts/inspect-cert.sh index 141ef4e8d..65cd39ac5 100755 --- a/test-scripts/inspect-cert.sh +++ b/test-scripts/inspect-cert.sh @@ -1,6 +1,6 @@ #!/bin/sh # SPDX-FileCopyrightText: © 2025 Phala Network -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 openssl x509 -text -noout -in $1 \ No newline at end of file diff --git a/tpm-attest/Cargo.toml b/tpm-attest/Cargo.toml index 931b5b631..db83d6d4f 100644 --- a/tpm-attest/Cargo.toml +++ b/tpm-attest/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "tpm-attest" diff --git a/tpm-attest/src/esapi.rs b/tpm-attest/src/esapi.rs index 7b5c41850..4d5776e79 100644 --- a/tpm-attest/src/esapi.rs +++ b/tpm-attest/src/esapi.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::{PcrSelection, PcrValue}; use anyhow::{bail, Result}; diff --git a/tpm-attest/src/gcp_ak.rs b/tpm-attest/src/gcp_ak.rs index 56a703135..0a77f74e6 100644 --- a/tpm-attest/src/gcp_ak.rs +++ b/tpm-attest/src/gcp_ak.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! GCP vTPM pre-provisioned AK loading //! diff --git a/tpm-attest/src/lib.rs b/tpm-attest/src/lib.rs index 10e179fdc..37a7e7199 100644 --- a/tpm-attest/src/lib.rs +++ b/tpm-attest/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 Attestation Library //! diff --git a/tpm-qvl/Cargo.toml b/tpm-qvl/Cargo.toml index 04d5fbb68..b6ab486b5 100644 --- a/tpm-qvl/Cargo.toml +++ b/tpm-qvl/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "tpm-qvl" diff --git a/tpm-qvl/src/collateral.rs b/tpm-qvl/src/collateral.rs index 2c08b6b56..60216ba88 100644 --- a/tpm-qvl/src/collateral.rs +++ b/tpm-qvl/src/collateral.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Collateral retrieval module //! diff --git a/tpm-qvl/src/lib.rs b/tpm-qvl/src/lib.rs index 2fe2b47de..a183e4dd6 100644 --- a/tpm-qvl/src/lib.rs +++ b/tpm-qvl/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM Quote Verification Library (QVL) //! diff --git a/tpm-qvl/src/verify.rs b/tpm-qvl/src/verify.rs index 9cd591ebc..875f58733 100644 --- a/tpm-qvl/src/verify.rs +++ b/tpm-qvl/src/verify.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM Quote Verification Module diff --git a/tpm-types/Cargo.toml b/tpm-types/Cargo.toml index cb81483ca..e1046519b 100644 --- a/tpm-types/Cargo.toml +++ b/tpm-types/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "tpm-types" diff --git a/tpm-types/src/lib.rs b/tpm-types/src/lib.rs index 18dd23f28..19d32d2c3 100644 --- a/tpm-types/src/lib.rs +++ b/tpm-types/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM Types - Common TPM-related type definitions //! diff --git a/tpm2/Cargo.toml b/tpm2/Cargo.toml index f51a07896..32de264c4 100644 --- a/tpm2/Cargo.toml +++ b/tpm2/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "tpm2" diff --git a/tpm2/src/bin/tpm2-test.rs b/tpm2/src/bin/tpm2-test.rs index 6afb2d827..20f91bf56 100644 --- a/tpm2/src/bin/tpm2-test.rs +++ b/tpm2/src/bin/tpm2-test.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 Test CLI //! diff --git a/tpm2/src/commands.rs b/tpm2/src/commands.rs index 4eb4b4e4b..9c34d89cb 100644 --- a/tpm2/src/commands.rs +++ b/tpm2/src/commands.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 command implementations //! diff --git a/tpm2/src/constants.rs b/tpm2/src/constants.rs index c562fc019..5baa0ef57 100644 --- a/tpm2/src/constants.rs +++ b/tpm2/src/constants.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 constants and command codes diff --git a/tpm2/src/device.rs b/tpm2/src/device.rs index f57a7d1ff..45b5ac492 100644 --- a/tpm2/src/device.rs +++ b/tpm2/src/device.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM device communication layer //! diff --git a/tpm2/src/lib.rs b/tpm2/src/lib.rs index ab5db337e..4fe0cb2a4 100644 --- a/tpm2/src/lib.rs +++ b/tpm2/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! Pure Rust TPM 2.0 implementation //! diff --git a/tpm2/src/marshal.rs b/tpm2/src/marshal.rs index e46eedd51..6e44ecc28 100644 --- a/tpm2/src/marshal.rs +++ b/tpm2/src/marshal.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 marshalling/unmarshalling utilities //! diff --git a/tpm2/src/session.rs b/tpm2/src/session.rs index b706990a7..4fce72563 100644 --- a/tpm2/src/session.rs +++ b/tpm2/src/session.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 session management diff --git a/tpm2/src/types.rs b/tpm2/src/types.rs index 65d9c709f..28a263c44 100644 --- a/tpm2/src/types.rs +++ b/tpm2/src/types.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! TPM 2.0 data types diff --git a/verifier/Cargo.toml b/verifier/Cargo.toml index 1f0513ef4..82529e92e 100644 --- a/verifier/Cargo.toml +++ b/verifier/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-verifier" diff --git a/verifier/builder/Dockerfile b/verifier/builder/Dockerfile index 2f7d6a7e7..1bc699f24 100644 --- a/verifier/builder/Dockerfile +++ b/verifier/builder/Dockerfile @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 FROM rust:1.92.0@sha256:48851a839d6a67370c9dbe0e709bedc138e3e404b161c5233aedcf2b717366e4 AS verifier-builder COPY builder/shared /build/shared diff --git a/verifier/builder/build-image.sh b/verifier/builder/build-image.sh index 75bcca791..a329c590d 100755 --- a/verifier/builder/build-image.sh +++ b/verifier/builder/build-image.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -euo pipefail diff --git a/verifier/builder/shared/config-qemu.sh b/verifier/builder/shared/config-qemu.sh index 94174a585..651fa7606 100755 --- a/verifier/builder/shared/config-qemu.sh +++ b/verifier/builder/shared/config-qemu.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 BUILD_DIR="$1" PREFIX="$2" diff --git a/verifier/builder/shared/pin-packages.sh b/verifier/builder/shared/pin-packages.sh index 5aa8ba4a4..f5ef7657a 100755 --- a/verifier/builder/shared/pin-packages.sh +++ b/verifier/builder/shared/pin-packages.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e PKG_LIST=$1 diff --git a/verifier/dstack-verifier.toml b/verifier/dstack-verifier.toml index 8c8a9b89e..f99c27bfb 100644 --- a/verifier/dstack-verifier.toml +++ b/verifier/dstack-verifier.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 # Server configuration address = "0.0.0.0" diff --git a/verifier/src/lib.rs b/verifier/src/lib.rs index 8ead45fdc..826a8c014 100644 --- a/verifier/src/lib.rs +++ b/verifier/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! CVM verification library //! diff --git a/verifier/src/main.rs b/verifier/src/main.rs index 1bd72a918..564a4de4d 100644 --- a/verifier/src/main.rs +++ b/verifier/src/main.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::sync::Arc; diff --git a/verifier/src/types.rs b/verifier/src/types.rs index c921ed8ba..5a6dc7ad7 100644 --- a/verifier/src/types.rs +++ b/verifier/src/types.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use ra_tls::attestation::AppInfo; use serde::{Deserialize, Serialize}; diff --git a/verifier/src/verification.rs b/verifier/src/verification.rs index d9e6da519..12c900888 100644 --- a/verifier/src/verification.rs +++ b/verifier/src/verification.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{ ffi::OsStr, diff --git a/verifier/test.sh b/verifier/test.sh index 4f9554cf4..1f43a5adf 100755 --- a/verifier/test.sh +++ b/verifier/test.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 set -e diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml index c5da0b832..ef83949dd 100644 --- a/vmm/Cargo.toml +++ b/vmm/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-vmm" diff --git a/vmm/rpc/Cargo.toml b/vmm/rpc/Cargo.toml index 75f067353..a81f3ee0c 100644 --- a/vmm/rpc/Cargo.toml +++ b/vmm/rpc/Cargo.toml @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: © 2024-2025 Phala Network # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: BUSL-1.1 [package] name = "dstack-vmm-rpc" diff --git a/vmm/rpc/build.rs b/vmm/rpc/build.rs index fe19530a5..5e0d5ddec 100644 --- a/vmm/rpc/build.rs +++ b/vmm/rpc/build.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 #![allow(clippy::expect_used)] diff --git a/vmm/rpc/proto/prpc.proto b/vmm/rpc/proto/prpc.proto index cd5a5d471..81cfa02d7 100644 --- a/vmm/rpc/proto/prpc.proto +++ b/vmm/rpc/proto/prpc.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/vmm/rpc/proto/vmm_rpc.proto b/vmm/rpc/proto/vmm_rpc.proto index 51c3c3dd3..1bc1f6de0 100644 --- a/vmm/rpc/proto/vmm_rpc.proto +++ b/vmm/rpc/proto/vmm_rpc.proto @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 syntax = "proto3"; diff --git a/vmm/rpc/src/lib.rs b/vmm/rpc/src/lib.rs index 4780ac98d..cea5f7b6a 100644 --- a/vmm/rpc/src/lib.rs +++ b/vmm/rpc/src/lib.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 extern crate alloc; diff --git a/vmm/src/app.rs b/vmm/src/app.rs index c176ce94f..002648520 100644 --- a/vmm/src/app.rs +++ b/vmm/src/app.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use crate::config::{Config, ProcessAnnotation, Protocol}; diff --git a/vmm/src/app/id_pool.rs b/vmm/src/app/id_pool.rs index 5bbb205e0..93538a78c 100644 --- a/vmm/src/app/id_pool.rs +++ b/vmm/src/app/id_pool.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::collections::BTreeSet; diff --git a/vmm/src/app/image.rs b/vmm/src/app/image.rs index ae5868e5e..8db48c2d9 100644 --- a/vmm/src/app/image.rs +++ b/vmm/src/app/image.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use fs_err as fs; use path_absolutize::Absolutize; diff --git a/vmm/src/app/qemu.rs b/vmm/src/app/qemu.rs index 735ca785f..4d27bfa5f 100644 --- a/vmm/src/app/qemu.rs +++ b/vmm/src/app/qemu.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 //! QEMU related code use crate::{ diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 8a593fc00..c178a1ff2 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: © 2024-2025 Phala Network // -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: BUSL-1.1 use std::{net::IpAddr, path::PathBuf, process::Command, str::FromStr}; diff --git a/vmm/src/console_v0.html b/vmm/src/console_v0.html index e6b6a0075..805217106 100644 --- a/vmm/src/console_v0.html +++ b/vmm/src/console_v0.html @@ -1,7 +1,7 @@ diff --git a/vmm/src/console_v1.html b/vmm/src/console_v1.html index 8ed1bcc52..de869b386 100644 --- a/vmm/src/console_v1.html +++ b/vmm/src/console_v1.html @@ -1,6 +1,6 @@ @@ -11,7 +11,7 @@ {{TITLE}}