Skip to content

Commit 9c50e19

Browse files
authored
Merge pull request #727 from Dstack-TEE/feat/os-image-dev-prod-indicator
feat(verifier): report dev/prod OS image and deployment identity in result
2 parents 56151e3 + 9813355 commit 9c50e19

5 files changed

Lines changed: 85 additions & 29 deletions

File tree

dstack-types/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ pub struct ImageInfo {
309309
/// may omit it, so callers should treat its absence as "unknown".
310310
#[serde(default)]
311311
pub version: String,
312+
/// dev vs prod image. absent in older metadata.json => prod.
313+
#[serde(default)]
314+
pub is_dev: bool,
312315
/// Optional OVMF measurement layout declared by the image. Older
313316
/// metadata.json files do not carry this — treat absence as "unknown" and
314317
/// fall back to version-based heuristics.

verifier/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ or
3333
"quote_verified": true,
3434
"event_log_verified": true, // See "Verification Process" for semantics
3535
"os_image_hash_verified": true,
36+
"os_image_is_dev": false, // true=dev image, false=prod, null=unknown/N/A
37+
"os_image_version": "0.5.10", // dstack OS version, null if unknown
38+
"tee_platform": "tdx", // tdx | gcp-tdx | nitro
3639
"report_data": "hex-encoded-64-byte-report-data",
3740
"tcb_status": "UpToDate",
3841
"advisory_ids": [],
42+
"key_provider": { "name": "kms", "id": "hex-string" }, // decoded; null if absent
3943
"app_info": {
4044
"app_id": "hex-string",
4145
"compose_hash": "hex-string",
@@ -185,3 +189,14 @@ The verifier performs three main verification steps:
185189
- Compares against the verified measurements from the quote
186190

187191
All three steps must pass for the verification to be considered valid.
192+
193+
### Identifying the deployment
194+
195+
Beyond pass/fail, the result carries a few descriptive fields so a relying party can apply its own policy:
196+
197+
- **`os_image_is_dev`** — `true` for a development OS image, `false` for production. Dev images are built for local testing and are not hardened for production use, so a relying party generally wants to reject them.
198+
- **`os_image_version`** — the dstack OS version (e.g. `0.5.10`), useful for enforcing a minimum version.
199+
- **`tee_platform`** — which TEE produced the verified quote: `tdx`, `gcp-tdx`, or `nitro`.
200+
- **`key_provider`** — the decoded `app_info.key_provider_info` (`{name, id}`); `name` is e.g. `kms` or `local`. A `local` key provider means the CVM is not KMS-backed, which is itself a dev/insecure posture signal. The raw bytes remain in `app_info.key_provider_info`.
201+
202+
`os_image_is_dev` and `os_image_version` are read from the image's `metadata.json`, which is part of `sha256sum.txt` and therefore bound to the `os_image_hash` that step 3 verifies against the quote — so they are as trustworthy as the os-image-hash check itself. They are `null` when the platform does not expose them (e.g. GCP TDX / Nitro Enclave) or when the image predates the field (images without `is_dev` are always production).

verifier/src/main.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,7 @@ async fn verify_cvm(
5050
error!("Verification failed: {:?}", e);
5151
Json(VerificationResponse {
5252
is_valid: false,
53-
details: VerificationDetails {
54-
quote_verified: false,
55-
event_log_verified: false,
56-
os_image_hash_verified: false,
57-
report_data: None,
58-
tcb_status: None,
59-
advisory_ids: vec![],
60-
app_info: None,
61-
acpi_tables: None,
62-
rtmr_debug: None,
63-
},
53+
details: VerificationDetails::default(),
6454
reason: Some(format!("Internal error: {}", e)),
6555
})
6656
}

verifier/src/types.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5+
use dstack_types::KeyProviderInfo;
56
use ra_tls::attestation::AppInfo;
67
use serde::{Deserialize, Serialize};
78

@@ -40,9 +41,17 @@ pub struct VerificationDetails {
4041
/// event log payloads.
4142
pub event_log_verified: bool,
4243
pub os_image_hash_verified: bool,
44+
/// dev vs prod OS image, from metadata.json (bound to os_image_hash). None if not exposed.
45+
pub os_image_is_dev: Option<bool>,
46+
/// dstack OS version, from the same metadata.json.
47+
pub os_image_version: Option<String>,
48+
/// "tdx" | "gcp-tdx" | "nitro".
49+
pub tee_platform: Option<String>,
4350
pub report_data: Option<String>,
4451
pub tcb_status: Option<String>,
4552
pub advisory_ids: Vec<String>,
53+
/// decoded app_info.key_provider_info; name is e.g. "kms" or "local".
54+
pub key_provider: Option<KeyProviderInfo>,
4655
pub app_info: Option<AppInfo>,
4756
#[serde(skip_serializing_if = "Option::is_none")]
4857
pub acpi_tables: Option<AcpiTables>,

verifier/src/verification.rs

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ use crate::types::{
2727
VerificationRequest, VerificationResponse,
2828
};
2929

30+
fn tee_platform_name(quote: &AttestationQuote) -> &'static str {
31+
match quote {
32+
AttestationQuote::DstackTdx(_) => "tdx",
33+
AttestationQuote::DstackGcpTdx(_) => "gcp-tdx",
34+
AttestationQuote::DstackNitroEnclave(_) => "nitro",
35+
}
36+
}
37+
38+
/// best-effort: None for empty/malformed blobs.
39+
fn decode_key_provider_info(bytes: &[u8]) -> Option<dstack_types::KeyProviderInfo> {
40+
if bytes.is_empty() {
41+
return None;
42+
}
43+
serde_json::from_slice(bytes).ok()
44+
}
45+
3046
fn collect_rtmr_mismatch(
3147
rtmr_label: &str,
3248
expected: &[u8],
@@ -136,6 +152,8 @@ struct ImagePaths {
136152
kernel_path: PathBuf,
137153
initrd_path: PathBuf,
138154
kernel_cmdline: String,
155+
is_dev: bool,
156+
version: String,
139157
}
140158

141159
pub struct CvmVerifier {
@@ -376,6 +394,8 @@ impl CvmVerifier {
376394
kernel_path,
377395
initrd_path,
378396
kernel_cmdline,
397+
is_dev: image_info.is_dev,
398+
version: image_info.version,
379399
})
380400
}
381401

@@ -413,23 +433,14 @@ impl CvmVerifier {
413433
} else {
414434
bail!("Quote is required");
415435
};
416-
let mut details = VerificationDetails {
417-
quote_verified: false,
418-
event_log_verified: false,
419-
os_image_hash_verified: false,
420-
report_data: None,
421-
tcb_status: None,
422-
advisory_ids: vec![],
423-
app_info: None,
424-
acpi_tables: None,
425-
rtmr_debug: None,
426-
};
436+
let mut details = VerificationDetails::default();
427437

428438
let debug = request.debug.unwrap_or(false);
429439
let verified = attestation.into_v1().verify(self.pccs_url.as_deref()).await;
430440
let verified_attestation = match verified {
431441
Ok(att) => {
432442
details.quote_verified = true;
443+
details.tee_platform = Some(tee_platform_name(&att.quote).to_string());
433444
details.tcb_status = att.report.tdx_report().map(|r| r.status.clone());
434445
details.advisory_ids = att
435446
.report
@@ -471,6 +482,7 @@ impl CvmVerifier {
471482
Ok(mut info) => {
472483
info.os_image_hash = vm_config.os_image_hash;
473484
details.event_log_verified = true;
485+
details.key_provider = decode_key_provider_info(&info.key_provider_info);
474486
details.app_info = Some(info);
475487
}
476488
Err(e) => {
@@ -546,11 +558,16 @@ impl CvmVerifier {
546558
rtmr2: report.rt_mr2.to_vec(),
547559
};
548560

549-
// Compute expected measurements (reusing the public API)
561+
// one download serves both measurement computation and the dev/version flags
562+
let image_paths = self.ensure_image_downloaded(vm_config).await?;
563+
details.os_image_is_dev = Some(image_paths.is_dev);
564+
if !image_paths.version.is_empty() {
565+
details.os_image_version = Some(image_paths.version.clone());
566+
}
567+
568+
// Compute expected measurements
550569
let (mrs, expected_logs) = if debug {
551570
// For debug mode, we need detailed logs and ACPI tables
552-
let image_paths = self.ensure_image_downloaded(vm_config).await?;
553-
554571
let TdxMeasurementDetails {
555572
measurements,
556573
rtmr_logs,
@@ -573,11 +590,16 @@ impl CvmVerifier {
573590

574591
(measurements, Some(rtmr_logs))
575592
} else {
576-
// For non-debug mode, reuse the public API with caching
593+
// For non-debug mode, use the cached-measurement path.
577594
(
578-
self.compute_measurements_for_config(vm_config)
579-
.await
580-
.context("Failed to compute expected measurements")?,
595+
self.load_or_compute_measurements(
596+
vm_config,
597+
&image_paths.fw_path,
598+
&image_paths.kernel_path,
599+
&image_paths.initrd_path,
600+
&image_paths.kernel_cmdline,
601+
)
602+
.context("Failed to compute expected measurements")?,
581603
None,
582604
)
583605
};
@@ -885,3 +907,20 @@ impl Mrs {
885907
Ok(())
886908
}
887909
}
910+
911+
#[cfg(test)]
912+
mod tests {
913+
use super::*;
914+
915+
#[test]
916+
fn decode_key_provider_info_parses_json_and_tolerates_garbage() {
917+
let info =
918+
decode_key_provider_info(br#"{"name":"kms","id":"abcd"}"#).expect("should parse");
919+
assert_eq!(info.name, "kms");
920+
assert_eq!(info.id, "abcd");
921+
922+
// empty/malformed must degrade to None, not fail the verify.
923+
assert!(decode_key_provider_info(b"").is_none());
924+
assert!(decode_key_provider_info(b"not json").is_none());
925+
}
926+
}

0 commit comments

Comments
 (0)