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    "/var/run/tappd.sock".to_string()
25}
26
27#[derive(Debug)]
28pub enum TappdClientKind {
29    Http,
30    Unix,
31}
32
33/// The main client for interacting with the legacy Tappd service
34pub struct TappdClient {
35    /// The base URL for HTTP requests
36    base_url: String,
37    /// The endpoint for Unix domain socket communication
38    endpoint: String,
39    /// The type of client (HTTP or Unix domain socket)
40    client: TappdClientKind,
41}
42
43impl BaseClient for TappdClient {}
44
45impl TappdClient {
46    pub fn new(endpoint: Option<&str>) -> Self {
47        let endpoint = get_tappd_endpoint(endpoint);
48        let (base_url, client) = match endpoint {
49            ref e if e.starts_with("http://") || e.starts_with("https://") => {
50                (e.to_string(), TappdClientKind::Http)
51            }
52            _ => ("http://localhost".to_string(), TappdClientKind::Unix),
53        };
54
55        TappdClient {
56            base_url,
57            endpoint,
58            client,
59        }
60    }
61
62    async fn send_rpc_request<S: Serialize, D: DeserializeOwned>(
63        &self,
64        path: &str,
65        payload: &S,
66    ) -> anyhow::Result<D> {
67        match &self.client {
68            TappdClientKind::Http => {
69                let client = Client::new();
70                let url = format!(
71                    "{}/{}",
72                    self.base_url.trim_end_matches('/'),
73                    path.trim_start_matches('/')
74                );
75                let res = client
76                    .post(&url)
77                    .json(payload)
78                    .header("Content-Type", "application/json")
79                    .send()
80                    .await?
81                    .error_for_status()?;
82                Ok(res.json().await?)
83            }
84            TappdClientKind::Unix => {
85                let mut unix_client = ClientUnix::try_new(&self.endpoint).await?;
86                let res = unix_client
87                    .send_request_json::<_, _, Value>(
88                        path,
89                        Method::POST,
90                        &[("Content-Type", "application/json")],
91                        Some(&payload),
92                    )
93                    .await?;
94                Ok(res.1)
95            }
96        }
97    }
98
99    /// Derives a key from the Tappd service using the path as both path and subject
100    pub async fn derive_key(&self, path: &str) -> Result<DeriveKeyResponse> {
101        self.derive_key_with_subject_and_alt_names(path, Some(path), None)
102            .await
103    }
104
105    /// Derives a key from the Tappd service with a specific subject
106    pub async fn derive_key_with_subject(
107        &self,
108        path: &str,
109        subject: &str,
110    ) -> Result<DeriveKeyResponse> {
111        self.derive_key_with_subject_and_alt_names(path, Some(subject), None)
112            .await
113    }
114
115    /// Derives a key from the Tappd service with full configuration
116    pub async fn derive_key_with_subject_and_alt_names(
117        &self,
118        path: &str,
119        subject: Option<&str>,
120        alt_names: Option<Vec<String>>,
121    ) -> Result<DeriveKeyResponse> {
122        let subject = subject.unwrap_or(path);
123
124        let mut payload = json!({
125            "path": path,
126            "subject": subject,
127        });
128
129        if let Some(alt_names) = alt_names {
130            if !alt_names.is_empty() {
131                payload["alt_names"] = json!(alt_names);
132            }
133        }
134
135        let response = self
136            .send_rpc_request("/prpc/Tappd.DeriveKey", &payload)
137            .await?;
138        Ok(response)
139    }
140
141    /// Sends a raw quote request with 64 bytes of report data
142    pub async fn get_quote(&self, report_data: Vec<u8>) -> Result<TdxQuoteResponse> {
143        if report_data.len() != 64 {
144            bail!("Report data must be exactly 64 bytes for raw quote");
145        }
146
147        let payload = json!({
148            "report_data": hex_encode(report_data),
149        });
150
151        let response = self
152            .send_rpc_request("/prpc/Tappd.RawQuote", &payload)
153            .await?;
154        Ok(response)
155    }
156
157    /// Retrieves information about the Tappd instance
158    pub async fn info(&self) -> Result<TappdInfoResponse> {
159        #[derive(Deserialize)]
160        struct RawInfoResponse {
161            app_id: String,
162            instance_id: String,
163            app_cert: String,
164            tcb_info: String,
165            app_name: String,
166        }
167
168        let raw_response: RawInfoResponse = self
169            .send_rpc_request("/prpc/Tappd.Info", &json!({}))
170            .await?;
171
172        let tcb_info: TappdTcbInfo = serde_json::from_str(&raw_response.tcb_info)?;
173
174        Ok(TappdInfoResponse {
175            app_id: raw_response.app_id,
176            instance_id: raw_response.instance_id,
177            app_cert: raw_response.app_cert,
178            tcb_info,
179            app_name: raw_response.app_name,
180        })
181    }
182}