diff --git a/dstack/tpm-qvl/src/verify.rs b/dstack/tpm-qvl/src/verify.rs index 04bda0329..9a81d0bb2 100644 --- a/dstack/tpm-qvl/src/verify.rs +++ b/dstack/tpm-qvl/src/verify.rs @@ -25,6 +25,9 @@ pub struct VerifiedReport { pub attest: TpmAttest, pub platform: Platform, pub pcr_values: Vec, + /// Event-log entries of the quoted PCRs, all replayed against `pcr_values`. + /// Entries of unquoted PCRs are not attested and are dropped. + pub event_log: Vec, } impl VerifiedReport { @@ -160,10 +163,11 @@ pub fn verify_quote_with_ca( }); } - verify_event_log("e.pcr_values, "e.event_log).map_err(|e| VerificationError { - status: status.clone(), - error: e.context("event log verification failed"), - })?; + let event_log = + verify_event_log("e.pcr_values, "e.event_log).map_err(|e| VerificationError { + status: status.clone(), + error: e.context("event log verification failed"), + })?; debug!("✓ Event Log replay verification successful"); status.pcr_verified = true; @@ -211,6 +215,7 @@ pub fn verify_quote_with_ca( attest, platform: quote.platform, pcr_values: quote.pcr_values.clone(), + event_log, }) } @@ -347,7 +352,9 @@ fn compute_pcr_digest(pcr_values: &[PcrValue]) -> Result> { Ok(hasher.finalize().to_vec()) } -fn verify_event_log(pcr_values: &[PcrValue], event_log: &[TpmEvent]) -> Result<()> { +/// Replay the event log against the quoted PCR values and return the replayed +/// entries. +fn verify_event_log(pcr_values: &[PcrValue], event_log: &[TpmEvent]) -> Result> { for pcr in pcr_values { let pcr_events: Vec<&TpmEvent> = event_log .iter() @@ -394,7 +401,11 @@ fn verify_event_log(pcr_values: &[PcrValue], event_log: &[TpmEvent]) -> Result<( } } - Ok(()) + Ok(event_log + .iter() + .filter(|e| pcr_values.iter().any(|p| p.index == e.pcr_index)) + .cloned() + .collect()) } fn extract_ak_public_key_from_cert(ak_cert_der: &[u8]) -> Result { @@ -843,4 +854,20 @@ mod tests { "unexpected error: {err}" ); } + + #[test] + fn event_log_keeps_only_quoted_pcrs() { + let event = |pcr_index, digest: u8| TpmEvent { + pcr_index, + digest: vec![digest; 32], + }; + let pcr0 = PcrValue { + index: 0, + algorithm: "sha256".into(), + value: Sha256::digest([[0u8; 32], [1u8; 32]].concat()).to_vec(), + }; + let log = [event(0, 1), event(2, 2), event(7, 3)]; + let kept = verify_event_log(&[pcr0], &log).unwrap(); + assert_eq!(kept.iter().map(|e| e.pcr_index).collect::>(), [0]); + } } diff --git a/dstack/verifier/src/verification.rs b/dstack/verifier/src/verification.rs index 77752d4fc..91442b3cd 100644 --- a/dstack/verifier/src/verification.rs +++ b/dstack/verifier/src/verification.rs @@ -22,11 +22,12 @@ use dstack_types::{TdxAttestationVariant, VmConfig}; use hex_literal::hex; use ra_tls::attestation::{ AppInfo, Attestation, AttestationQuote, AttestationVerifier, DstackVerifiedReport, NitroPcrs, - TpmQuote, VerifiedAttestation, VersionedAttestation, + VerifiedAttestation, VersionedAttestation, }; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; use tokio::{io::AsyncWriteExt, process::Command}; +use tpm_qvl::verify::VerifiedReport as TpmVerifiedReport; use tracing::{debug, info, warn}; use crate::types::{ @@ -677,8 +678,12 @@ impl CvmVerifier { .decode_vm_config(&vm_config) .context("Failed to decode VM config")?; match &attestation.quote { - AttestationQuote::DstackGcpTdx(quote) => { - self.verify_os_image_hash_for_gcp_tdx(&vm_config, "e.tpm_quote)?; + AttestationQuote::DstackGcpTdx(_) => { + let DstackVerifiedReport::DstackGcpTdx { tpm_report, .. } = &attestation.report + else { + bail!("GCP TDX quote without a GCP TDX report"); + }; + self.verify_os_image_hash_for_gcp_tdx(&vm_config, tpm_report)?; } // The declared scheme alone selects the path, matched exhaustively // so a new variant fails the build here instead of taking one. @@ -925,17 +930,13 @@ impl CvmVerifier { fn verify_os_image_hash_for_gcp_tdx( &self, vm_config: &VmConfig, - tpm_quote: &TpmQuote, + tpm_report: &TpmVerifiedReport, ) -> Result<()> { // Verify PCR 0 (GCP OVMF firmware) const EXPECTED_PCR0: [u8; 32] = hex!("0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802"); - let pcr0 = tpm_quote - .pcr_values - .iter() - .find(|p| p.index == 0) - .context("PCR 0 not found in TPM quote")?; + let pcr0 = tpm_report.get_pcr(0)?; let document = vm_config .gcp_measurement @@ -951,7 +952,7 @@ impl CvmVerifier { .context("failed to decode vm_config.gcp_measurement CBOR")?; let expected_uki_hash = &measurement.uki_authenticode_sha256; - let pcr2_events: Vec<_> = tpm_quote + let pcr2_events: Vec<_> = tpm_report .event_log .iter() .filter(|e| e.pcr_index == 2) @@ -960,10 +961,10 @@ impl CvmVerifier { // Extract Event 28 (3rd event, 0-indexed as 2) // NOTE: This is GCP OVMF-specific behavior let event_28_digest = { - if pcr0.value != EXPECTED_PCR0 { + if pcr0 != EXPECTED_PCR0 { bail!( "PCR 0 mismatch: expected GCP OVMF v2, got {}", - hex::encode(&pcr0.value) + hex::encode(&pcr0) ); } &pcr2_events.get(2).context("Event 28 not found")?.digest @@ -1133,9 +1134,9 @@ mod tests { use dstack_attest::amd_sev_snp::{AmdSnpTcbInfo, VerifiedAmdSnpReport}; use ra_tls::attestation::{ AwsNitroTpmVerifiedReport, DstackAwsNitroTpmQuote, DstackGcpTdxQuote, DstackNitroQuote, - NitroVerifiedReport, SnpQuote, TdxQuote, + NitroVerifiedReport, SnpQuote, TdxQuote, TpmQuote, }; - use tpm_qvl::verify::{ClockInfo, QuoteInfo, TpmAttest, VerifiedReport as TpmVerifiedReport}; + use tpm_qvl::verify::{ClockInfo, QuoteInfo, TpmAttest}; fn aws_boot_pcrs(pcr4: u8) -> BTreeMap> { BTreeMap::from([ @@ -1240,6 +1241,7 @@ mod tests { }, platform: dstack_types::Platform::Gcp, pcr_values: Vec::new(), + event_log: Vec::new(), } } @@ -1469,16 +1471,12 @@ mod tests { .unwrap(); let expected_pcr0 = hex!("0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802"); - let gcp_quote = |pcr0: Vec, event_28: Vec| TpmQuote { - message: Vec::new(), - signature: Vec::new(), + let gcp_report = |pcr0: Vec, event_28: Vec| TpmVerifiedReport { pcr_values: vec![tpm_types::PcrValue { index: 0, algorithm: "sha256".into(), value: pcr0, }], - ak_cert: Vec::new(), - platform: dstack_types::Platform::Gcp, event_log: vec![ tpm_types::TpmEvent { pcr_index: 2, @@ -1493,23 +1491,24 @@ mod tests { digest: event_28, }, ], + ..tpm_report_for_gcp() }; verifier .verify_os_image_hash_for_gcp_tdx( &gcp_config, - &gcp_quote(expected_pcr0.to_vec(), uki_hash.clone()), + &gcp_report(expected_pcr0.to_vec(), uki_hash.clone()), ) .unwrap(); assert!(verifier .verify_os_image_hash_for_gcp_tdx( &gcp_config, - &gcp_quote(vec![0; 32], uki_hash.clone()), + &gcp_report(vec![0; 32], uki_hash.clone()), ) .is_err()); assert!(verifier .verify_os_image_hash_for_gcp_tdx( &gcp_config, - &gcp_quote(expected_pcr0.to_vec(), vec![0; 32]), + &gcp_report(expected_pcr0.to_vec(), vec![0; 32]), ) .is_err()); let mut missing_document = gcp_config; @@ -1517,7 +1516,7 @@ mod tests { assert!(verifier .verify_os_image_hash_for_gcp_tdx( &missing_document, - &gcp_quote(expected_pcr0.to_vec(), uki_hash), + &gcp_report(expected_pcr0.to_vec(), uki_hash), ) .is_err()); }