From 444e50250e1b8ca77c30d5d8b0f762264d75404b Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 24 Sep 2026 02:01:14 -0700 Subject: [PATCH] fix(tpm-qvl): reject duplicate PCR indices before the event-log replay --- dstack/tpm-qvl/src/verify.rs | 72 +++++++++++++++++++++++++++++++----- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/dstack/tpm-qvl/src/verify.rs b/dstack/tpm-qvl/src/verify.rs index 9a81d0bb2..a44a4f6e7 100644 --- a/dstack/tpm-qvl/src/verify.rs +++ b/dstack/tpm-qvl/src/verify.rs @@ -10,6 +10,7 @@ use dstack_types::Platform; use p256::ecdsa::{signature::hazmat::PrehashVerifier, Signature, VerifyingKey}; use rsa::RsaPublicKey; use sha2::{Digest, Sha256}; +use std::collections::{HashMap, HashSet}; use tracing::{debug, warn}; use x509_parser::prelude::*; @@ -99,6 +100,15 @@ pub fn verify_quote_with_ca( }); } + // A TPM selects each PCR once; duplicates would make the unsigned replay below quadratic. + let mut seen = HashSet::new(); + if let Some(pcr) = quote.pcr_values.iter().find(|p| !seen.insert(p.index)) { + return Err(VerificationError { + status, + error: anyhow!("duplicate PCR index {} in quote", pcr.index), + }); + } + // compute_pcr_digest() and the event-log replay below assume the SHA-256 PCR // bank. Reject other banks explicitly instead of silently failing with a // confusing "PCR digest mismatch". @@ -355,19 +365,22 @@ fn compute_pcr_digest(pcr_values: &[PcrValue]) -> 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> { + let quoted: HashSet = pcr_values.iter().map(|p| p.index).collect(); + let mut events_by_pcr: HashMap> = HashMap::new(); + for event in event_log.iter().filter(|e| quoted.contains(&e.pcr_index)) { + events_by_pcr + .entry(event.pcr_index) + .or_default() + .push(event); + } for pcr in pcr_values { - let pcr_events: Vec<&TpmEvent> = event_log - .iter() - .filter(|e| e.pcr_index == pcr.index) - .collect(); - - if pcr_events.is_empty() { + let Some(pcr_events) = events_by_pcr.get(&pcr.index) else { continue; - } + }; // Replay PCR extension to verify Event Log matches quote let mut replayed_pcr = vec![0u8; 32]; - for event in &pcr_events { + for event in pcr_events { let mut hasher = Sha256::new(); hasher.update(&replayed_pcr); hasher.update(&event.digest); @@ -403,7 +416,7 @@ fn verify_event_log(pcr_values: &[PcrValue], event_log: &[TpmEvent]) -> Result>(), [0]); } + + #[test] + fn rejects_duplicate_pcr_indices() { + let value = vec![0xaa; 32]; + let values = vec![ + PcrValue { + index: 0, + algorithm: "sha256".into(), + value: value.clone(), + }, + PcrValue { + index: 0, + algorithm: "sha256".into(), + value: value.clone(), + }, + ]; + let digest = Sha256::digest([value.clone(), value].concat()).to_vec(); + // Two single-PCR selections both naming PCR 0. + let single = attest_message(&[0], 0x000b, &digest); + let selection = &single[39..44]; + let message = [ + &single[..35], + &2u32.to_be_bytes(), + selection, + selection, + &single[44..], + ] + .concat(); + let err = match verify_quote_with_ca( + "e_of(values, message), + &empty_collateral(), + GCP_ROOT_CA, + ) { + Ok(_) => panic!("duplicate PCR indices verified"), + Err(err) => err.error.to_string(), + }; + assert!( + err.contains("duplicate PCR index 0"), + "unexpected error: {err}" + ); + } }