dstack_sdk/
tappd_client.rs1use 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
33pub struct TappdClient {
35 base_url: String,
37 endpoint: String,
39 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 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 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 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 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 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}