Skip to content

Commit 88485ce

Browse files
committed
fix(attest): accept one vm_config, not competing copies
Signed-off-by: Kevin Wang <wy721@qq.com>
1 parent 3c87784 commit 88485ce

4 files changed

Lines changed: 58 additions & 88 deletions

File tree

‎dstack/dstack-attest/src/attestation.rs‎

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -419,23 +419,24 @@ fn find_event_payloads(runtime_events: &[RuntimeEvent], name: &str) -> Vec<Vec<u
419419
.collect()
420420
}
421421

422-
fn decode_vm_config_with_fallback(config: &str, fallback_config: &str) -> Result<VmConfig> {
423-
let config = if config.is_empty() {
424-
fallback_config
425-
} else {
426-
config
427-
};
428-
let config = if config.is_empty() { "{}" } else { config };
429-
let config = vm_config_json_from_config(config).unwrap_or(Cow::Borrowed(config));
430-
serde_json::from_str(&config).context("Failed to parse vm config")
422+
/// The VM config to decode: the caller-supplied copy or the one carried in the
423+
/// attestation. When both are given they must be identical, so no reader can
424+
/// authorize on one copy and decode another.
425+
pub fn resolve_vm_config<'a>(external: &'a str, embedded: &'a str) -> Result<&'a str> {
426+
if external.is_empty() {
427+
return Ok(embedded);
428+
}
429+
if !embedded.is_empty() && external != embedded {
430+
bail!("vm_config does not match the config carried in the attestation");
431+
}
432+
Ok(external)
431433
}
432434

433-
fn vm_config_json_from_config(config: &str) -> Option<Cow<'_, str>> {
434-
let value = serde_json::from_str::<serde_json::Value>(config).ok()?;
435-
value
436-
.get("vm_config")
437-
.and_then(|value| value.as_str())
438-
.map(|vm_config| Cow::Owned(vm_config.to_string()))
435+
fn decode_vm_config_with_fallback(config: &str, fallback_config: &str) -> Result<VmConfig> {
436+
let config = resolve_vm_config(config, fallback_config)?;
437+
// No vm config for nitro enclave
438+
let config = if config.is_empty() { "{}" } else { config };
439+
serde_json::from_str(config).context("Failed to parse vm config")
439440
}
440441

441442
fn mr_config_document_from_value(value: &serde_json::Value) -> Result<Option<String>> {
@@ -453,16 +454,7 @@ fn mr_config_document_from_config(config: &str) -> Result<Option<String>> {
453454
let Ok(value) = serde_json::from_str::<serde_json::Value>(config) else {
454455
return Ok(None);
455456
};
456-
if let Some(mr_config) = mr_config_document_from_value(&value)? {
457-
return Ok(Some(mr_config));
458-
}
459-
460-
let Some(vm_config) = value.get("vm_config").and_then(|value| value.as_str()) else {
461-
return Ok(None);
462-
};
463-
let vm_config = serde_json::from_str::<serde_json::Value>(vm_config)
464-
.context("Failed to parse nested vm_config for amd sev-snp mr_config")?;
465-
mr_config_document_from_value(&vm_config)
457+
mr_config_document_from_value(&value)
466458
}
467459

468460
pub use dstack_types::TeeVariant;
@@ -1685,20 +1677,18 @@ fn decode_app_info_sev_snp(
16851677
external_vm_config: &str,
16861678
) -> Result<AppInfo> {
16871679
let parsed = crate::amd_sev_snp::parse_unverified_amd_snp_report(report)?;
1680+
let config = resolve_vm_config(external_vm_config, embedded_config)?;
16881681
let mr_config_document = if let Some(mr_config) = mr_config {
16891682
Cow::Borrowed(mr_config)
1690-
} else if let Some(mr_config) = mr_config_document_from_config(external_vm_config)? {
1691-
Cow::Owned(mr_config)
1692-
} else if let Some(mr_config) = mr_config_document_from_config(embedded_config)? {
1683+
} else if let Some(mr_config) = mr_config_document_from_config(config)? {
16931684
Cow::Owned(mr_config)
16941685
} else {
16951686
bail!("amd sev-snp mr_config is missing");
16961687
};
16971688
let mr_config = verify_snp_mr_config_host_data(mr_config_document.as_ref(), &parsed.host_data)?;
16981689

16991690
let key_provider_info = key_provider_info_from_mr_config(&mr_config)?;
1700-
let os_image_hash =
1701-
decode_vm_config_with_fallback(external_vm_config, embedded_config)?.os_image_hash;
1691+
let os_image_hash = decode_vm_config_with_fallback(config, "")?.os_image_hash;
17021692
let mrs = decode_mr_sev_snp(&parsed.measurement, &parsed.host_data);
17031693

17041694
Ok(AppInfo {
@@ -1983,17 +1973,8 @@ impl<T: GetDeviceId> Attestation<T> {
19831973
}
19841974

19851975
/// Decode the VM config from the external or embedded config
1986-
pub fn decode_vm_config<'a>(&'a self, mut config: &'a str) -> Result<VmConfig> {
1987-
if config.is_empty() {
1988-
config = &self.config;
1989-
}
1990-
if config.is_empty() {
1991-
// No vm config for nitro enclave
1992-
config = "{}";
1993-
}
1994-
let vm_config: VmConfig =
1995-
serde_json::from_str(config).context("Failed to parse vm config")?;
1996-
Ok(vm_config)
1976+
pub fn decode_vm_config(&self, config: &str) -> Result<VmConfig> {
1977+
decode_vm_config_with_fallback(config, &self.config)
19971978
}
19981979

19991980
/// Decode the app info from the platform-specific app info source.

‎dstack/dstack-mr/src/sev.rs‎

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,38 +1077,17 @@ pub struct SnpLaunchInputs {
10771077
/// `mr_config` document out of a VMM `vm_config` string.
10781078
///
10791079
/// The fields are intentionally explicit so missing SNP launch inputs fail
1080-
/// closed instead of falling back to TDX event-log decoding. Both the top-level
1081-
/// shape and the legacy nested `vm_config` string shape are accepted.
1080+
/// closed instead of falling back to TDX event-log decoding.
10821081
pub fn parse_snp_inputs_from_vm_config(vm_config: &str) -> Result<SnpLaunchInputs> {
1083-
let value: serde_json::Value =
1082+
let parsed: SevSnpMeasurementVmConfig =
10841083
serde_json::from_str(vm_config).context("failed to parse vm_config for amd sev-snp")?;
1085-
let parsed: SevSnpMeasurementVmConfig = serde_json::from_value(value.clone())
1086-
.context("failed to parse vm_config for amd sev-snp")?;
1087-
let nested = value
1088-
.get("vm_config")
1089-
.and_then(|value| value.as_str())
1090-
.map(|vm_config| {
1091-
serde_json::from_str::<SevSnpMeasurementVmConfig>(vm_config)
1092-
.context("failed to parse nested vm_config for amd sev-snp")
1093-
})
1094-
.transpose()?;
10951084
let measurement_document = parsed
10961085
.sev_snp_measurement
1097-
.or_else(|| {
1098-
nested
1099-
.as_ref()
1100-
.and_then(|nested| nested.sev_snp_measurement.clone())
1101-
})
1102-
.ok_or_else(|| anyhow::anyhow!("sev_snp_measurement is required for amd sev-snp"))?;
1103-
let os_image_hash = if !parsed.os_image_hash.is_empty() {
1104-
parsed.os_image_hash
1105-
} else {
1106-
nested
1107-
.as_ref()
1108-
.map(|nested| nested.os_image_hash.clone())
1109-
.filter(|hash| !hash.is_empty())
1110-
.ok_or_else(|| anyhow::anyhow!("os_image_hash is required for amd sev-snp"))?
1111-
};
1086+
.context("sev_snp_measurement is required for amd sev-snp")?;
1087+
let os_image_hash = parsed.os_image_hash;
1088+
if os_image_hash.is_empty() {
1089+
bail!("os_image_hash is required for amd sev-snp");
1090+
}
11121091
let document: SnpMeasurementDocument = serde_json::from_str(&measurement_document)
11131092
.context("invalid amd sev-snp measurement document")?;
11141093
dstack_types::SevOsImageMeasurementDocument::new(
@@ -1122,8 +1101,7 @@ pub fn parse_snp_inputs_from_vm_config(vm_config: &str) -> Result<SnpLaunchInput
11221101
validate_measurement_input(&input)?;
11231102
let mr_config_document = parsed
11241103
.mr_config
1125-
.or_else(|| nested.and_then(|nested| nested.mr_config))
1126-
.ok_or_else(|| anyhow::anyhow!("mr_config is required for amd sev-snp"))?;
1104+
.context("mr_config is required for amd sev-snp")?;
11271105
MrConfigV3::from_document(&mr_config_document)
11281106
.context("invalid amd sev-snp mr_config document")?;
11291107
Ok(SnpLaunchInputs {

‎dstack/kms/src/main_service.rs‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use k256::ecdsa::SigningKey;
2424
use moka::future::Cache;
2525
use ra_rpc::{CallContext, RpcCall};
2626
use ra_tls::{
27-
attestation::{AttestationVerifier, TeeVariant, VerifiedAttestation},
27+
attestation::{resolve_vm_config, AttestationVerifier, TeeVariant, VerifiedAttestation},
2828
cert::{CaCert, CertRequest, CertSigningRequestV1, CertSigningRequestV2, Csr},
2929
kdf,
3030
};
@@ -257,14 +257,9 @@ pub(crate) fn build_boot_info_for_attestation(
257257
vm_config_str: &str,
258258
) -> Result<BootInfo> {
259259
let boot_info = if att.report.amd_snp_report().is_some() {
260-
let vm_config_str = if vm_config_str.is_empty() {
261-
att.config.as_str()
262-
} else {
263-
vm_config_str
264-
};
265260
amd_attest::build_amd_snp_boot_info_from_verified_attestation_and_vm_config(
266261
att,
267-
vm_config_str,
262+
resolve_vm_config(vm_config_str, &att.config)?,
268263
)?
269264
} else {
270265
build_boot_info(att, use_boottime_mr, vm_config_str)?
@@ -1159,6 +1154,28 @@ mod tests {
11591154
assert_eq!(boot_info.app_id, vec![0x11; 20]);
11601155
}
11611156

1157+
#[test]
1158+
fn build_boot_info_for_attestation_rejects_a_vm_config_differing_from_the_attested_one() {
1159+
let input = valid_snp_measurement_input();
1160+
let measurement = compute_expected_measurement(&input).unwrap();
1161+
let mr_config = valid_snp_mr_config();
1162+
let vm_config = snp_vm_config(&input, &mr_config);
1163+
let attestation = verified_snp_attestation_with_config(
1164+
measurement,
1165+
[0xab; 64],
1166+
vm_config.clone(),
1167+
&mr_config,
1168+
);
1169+
let mut request: serde_json::Value = serde_json::from_str(&vm_config).unwrap();
1170+
request["cpu_count"] = 8.into();
1171+
1172+
let err = build_boot_info_for_attestation(&attestation, false, &request.to_string())
1173+
.expect_err("a vm_config differing from the attested one must be rejected");
1174+
assert!(format!("{err:#}").contains("does not match the config carried"));
1175+
build_boot_info_for_attestation(&attestation, false, &vm_config)
1176+
.expect("an identical copy is accepted");
1177+
}
1178+
11621179
#[test]
11631180
fn build_boot_info_for_attestation_accepts_self_contained_snp_input_without_config() {
11641181
let input = valid_snp_measurement_input();

‎dstack/verifier/src/verification.rs‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ use dstack_mr::{tdx::TdxRtmr0AcpiHashes, TdxMeasurements};
2222
use dstack_types::{sha256sum, TdxAttestationVariant, VmConfig};
2323
use hex_literal::hex;
2424
use ra_tls::attestation::{
25-
AppInfo, Attestation, AttestationQuote, AttestationVerifier, DstackVerifiedReport, NitroPcrs,
26-
VerifiedAttestation, VersionedAttestation,
25+
resolve_vm_config, AppInfo, Attestation, AttestationQuote, AttestationVerifier,
26+
DstackVerifiedReport, NitroPcrs, VerifiedAttestation, VersionedAttestation,
2727
};
2828
use serde::{Deserialize, Serialize};
2929
use sha2::{Digest as _, Sha256};
@@ -764,14 +764,8 @@ impl CvmVerifier {
764764
attestation: &VerifiedAttestation,
765765
details: &mut VerificationDetails,
766766
) -> Result<VmConfig> {
767-
// The raw config string used for platform-specific binding: the explicit
768-
// request `vm_config` when supplied, otherwise the one embedded in the
769-
// attestation (mirroring `decode_vm_config`'s own fallback).
770-
let raw_config = if vm_config.is_empty() {
771-
attestation.config.clone()
772-
} else {
773-
vm_config.clone()
774-
};
767+
// The raw config string used for platform-specific binding.
768+
let raw_config = resolve_vm_config(&vm_config, &attestation.config)?.to_string();
775769
let mut vm_config = attestation
776770
.decode_vm_config(&vm_config)
777771
.context("Failed to decode VM config")?;

0 commit comments

Comments
 (0)