Skip to main content

dstack_sdk/
tappd_client.rs

1// SPDX-FileCopyrightText: © 2025 Daniel Sharifi <daniel.sharifi@nearone.org>
2// SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
3//
4// SPDX-License-Identifier: Apache-2.0
5
6use crate::dstack_client::BaseClient;
7use anyhow::{bail, Result};
8use hex::encode as hex_encode;
9use http_client_unix_domain_socket::{ClientUnix, Method};
10use reqwest::Client;
11use serde::{de::DeserializeOwned, Deserialize, Serialize};
12use serde_json::{json, Value};
13use std::env;
14
15pub use dstack_sdk_types::tappd::*;
16
17fn get_tappd_endpoint(endpoint: Option<&str>) -> String {
18    if let Some(e) = endpoint {
19        return e.to_string();
20    }
21    if let Ok(sim_endpoint) = env::var("TAPPD_SIMULATOR_ENDPOINT") {
22        return sim_endpoint;
23    }
24    // Try paths in order: legacy paths first, then namespaced paths
25    const SOCKET_PATHS: &[&str] = &[
26        "/var/run/tappd.sock",
27        "/run/tappd.sock",
28        "/var/run/dstack/tappd.sock",
29        "/run/dstack/tappd.sock",
30    ];
31    for path in SOCKET_PATHS {
32        if std::path::Path::new(path).exists() {
33            return path.to_string();
34        }
35    }
36    SOCKET_PATHS[0].to_string()
37}
38
39#[derive(Debug)]
40pub enum TappdClientKind {
41    Http,
42    Unix,
43}
44
45/// The main client for interacting with the legacy Tappd service
46pub struct TappdClient {
47    /// The base URL for HTTP requests
48    base_url: String,
49    /// The endpoint for Unix domain socket communication
50    endpoint: String,
51    /// The type of client (HTTP or Unix domain socket)
52    client: TappdClientKind,
53}
54
55impl BaseClient for TappdClient {}
56
57impl TappdClient {
58    pub fn new(endpoint: Option<&str>) -> Self {
59        let endpoint = get_tappd_endpoint(endpoint);
60        let (base_url, client) = match endpoint {
61            ref e if e.starts_with("http://") || e.starts_with("https://") => {
62                (e.to_string(), TappdClientKind::Http)
63            }
64            _ => ("http://localhost".to_string(), TappdClientKind::Unix),
65        };
66
67        TappdClient {
68            base_url,
69            endpoint,
70            client,
71        }
72    }
73
74    async fn send_rpc_request<S: Serialize, D: DeserializeOwned>(
75        &self,
76        path: &str,
77        payload: &S,
78    ) -> anyhow::Result<D> {
79        match &self.client {
80            TappdClientKind::Http => {
81                let client = Client::new();
82                let url = format!(
83                    "{}/{}",
84                    self.base_url.trim_end_matches('/'),
85                    path.trim_start_matches('/')
86                );
87                let res = client
88                    .post(&url)
89                    .json(payload)
90                    .header("Content-Type", "application/json")
91                    .send()
92                    .await?
93                    .error_for_status()?;
94                Ok(res.json().await?)
95            }
96            TappdClientKind::Unix => {
97                let mut unix_client = ClientUnix::try_new(&self.endpoint).await?;
98                let res = unix_client
99                    .send_request_json::<_, _, Value>(
100                        path,
101                        Method::POST,
102                        &[("Content-Type", "application/json")],
103                        Some(&payload),
104                    )
105                    .await?;
106                Ok(res.1)
107            }
108        }
109    }
110
111    /// Derives a key from the Tappd service using the path as both path and subject
112    pub async fn derive_key(&self, path: &str) -> Result<DeriveKeyResponse> {
113        self.derive_key_with_subject_and_alt_names(path, Some(path), None)
114            .await
115    }
116
117    /// Derives a key from the Tappd service with a specific subject
118    pub async fn derive_key_with_subject(
119        &self,
120        path: &str,
121        subject: &str,
122    ) -> Result<DeriveKeyResponse> {
123        self.derive_key_with_subject_and_alt_names(path, Some(subject), None)
124            .await
125    }
126
127    /// Derives a key from the Tappd service with full configuration
128    pub async fn derive_key_with_subject_and_alt_names(
129        &self,
130        path: &str,
131        subject: Option<&str>,
132        alt_names: Option<Vec<String>>,
133    ) -> Result<DeriveKeyResponse> {
134        let subject = subject.unwrap_or(path);
135
136        let mut payload = json!({
137            "path": path,
138            "subject": subject,
139        });
140
141        if let Some(alt_names) = alt_names {
142            if !alt_names.is_empty() {
143                payload["alt_names"] = json!(alt_names);
144            }
145        }
146
147        let response = self
148            .send_rpc_request("/prpc/Tappd.DeriveKey", &payload)
149            .await?;
150        Ok(response)
151    }
152
153    /// Sends a raw quote request with 64 bytes of report data
154    pub async fn get_quote(&self, report_data: Vec<u8>) -> Result<TdxQuoteResponse> {
155        if report_data.len() != 64 {
156            bail!("Report data must be exactly 64 bytes for raw quote");
157        }
158
159        let payload = json!({
160            "report_data": hex_encode(report_data),
161        });
162
163        let response = self
164            .send_rpc_request("/prpc/Tappd.RawQuote", &payload)
165            .await?;
166        Ok(response)
167    }
168
169    /// Retrieves information about the Tappd instance
170    pub async fn info(&self) -> Result<TappdInfoResponse> {
171        #[derive(Deserialize)]
172        struct RawInfoResponse {
173            app_id: String,
174            instance_id: String,
175            app_cert: String,
176            tcb_info: String,
177            app_name: String,
178        }
179
180        let raw_response: RawInfoResponse = self
181            .send_rpc_request("/prpc/Tappd.Info", &json!({}))
182            .await?;
183
184        let tcb_info: TappdTcbInfo = serde_json::from_str(&raw_response.tcb_info)?;
185
186        Ok(TappdInfoResponse {
187            app_id: raw_response.app_id,
188            instance_id: raw_response.instance_id,
189            app_cert: raw_response.app_cert,
190            tcb_info,
191            app_name: raw_response.app_name,
192        })
193    }
194}