Skip to content

Commit f3e0537

Browse files
authored
Merge pull request #230 from Dstack-TEE/gw-quote
gateway: Add api for evidences
2 parents e26d73d + 1c397e9 commit f3e0537

9 files changed

Lines changed: 194 additions & 24 deletions

File tree

certbot/src/bot.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ impl CertBot {
210210
}
211211
}
212212

213-
fn read_pubkey(cert_pem: &str) -> Result<Vec<u8>> {
213+
pub fn read_pubkey(cert_pem: &str) -> Result<Vec<u8>> {
214214
let cert = read_pem(cert_pem)?;
215215
let public_key = cert.parse_x509().context("failed to parse x509 cert")?;
216216
Ok(public_key.tbs_certificate.public_key().raw.to_vec())

certbot/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
//! to the documentation of individual structs and functions.
1414
1515
pub use acme_client::AcmeClient;
16-
pub use bot::{CertBot, CertBotConfig};
16+
pub use bot::{read_pubkey, CertBot, CertBotConfig};
1717
pub use dns01_client::Dns01Client;
1818
pub use workdir::WorkDir;
1919

certbot/src/workdir.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ impl WorkDir {
5353
Ok(credentials.account_id)
5454
}
5555

56+
pub fn acme_account_quote_path(&self) -> PathBuf {
57+
self.workdir.join("acme-account.quote")
58+
}
59+
5660
pub fn list_cert_public_keys(&self) -> Result<BTreeSet<Vec<u8>>> {
5761
crate::bot::list_cert_public_keys(self.backup_dir())
5862
}

gateway/rpc/proto/gateway_rpc.proto

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ message WireGuardPeer {
2525
string ip = 2;
2626
// The wireguard peer endpoint.
2727
string endpoint = 3;
28-
}
28+
}
2929

3030
// WireGuardConfig is the configuration of the WireGuard.
3131
message WireGuardConfig {
@@ -79,12 +79,25 @@ message HostInfo {
7979
uint64 num_connections = 7;
8080
}
8181

82+
message QuotedPublicKey {
83+
bytes public_key = 1;
84+
string quote = 2;
85+
}
86+
8287
// AcmeInfoResponse is the response for AcmeInfo.
8388
message AcmeInfoResponse {
8489
// The ACME account URI.
8590
string account_uri = 1;
8691
// The public key history of the certificate.
8792
repeated bytes hist_keys = 2;
93+
// The quoted public key of the certificate.
94+
repeated QuotedPublicKey quoted_hist_keys = 3;
95+
// The quote of the ACME account URI.
96+
string account_quote = 4;
97+
// Active certificate
98+
string active_cert = 5;
99+
// The domain that serves ZT-HTTPS
100+
string base_domain = 6;
88101
}
89102

90103
// Get HostInfo for associated instance id.

gateway/src/main_service.rs

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::{
22
collections::{BTreeMap, BTreeSet},
33
net::Ipv4Addr,
44
ops::Deref,
5+
path::Path,
56
sync::{Arc, Mutex, MutexGuard, RwLock},
67
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
78
};
@@ -12,11 +13,14 @@ use certbot::{CertBot, WorkDir};
1213
use cmd_lib::run_cmd as cmd;
1314
use dstack_gateway_rpc::{
1415
gateway_server::{GatewayRpc, GatewayServer},
15-
AcmeInfoResponse, GatewayState, GuestAgentConfig, RegisterCvmRequest, RegisterCvmResponse,
16-
WireGuardConfig, WireGuardPeer,
16+
AcmeInfoResponse, GatewayState, GuestAgentConfig, QuotedPublicKey, RegisterCvmRequest,
17+
RegisterCvmResponse, WireGuardConfig, WireGuardPeer,
1718
};
19+
use dstack_guest_agent_rpc::{dstack_guest_client::DstackGuestClient, RawQuoteArgs};
1820
use fs_err as fs;
21+
use http_client::prpc::PrpcClient;
1922
use ra_rpc::{CallContext, RpcCall, VerifiedAttestation};
23+
use ra_tls::attestation::QuoteContentType;
2024
use rand::seq::IteratorRandom;
2125
use rinja::Template as _;
2226
use safe_write::safe_write;
@@ -170,6 +174,51 @@ impl Proxy {
170174
}
171175
Ok(renewed)
172176
}
177+
178+
pub(crate) async fn acme_info(&self) -> Result<AcmeInfoResponse> {
179+
let config = self.lock().config.clone();
180+
let workdir = WorkDir::new(&config.certbot.workdir);
181+
let account_uri = workdir.acme_account_uri().unwrap_or_default();
182+
let keys = workdir.list_cert_public_keys().unwrap_or_default();
183+
let agent = crate::dstack_agent().context("Failed to get dstack agent")?;
184+
let account_quote = get_or_generate_quote(
185+
&agent,
186+
QuoteContentType::Custom("acme-account"),
187+
account_uri.as_bytes(),
188+
workdir.acme_account_quote_path(),
189+
)
190+
.await
191+
.unwrap_or_default();
192+
193+
let mut quoted_hist_keys = vec![];
194+
for cert_path in workdir.list_certs().unwrap_or_default() {
195+
let cert_pem = fs::read_to_string(&cert_path).context("Failed to read key")?;
196+
let pubkey = certbot::read_pubkey(&cert_pem).context("Failed to read pubkey")?;
197+
let quote = get_or_generate_quote(
198+
&agent,
199+
QuoteContentType::Custom("zt-cert"),
200+
&pubkey,
201+
cert_path.display().to_string() + ".quote",
202+
)
203+
.await
204+
.unwrap_or_default();
205+
quoted_hist_keys.push(QuotedPublicKey {
206+
public_key: pubkey,
207+
quote,
208+
});
209+
}
210+
let active_cert =
211+
fs::read_to_string(workdir.cert_path()).context("Failed to read active cert")?;
212+
213+
Ok(AcmeInfoResponse {
214+
account_uri,
215+
hist_keys: keys.into_iter().collect(),
216+
account_quote,
217+
quoted_hist_keys,
218+
active_cert,
219+
base_domain: config.proxy.base_domain.clone(),
220+
})
221+
}
173222
}
174223

175224
fn load_state(state_path: &str) -> Result<ProxyStateMut> {
@@ -681,14 +730,7 @@ impl GatewayRpc for RpcHandler {
681730
}
682731

683732
async fn acme_info(self) -> Result<AcmeInfoResponse> {
684-
let state = self.state.lock();
685-
let workdir = WorkDir::new(&state.config.certbot.workdir);
686-
let account_uri = workdir.acme_account_uri().unwrap_or_default();
687-
let keys = workdir.list_cert_public_keys().unwrap_or_default();
688-
Ok(AcmeInfoResponse {
689-
account_uri,
690-
hist_keys: keys.into_iter().collect(),
691-
})
733+
self.state.acme_info().await
692734
}
693735

694736
async fn update_state(self, request: GatewayState) -> Result<()> {
@@ -725,6 +767,26 @@ impl GatewayRpc for RpcHandler {
725767
}
726768
}
727769

770+
async fn get_or_generate_quote(
771+
agent: &DstackGuestClient<PrpcClient>,
772+
content_type: QuoteContentType<'_>,
773+
payload: &[u8],
774+
quote_path: impl AsRef<Path>,
775+
) -> Result<String> {
776+
let quote_path = quote_path.as_ref();
777+
if fs::metadata(quote_path).is_ok() {
778+
return fs::read_to_string(quote_path).context("Failed to read quote");
779+
}
780+
let report_data = content_type.to_report_data(payload).to_vec();
781+
let response = agent
782+
.get_quote(RawQuoteArgs { report_data })
783+
.await
784+
.context("Failed to get quote")?;
785+
let quote = serde_json::to_string(&response).context("Failed to serialize quote")?;
786+
safe_write(quote_path, &quote).context("Failed to write quote")?;
787+
Ok(quote)
788+
}
789+
728790
impl RpcCall<Proxy> for RpcHandler {
729791
type PrpcService = GatewayServer<Self>;
730792

gateway/src/proxy/tls_terminate.rs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use hyper_util::rt::tokio::TokioIo;
1313
use rustls::pki_types::pem::PemObject;
1414
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
1515
use rustls::version::{TLS12, TLS13};
16+
use serde::Serialize;
1617
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
1718
use tokio::net::TcpStream;
1819
use tokio::time::timeout;
@@ -124,6 +125,22 @@ pub(crate) fn create_acceptor(config: &ProxyConfig) -> Result<TlsAcceptor> {
124125
Ok(acceptor)
125126
}
126127

128+
fn json_response(body: &impl Serialize) -> Result<Response<String>> {
129+
let body = serde_json::to_string(body).context("Failed to serialize response")?;
130+
Response::builder()
131+
.status(StatusCode::OK)
132+
.header("Content-Type", "application/json")
133+
.body(body)
134+
.context("Failed to build response")
135+
}
136+
137+
fn empty_response(status: StatusCode) -> Result<Response<String>> {
138+
Response::builder()
139+
.status(status)
140+
.body(String::new())
141+
.context("Failed to build response")
142+
}
143+
127144
impl Proxy {
128145
/// Reload the TLS acceptor with fresh certificates
129146
pub fn reload_certificates(&self) -> Result<()> {
@@ -141,6 +158,61 @@ impl Proxy {
141158
Ok(())
142159
}
143160

161+
pub(crate) async fn handle_this_node(
162+
&self,
163+
inbound: TcpStream,
164+
buffer: Vec<u8>,
165+
port: u16,
166+
) -> Result<()> {
167+
if port != 80 {
168+
bail!("Only port 80 is supported for this node");
169+
}
170+
let stream = self.tls_accept(inbound, buffer).await?;
171+
let io = TokioIo::new(stream);
172+
173+
let service = service_fn(|req: Request<Incoming>| async move {
174+
// Only respond to GET / requests
175+
if req.method() != hyper::Method::GET {
176+
return empty_response(StatusCode::METHOD_NOT_ALLOWED);
177+
}
178+
if req.uri().path() == "/health" {
179+
return empty_response(StatusCode::OK);
180+
}
181+
let path = req.uri().path().trim_start_matches("/.dstack");
182+
match path {
183+
"/index" => {
184+
let body = serde_json::json!({
185+
"type": "dstack gateway",
186+
"paths": [
187+
"/index",
188+
"/app-info",
189+
"/acme-info",
190+
],
191+
});
192+
json_response(&body)
193+
}
194+
"/app-info" => {
195+
let agent = crate::dstack_agent().context("Failed to get dstack agent")?;
196+
let app_info = agent.info().await.context("Failed to get app info")?;
197+
json_response(&app_info)
198+
}
199+
"/acme-info" => {
200+
let acme_info = self.acme_info().await.context("Failed to get acme info")?;
201+
json_response(&acme_info)
202+
}
203+
_ => empty_response(StatusCode::NOT_FOUND),
204+
}
205+
});
206+
207+
http1::Builder::new()
208+
.serve_connection(io, service)
209+
.await
210+
.context("Failed to serve HTTP connection")?;
211+
212+
Ok(())
213+
}
214+
215+
/// Deprecated legacy endpoint
144216
pub(crate) async fn handle_health_check(
145217
&self,
146218
inbound: TcpStream,
@@ -158,17 +230,15 @@ impl Proxy {
158230
let service = service_fn(|req: Request<Incoming>| async move {
159231
// Only respond to GET / requests
160232
if req.method() != hyper::Method::GET || req.uri().path() != "/" {
161-
return Ok::<_, anyhow::Error>(
162-
Response::builder()
163-
.status(StatusCode::METHOD_NOT_ALLOWED)
164-
.body(String::new())
165-
.unwrap(),
166-
);
233+
return Response::builder()
234+
.status(StatusCode::NOT_FOUND)
235+
.body(String::new())
236+
.context("Failed to build response");
167237
}
168-
Ok(Response::builder()
238+
Response::builder()
169239
.status(StatusCode::OK)
170240
.body(String::new())
171-
.unwrap())
241+
.context("Failed to build response")
172242
});
173243

174244
http1::Builder::new()
@@ -214,6 +284,9 @@ impl Proxy {
214284
if app_id == "health" {
215285
return self.handle_health_check(inbound, buffer, port).await;
216286
}
287+
if app_id == "gateway" {
288+
return self.handle_this_node(inbound, buffer, port).await;
289+
}
217290
let addresses = self
218291
.lock()
219292
.select_top_n_hosts(app_id)

guest-agent/dstack.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ data_disks = ["/"]
1616
enabled = false
1717
quote_file = "quote.hex"
1818
event_log_file = "eventlog.json"
19+
sys_config_file = "sys-config.json"
1920

2021
[internal-v0]
2122
address = "unix:/var/run/tappd.sock"

guest-agent/rpc/proto/agent_rpc.proto

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ message GetQuoteResponse {
162162
string event_log = 2;
163163
// Report data
164164
bytes report_data = 3;
165+
// Hw config
166+
string vm_config = 4;
165167
}
166168

167169
message EmitEventArgs {

guest-agent/src/rpc_service.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,17 +212,23 @@ impl DstackGuestRpc for InternalRpcHandler {
212212
}
213213
let report_data = pad64(&request.report_data).context("Report data is too long")?;
214214
if self.state.config().simulator.enabled {
215-
return simulate_quote(self.state.config(), report_data);
215+
return simulate_quote(
216+
self.state.config(),
217+
report_data,
218+
&self.state.inner.vm_config,
219+
);
216220
}
217221
let (_, quote) =
218222
tdx_attest::get_quote(&report_data, None).context("Failed to get quote")?;
219223
let event_log = read_event_logs().context("Failed to decode event log")?;
220224
let event_log =
221225
serde_json::to_string(&event_log).context("Failed to serialize event log")?;
226+
222227
Ok(GetQuoteResponse {
223228
quote,
224229
event_log,
225230
report_data: report_data.to_vec(),
231+
vm_config: self.state.inner.vm_config.clone(),
226232
})
227233
}
228234

@@ -238,7 +244,11 @@ impl DstackGuestRpc for InternalRpcHandler {
238244
}
239245
}
240246

241-
fn simulate_quote(config: &Config, report_data: [u8; 64]) -> Result<GetQuoteResponse> {
247+
fn simulate_quote(
248+
config: &Config,
249+
report_data: [u8; 64],
250+
vm_config: &str,
251+
) -> Result<GetQuoteResponse> {
242252
let quote_file =
243253
fs::read_to_string(&config.simulator.quote_file).context("Failed to read quote file")?;
244254
let mut quote = hex::decode(quote_file.trim()).context("Failed to decode quote")?;
@@ -252,6 +262,7 @@ fn simulate_quote(config: &Config, report_data: [u8; 64]) -> Result<GetQuoteResp
252262
quote,
253263
event_log,
254264
report_data: report_data.to_vec(),
265+
vm_config: vm_config.to_string(),
255266
})
256267
}
257268

@@ -332,7 +343,11 @@ impl TappdRpc for InternalRpcHandlerV0 {
332343
let report_data =
333344
content_type.to_report_data_with_hash(&request.report_data, &request.hash_algorithm)?;
334345
if self.state.config().simulator.enabled {
335-
let response = simulate_quote(self.state.config(), report_data)?;
346+
let response = simulate_quote(
347+
self.state.config(),
348+
report_data,
349+
&self.state.inner.vm_config,
350+
)?;
336351
return Ok(TdxQuoteResponse {
337352
quote: response.quote,
338353
event_log: response.event_log,

0 commit comments

Comments
 (0)