Skip to content

Commit 36f4151

Browse files
authored
Merge pull request #421 from Dstack-TEE/fix-ct-monitor-endpoint
ct_monitor: Use HTTP endpoint instead of pRPC for acme-info
2 parents c122dd6 + 10a6bbe commit 36f4151

File tree

3 files changed

+279
-29
lines changed

3 files changed

+279
-29
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ct_monitor/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ license.workspace = true
1111

1212
[dependencies]
1313
anyhow.workspace = true
14-
clap = { workspace = true, features = ["derive"] }
14+
clap = { workspace = true, features = ["derive", "env"] }
15+
hex = { workspace = true, features = ["alloc", "std"] }
1516
hex_fmt.workspace = true
1617
regex.workspace = true
1718
reqwest = { workspace = true, default-features = false, features = ["json", "rustls-tls", "charset", "hickory-dns"] }
1819
serde = { workspace = true, features = ["derive"] }
20+
serde-human-bytes.workspace = true
1921
serde_json.workspace = true
22+
sha2.workspace = true
2023
tokio = { workspace = true, features = ["full"] }
2124
tracing.workspace = true
2225
tracing-subscriber.workspace = true
2326
x509-parser.workspace = true
24-
25-
dstack-gateway-rpc.workspace = true
26-
ra-rpc = { workspace = true, default-features = false, features = ["client"] }

ct_monitor/src/main.rs

Lines changed: 272 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,95 @@
44

55
use anyhow::{bail, Context, Result};
66
use clap::Parser;
7-
use dstack_gateway_rpc::gateway_client::GatewayClient;
8-
use ra_rpc::client::RaClient;
97
use regex::Regex;
108
use serde::{Deserialize, Serialize};
9+
use serde_human_bytes as hex_bytes;
10+
use sha2::{Digest, Sha512};
1111
use std::collections::BTreeSet;
1212
use std::time::Duration;
13-
use tracing::{debug, error, info};
13+
use tracing::{debug, error, info, warn};
1414
use x509_parser::prelude::*;
1515

1616
const BASE_URL: &str = "https://crt.sh";
1717

18+
/// Quoted public key with TDX quote
19+
#[derive(Debug, Deserialize)]
20+
struct QuotedPublicKey {
21+
/// Hex-encoded public key
22+
public_key: String,
23+
/// JSON-encoded GetQuoteResponse
24+
quote: String,
25+
}
26+
27+
/// GetQuoteResponse from guest-agent
28+
#[derive(Debug, Deserialize)]
29+
struct GetQuoteResponse {
30+
/// TDX quote (hex-encoded in JSON)
31+
#[serde(with = "hex_bytes")]
32+
quote: Vec<u8>,
33+
/// JSON-encoded event log
34+
event_log: String,
35+
/// VM configuration
36+
vm_config: String,
37+
}
38+
39+
/// Request for dstack-verifier
40+
#[derive(Debug, Serialize)]
41+
struct VerificationRequest {
42+
quote: String,
43+
event_log: String,
44+
vm_config: String,
45+
pccs_url: Option<String>,
46+
}
47+
48+
/// Response from dstack-verifier
49+
#[derive(Debug, Deserialize)]
50+
struct VerificationResponse {
51+
is_valid: bool,
52+
details: VerificationDetails,
53+
reason: Option<String>,
54+
}
55+
56+
#[derive(Debug, Deserialize)]
57+
struct VerificationDetails {
58+
#[allow(dead_code)]
59+
quote_verified: bool,
60+
#[allow(dead_code)]
61+
event_log_verified: bool,
62+
#[allow(dead_code)]
63+
os_image_hash_verified: bool,
64+
report_data: Option<String>,
65+
app_info: Option<AppInfo>,
66+
}
67+
68+
/// App info from verification response
69+
#[derive(Debug, Deserialize)]
70+
struct AppInfo {
71+
#[serde(with = "hex_bytes")]
72+
app_id: Vec<u8>,
73+
#[serde(with = "hex_bytes")]
74+
compose_hash: Vec<u8>,
75+
#[serde(with = "hex_bytes")]
76+
os_image_hash: Vec<u8>,
77+
}
78+
79+
#[derive(Debug, Deserialize)]
80+
struct AcmeInfoResponse {
81+
#[allow(dead_code)]
82+
account_uri: String,
83+
#[allow(dead_code)]
84+
hist_keys: Vec<String>,
85+
quoted_hist_keys: Vec<QuotedPublicKey>,
86+
}
87+
1888
struct Monitor {
1989
gateway_uri: String,
20-
domain: String,
90+
verifier_url: String,
91+
pccs_url: Option<String>,
92+
base_domain: String,
2193
known_keys: BTreeSet<Vec<u8>>,
2294
last_checked: Option<u64>,
95+
client: reqwest::Client,
2396
}
2497

2598
#[derive(Debug, Serialize, Deserialize)]
@@ -37,24 +110,194 @@ struct CTLog {
37110
}
38111

39112
impl Monitor {
40-
fn new(gateway_uri: String, domain: String) -> Result<Self> {
41-
validate_domain(&domain)?;
113+
/// Create a new monitor
114+
/// `gateway` format: `base_domain[:port]`, e.g., `example.com` or `example.com:8443`
115+
fn new(gateway: String, verifier_url: String, pccs_url: Option<String>) -> Result<Self> {
116+
let (base_domain, gateway_uri) = Self::parse_gateway(&gateway)?;
117+
validate_domain(&base_domain)?;
42118
Ok(Self {
43119
gateway_uri,
44-
domain,
120+
verifier_url,
121+
pccs_url,
122+
base_domain,
45123
known_keys: BTreeSet::new(),
46124
last_checked: None,
125+
client: reqwest::Client::new(),
47126
})
48127
}
49128

129+
/// Parse gateway input into base_domain and gateway URI
130+
/// Input: `base_domain[:port]`, e.g., `example.com` or `example.com:8443`
131+
/// Output: (base_domain, gateway_uri)
132+
fn parse_gateway(gateway: &str) -> Result<(String, String)> {
133+
let (base_domain, port) = match gateway.rsplit_once(':') {
134+
Some((domain, port_str)) => {
135+
// Validate port is a number
136+
let _: u16 = port_str.parse().context("invalid port number")?;
137+
(domain.to_string(), Some(port_str.to_string()))
138+
}
139+
None => (gateway.to_string(), None),
140+
};
141+
142+
let gateway_uri = match port {
143+
Some(p) => format!("https://gateway.{}:{}", base_domain, p),
144+
None => format!("https://gateway.{}", base_domain),
145+
};
146+
147+
Ok((base_domain, gateway_uri))
148+
}
149+
150+
/// Compute expected report_data for a public key using zt-cert content type
151+
fn compute_expected_report_data(public_key: &[u8]) -> [u8; 64] {
152+
// Format: sha512("zt-cert:" + public_key)
153+
let mut hasher = Sha512::new();
154+
hasher.update(b"zt-cert:");
155+
hasher.update(public_key);
156+
hasher.finalize().into()
157+
}
158+
159+
/// Verify a quoted public key using the verifier service
160+
/// Returns (public_key, app_info)
161+
async fn verify_quoted_key(&self, quoted_key: &QuotedPublicKey) -> Result<(Vec<u8>, AppInfo)> {
162+
let public_key =
163+
hex::decode(&quoted_key.public_key).context("invalid hex in public_key")?;
164+
165+
if quoted_key.quote.is_empty() {
166+
bail!("empty quote for public key");
167+
}
168+
169+
// Parse the GetQuoteResponse from the quote field
170+
let quote_response: GetQuoteResponse =
171+
serde_json::from_str(&quoted_key.quote).context("failed to parse quote response")?;
172+
173+
// Build verification request
174+
let verify_request = VerificationRequest {
175+
quote: hex::encode(&quote_response.quote),
176+
event_log: quote_response.event_log,
177+
vm_config: quote_response.vm_config,
178+
pccs_url: self.pccs_url.clone(),
179+
};
180+
181+
// Call verifier
182+
let verify_url = format!("{}/verify", self.verifier_url.trim_end_matches('/'));
183+
let response = self
184+
.client
185+
.post(&verify_url)
186+
.json(&verify_request)
187+
.send()
188+
.await
189+
.context("failed to call verifier")?;
190+
191+
if !response.status().is_success() {
192+
bail!("verifier returned HTTP {}", response.status().as_u16());
193+
}
194+
195+
let verify_response: VerificationResponse = response
196+
.json()
197+
.await
198+
.context("failed to parse verifier response")?;
199+
200+
if !verify_response.is_valid {
201+
bail!(
202+
"quote verification failed: {}",
203+
verify_response.reason.unwrap_or_default()
204+
);
205+
}
206+
207+
// Verify report_data matches expected value
208+
let expected_report_data = Self::compute_expected_report_data(&public_key);
209+
let expected_hex = hex::encode(expected_report_data);
210+
211+
let actual_report_data = verify_response
212+
.details
213+
.report_data
214+
.context("verifier did not return report_data")?;
215+
216+
if actual_report_data != expected_hex {
217+
bail!(
218+
"report_data mismatch: expected {}, got {}",
219+
expected_hex,
220+
actual_report_data
221+
);
222+
}
223+
224+
let app_info = verify_response
225+
.details
226+
.app_info
227+
.context("verifier did not return app_info")?;
228+
229+
Ok((public_key, app_info))
230+
}
231+
50232
async fn refresh_known_keys(&mut self) -> Result<()> {
51-
info!("fetching known public keys from {}", self.gateway_uri);
52-
// TODO: Use RA-TLS
53-
let tls_no_check = true;
54-
let rpc = GatewayClient::new(RaClient::new(self.gateway_uri.clone(), tls_no_check)?);
55-
let info = rpc.acme_info().await?;
56-
self.known_keys = info.hist_keys.into_iter().collect();
57-
info!("got {} known public keys", self.known_keys.len());
233+
let acme_info_url = format!(
234+
"{}/.dstack/acme-info",
235+
self.gateway_uri.trim_end_matches('/')
236+
);
237+
info!("fetching known public keys from {}", acme_info_url);
238+
239+
let response = self
240+
.client
241+
.get(&acme_info_url)
242+
.send()
243+
.await
244+
.context("failed to fetch acme-info")?;
245+
246+
if !response.status().is_success() {
247+
bail!(
248+
"failed to fetch acme-info: HTTP {}",
249+
response.status().as_u16()
250+
);
251+
}
252+
253+
let info: AcmeInfoResponse = response
254+
.json()
255+
.await
256+
.context("failed to parse acme-info response")?;
257+
258+
info!(
259+
"got {} quoted public keys, verifying...",
260+
info.quoted_hist_keys.len()
261+
);
262+
263+
let mut verified_keys = BTreeSet::new();
264+
for (i, quoted_key) in info.quoted_hist_keys.iter().enumerate() {
265+
match self.verify_quoted_key(quoted_key).await {
266+
Ok((public_key, app_info)) => {
267+
info!(
268+
"✅ verified public key {}: {}",
269+
i,
270+
hex_fmt::HexFmt(&public_key)
271+
);
272+
info!(" app_id: {}", hex_fmt::HexFmt(&app_info.app_id));
273+
info!(
274+
" compose_hash: {}",
275+
hex_fmt::HexFmt(&app_info.compose_hash)
276+
);
277+
info!(
278+
" os_image_hash: {}",
279+
hex_fmt::HexFmt(&app_info.os_image_hash)
280+
);
281+
verified_keys.insert(public_key);
282+
}
283+
Err(e) => {
284+
warn!(
285+
"⚠️ failed to verify public key {}: {}",
286+
i,
287+
hex_fmt::HexFmt(&quoted_key.public_key)
288+
);
289+
warn!(" error: {:#}", e);
290+
// Continue with other keys, but don't add this one
291+
}
292+
}
293+
}
294+
295+
if verified_keys.is_empty() && !info.quoted_hist_keys.is_empty() {
296+
bail!("no public keys could be verified");
297+
}
298+
299+
self.known_keys = verified_keys;
300+
info!("verified {} public keys", self.known_keys.len());
58301
for key in self.known_keys.iter() {
59302
debug!(" {}", hex_fmt::HexFmt(key));
60303
}
@@ -64,7 +307,7 @@ impl Monitor {
64307
async fn get_logs(&self, count: u32) -> Result<Vec<CTLog>> {
65308
let url = format!(
66309
"{}/?q={}&output=json&limit={}",
67-
BASE_URL, self.domain, count
310+
BASE_URL, self.base_domain, count
68311
);
69312
let response = reqwest::get(&url).await?;
70313
Ok(response.json().await?)
@@ -125,7 +368,7 @@ impl Monitor {
125368
}
126369

127370
async fn run(&mut self) {
128-
info!("monitoring {}...", self.domain);
371+
info!("monitoring {}...", self.base_domain);
129372
loop {
130373
if let Err(err) = self.refresh_known_keys().await {
131374
error!("error refreshing known keys: {}", err);
@@ -151,12 +394,18 @@ fn validate_domain(domain: &str) -> Result<()> {
151394
#[derive(Parser, Debug)]
152395
#[command(author, version, about, long_about = None)]
153396
struct Args {
154-
/// The gateway URI
155-
#[arg(short, long)]
156-
gateway_uri: String,
157-
/// Domain name to monitor
158-
#[arg(short, long)]
159-
domain: String,
397+
/// Gateway address in format: base_domain[:port]
398+
/// e.g., "example.com" or "example.com:8443"
399+
#[arg(short, long, env = "GATEWAY")]
400+
gateway: String,
401+
402+
/// The dstack-verifier URL
403+
#[arg(short, long, env = "VERIFIER_URL")]
404+
verifier_url: String,
405+
406+
/// PCCS URL for TDX collateral fetching (optional)
407+
#[arg(long, env = "PCCS_URL")]
408+
pccs_url: Option<String>,
160409
}
161410

162411
#[tokio::main]
@@ -167,7 +416,7 @@ async fn main() -> anyhow::Result<()> {
167416
fmt().with_env_filter(filter).init();
168417
}
169418
let args = Args::parse();
170-
let mut monitor = Monitor::new(args.gateway_uri, args.domain)?;
419+
let mut monitor = Monitor::new(args.gateway, args.verifier_url, args.pccs_url)?;
171420
monitor.run().await;
172421
Ok(())
173422
}

0 commit comments

Comments
 (0)