Skip to main content

dcap_qvl/
tcb_info.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::cmp::Ordering;
4use derive_more::Display;
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "borsh_schema")]
8use borsh::BorshSchema;
9#[cfg(feature = "borsh")]
10use borsh::{BorshDeserialize, BorshSerialize};
11
12#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
15#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
16pub struct TcbInfo {
17    pub id: String,
18    pub version: u8,
19    pub issue_date: String,
20    pub next_update: String,
21    pub fmspc: String,
22    pub pce_id: String,
23    pub tcb_type: u32,
24    pub tcb_evaluation_data_number: u32,
25    pub tcb_levels: Vec<TcbLevel>,
26    #[serde(default)]
27    pub tdx_module: Option<TdxModule>,
28    #[serde(rename = "tdxModuleIdentities", default)]
29    pub tdx_module_identities: Vec<TdxModuleIdentity>,
30}
31
32impl TcbInfo {
33    /// Canonicalize `tcb_levels` ordering to match Intel QVL.
34    ///
35    /// Intel's QVL does not rely on the JSON order of `tcbLevels`. Instead, it
36    /// inserts levels into a sorted container using a custom comparator:
37    ///
38    /// - First by SGX CPU SVN components (lexicographically, highest first)
39    /// - Then by PCE SVN (highest first)
40    /// - For TDX TCB Info (version >= 3, id == "TDX"), by TDX TCB components
41    ///   as a final tiebreaker (lexicographically, highest first)
42    ///
43    /// This function mirrors that behavior so that matching logic operates on a
44    /// stable, implementation-defined order rather than whatever the PCS JSON
45    /// happens to contain.
46    pub(crate) fn canonicalize_tcb_levels(&mut self) {
47        let is_tdx = self.version >= 3 && self.id == "TDX";
48        self.tcb_levels
49            .sort_by(|a, b| compare_tcb_levels(a, b, is_tdx));
50    }
51}
52
53#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
56#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
57pub struct TcbLevel {
58    pub tcb: Tcb,
59    pub tcb_date: String,
60    pub tcb_status: TcbStatus,
61    #[serde(rename = "advisoryIDs", default)]
62    pub advisory_ids: Vec<String>,
63}
64
65fn compare_tcb_levels(a: &TcbLevel, b: &TcbLevel, is_tdx: bool) -> Ordering {
66    // Primary key: SGX CPU SVN components (lexicographically, highest first)
67    b.tcb
68        .sgx_components
69        .cmp(&a.tcb.sgx_components)
70        // Then by PCE SVN (highest first)
71        .then_with(|| b.tcb.pce_svn.cmp(&a.tcb.pce_svn))
72        // For TDX, refine by TDX TCB components as final tiebreaker
73        .then_with(|| {
74            if is_tdx {
75                b.tcb.tdx_components.cmp(&a.tcb.tdx_components)
76            } else {
77                Ordering::Equal
78            }
79        })
80}
81
82#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
85#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
86pub struct Tcb {
87    #[serde(rename = "sgxtcbcomponents")]
88    pub sgx_components: Vec<TcbComponents>,
89    #[serde(rename = "tdxtcbcomponents", default)]
90    pub tdx_components: Vec<TcbComponents>,
91    #[serde(rename = "pcesvn")]
92    pub pce_svn: u16,
93}
94
95#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
98#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
99pub struct TcbComponents {
100    pub svn: u8,
101}
102
103#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
106#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
107pub struct TdxModule {
108    /// Expected TDX module MRSIGNER as hex string
109    pub mrsigner: String,
110    /// Expected SEAMATTRIBUTES value as hex string
111    pub attributes: String,
112    /// Mask to apply when comparing SEAMATTRIBUTES, as hex string
113    pub attributes_mask: String,
114}
115
116#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
119#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
120pub struct TdxModuleIdentity {
121    pub id: String,
122    pub mrsigner: String,
123    pub attributes: String,
124    pub attributes_mask: String,
125    pub tcb_levels: Vec<TdxModuleTcbLevel>,
126}
127
128#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
131#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
132pub struct TdxModuleTcbLevel {
133    pub tcb: TdxModuleTcb,
134    pub tcb_date: String,
135    pub tcb_status: TcbStatus,
136    #[serde(rename = "advisoryIDs", default)]
137    pub advisory_ids: Vec<String>,
138}
139
140#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
141#[serde(rename_all = "camelCase")]
142#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
143#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
144pub struct TdxModuleTcb {
145    pub isvsvn: u8,
146}
147
148#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, Display)]
149#[display("{_variant}")]
150#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
151#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
152pub enum TcbStatus {
153    UpToDate,
154    OutOfDateConfigurationNeeded,
155    OutOfDate,
156    ConfigurationAndSWHardeningNeeded,
157    ConfigurationNeeded,
158    SWHardeningNeeded,
159    Revoked,
160}
161
162impl TcbStatus {
163    fn severity(&self) -> u8 {
164        match self {
165            Self::UpToDate => 0,
166            Self::SWHardeningNeeded => 1,
167            Self::ConfigurationNeeded => 2,
168            Self::ConfigurationAndSWHardeningNeeded => 3,
169            Self::OutOfDate => 4,
170            Self::OutOfDateConfigurationNeeded => 5,
171            Self::Revoked => 6,
172        }
173    }
174
175    /// Converge a platform status with a QE or TDX module status using Intel's
176    /// appraisal rule for an out-of-date component on a configured platform.
177    pub(crate) fn converge_with_component(self, component: TcbStatus) -> TcbStatus {
178        use TcbStatus::*;
179        match (component, self) {
180            (OutOfDate, ConfigurationNeeded | ConfigurationAndSWHardeningNeeded) => {
181                OutOfDateConfigurationNeeded
182            }
183            _ => component.max(self),
184        }
185    }
186}
187
188impl Ord for TcbStatus {
189    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
190        self.severity().cmp(&other.severity())
191    }
192}
193
194impl PartialOrd for TcbStatus {
195    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
196        Some(self.cmp(other))
197    }
198}
199
200/// TCB status with advisory IDs
201///
202/// This is the result of matching a TCB level, used by both
203/// platform TCB matching and QE Identity verification.
204#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
205#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
206#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
207pub struct TcbStatusWithAdvisory {
208    pub status: TcbStatus,
209    pub advisory_ids: Vec<String>,
210}
211
212impl TcbStatusWithAdvisory {
213    /// Create a new TcbStatus with the given status and advisory IDs
214    pub fn new(status: TcbStatus, advisory_ids: Vec<String>) -> Self {
215        Self {
216            status,
217            advisory_ids,
218        }
219    }
220
221    /// Merge a platform status with a QE status using Intel QVL convergence rules.
222    pub fn merge(self, other: &TcbStatusWithAdvisory) -> Self {
223        let final_status = self.status.converge_with_component(other.status);
224
225        let mut advisory_ids = self.advisory_ids;
226        for id in &other.advisory_ids {
227            if !advisory_ids.contains(id) {
228                advisory_ids.push(id.clone());
229            }
230        }
231
232        Self {
233            status: final_status,
234            advisory_ids,
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use TcbStatus::*;
243
244    #[test]
245    fn test_tcb_status_merge_both_up_to_date() {
246        let a = TcbStatusWithAdvisory::new(UpToDate, vec![]);
247        let b = TcbStatusWithAdvisory::new(UpToDate, vec![]);
248        let result = a.merge(&b);
249        assert_eq!(result.status, UpToDate);
250        assert!(result.advisory_ids.is_empty());
251    }
252
253    #[test]
254    fn test_tcb_status_merge_takes_worse() {
255        let a = TcbStatusWithAdvisory::new(UpToDate, vec![]);
256        let b = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]);
257        let result = a.merge(&b);
258        assert_eq!(result.status, OutOfDate);
259        assert_eq!(result.advisory_ids, vec!["INTEL-SA-00001"]);
260    }
261
262    #[test]
263    fn qe_out_of_date_converges_configuration_status() {
264        let platform = TcbStatusWithAdvisory::new(TcbStatus::ConfigurationNeeded, vec![]);
265        let qe = TcbStatusWithAdvisory::new(TcbStatus::OutOfDate, vec![]);
266        assert_eq!(
267            platform.merge(&qe).status,
268            TcbStatus::OutOfDateConfigurationNeeded
269        );
270    }
271
272    #[test]
273    fn test_tcb_status_merge_combines_advisories() {
274        let a = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]);
275        let b = TcbStatusWithAdvisory::new(SWHardeningNeeded, vec!["INTEL-SA-00002".into()]);
276        let result = a.merge(&b);
277        assert_eq!(result.status, OutOfDate);
278        assert_eq!(
279            result.advisory_ids,
280            vec!["INTEL-SA-00001", "INTEL-SA-00002"]
281        );
282    }
283
284    #[test]
285    fn test_tcb_status_merge_deduplicates_advisories() {
286        let a = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]);
287        let b = TcbStatusWithAdvisory::new(OutOfDate, vec!["INTEL-SA-00001".into()]);
288        let result = a.merge(&b);
289        assert_eq!(result.advisory_ids, vec!["INTEL-SA-00001"]);
290    }
291
292    fn make_tcb_level(sgx: &[u8], pce_svn: u16, tdx: &[u8], status: TcbStatus) -> TcbLevel {
293        TcbLevel {
294            tcb: Tcb {
295                sgx_components: sgx.iter().map(|&svn| TcbComponents { svn }).collect(),
296                tdx_components: tdx.iter().map(|&svn| TcbComponents { svn }).collect(),
297                pce_svn,
298            },
299            tcb_date: String::new(),
300            tcb_status: status,
301            advisory_ids: vec![],
302        }
303    }
304
305    fn make_tcb_info(id: &str, tcb_levels: Vec<TcbLevel>) -> TcbInfo {
306        TcbInfo {
307            id: id.into(),
308            version: 3,
309            issue_date: String::new(),
310            next_update: String::new(),
311            fmspc: String::new(),
312            pce_id: String::new(),
313            tcb_type: 0,
314            tcb_evaluation_data_number: 0,
315            tcb_levels,
316            tdx_module: None,
317            tdx_module_identities: vec![],
318        }
319    }
320
321    #[allow(clippy::expect_used)]
322    fn first_component_svn(components: &[TcbComponents]) -> u8 {
323        components
324            .first()
325            .expect("expected at least one TCB component")
326            .svn
327    }
328
329    #[test]
330    fn test_canonicalize_sgx_sorts_by_cpu_svn_desc() {
331        let mut info = make_tcb_info(
332            "SGX",
333            vec![
334                make_tcb_level(&[2, 0], 10, &[], UpToDate),
335                make_tcb_level(&[5, 0], 10, &[], UpToDate),
336                make_tcb_level(&[3, 0], 10, &[], UpToDate),
337            ],
338        );
339        info.canonicalize_tcb_levels();
340        let svns: Vec<u8> = info
341            .tcb_levels
342            .iter()
343            .map(|l| first_component_svn(&l.tcb.sgx_components))
344            .collect();
345        assert_eq!(svns, vec![5, 3, 2]);
346    }
347
348    #[test]
349    fn test_canonicalize_sgx_pce_svn_tiebreaker() {
350        let mut info = make_tcb_info(
351            "SGX",
352            vec![
353                make_tcb_level(&[5], 7, &[], UpToDate),
354                make_tcb_level(&[5], 12, &[], UpToDate),
355                make_tcb_level(&[5], 9, &[], UpToDate),
356            ],
357        );
358        info.canonicalize_tcb_levels();
359        let pce_svns: Vec<u16> = info.tcb_levels.iter().map(|l| l.tcb.pce_svn).collect();
360        assert_eq!(pce_svns, vec![12, 9, 7]);
361    }
362
363    #[test]
364    fn test_canonicalize_tdx_components_tiebreaker() {
365        let mut info = make_tcb_info(
366            "TDX",
367            vec![
368                make_tcb_level(&[5], 10, &[1, 0], UpToDate),
369                make_tcb_level(&[5], 10, &[3, 0], UpToDate),
370                make_tcb_level(&[5], 10, &[2, 0], UpToDate),
371            ],
372        );
373        info.canonicalize_tcb_levels();
374        let tdx_svns: Vec<u8> = info
375            .tcb_levels
376            .iter()
377            .map(|l| first_component_svn(&l.tcb.tdx_components))
378            .collect();
379        assert_eq!(tdx_svns, vec![3, 2, 1]);
380    }
381
382    #[test]
383    fn test_canonicalize_sgx_ignores_tdx_components() {
384        let mut info = make_tcb_info(
385            "SGX",
386            vec![
387                make_tcb_level(&[5], 10, &[1], UpToDate),
388                make_tcb_level(&[5], 10, &[9], UpToDate),
389            ],
390        );
391        info.canonicalize_tcb_levels();
392        // For SGX, tdx_components should NOT break the tie — order is stable
393        let tdx_svns: Vec<u8> = info
394            .tcb_levels
395            .iter()
396            .map(|l| first_component_svn(&l.tcb.tdx_components))
397            .collect();
398        assert_eq!(tdx_svns, vec![1, 9]);
399    }
400}