Skip to content

Commit b291c26

Browse files
authored
Merge pull request fortanix#771 from fortanix/jb/eval-nums-select-best
[PROD-10366] Add feature to select best TCB eval num given specific TCB level
2 parents 4088b32 + 769971a commit b291c26

26 files changed

+250
-27
lines changed

intel-sgx/pcs/src/io.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,66 @@ fn write_to_path<T: serde::ser::Serialize>(path: &PathBuf, obj: &T) -> Result<()
4545
.map_err(|e| Error::IoError(e))
4646
}
4747

48-
pub fn read_from_file<T: DeserializeOwned>(dir: &str, filename: &str) -> Result<T, Error> {
48+
pub fn read_from_file<T: DeserializeOwned>(dir: &str, filename: impl AsRef<Path>) -> Result<T, Error> {
4949
let path = Path::new(dir);
5050
let path = path.join(filename);
5151
let file = File::open(path)?;
5252
let reader = BufReader::new(file);
5353
let obj = serde_json::from_reader(reader)?;
5454
Ok(obj)
5555
}
56+
57+
pub(crate) fn compose_filename(prefix: &str, extension: &str, evaluation_data_number: Option<u64>) -> String {
58+
if let Some(evaluation_data_number) = evaluation_data_number {
59+
format!("{}-{evaluation_data_number}{}", prefix, extension)
60+
} else {
61+
format!("{}{}", prefix, extension)
62+
}
63+
}
64+
65+
pub(crate) fn all_files<'a>(input_dir: &str, prefix: impl AsRef<str> + 'a, extension: &'a str) -> impl Iterator<Item = Result<std::fs::DirEntry, Error>> + 'a {
66+
// Construct an iterator where if read_dir() returns an error, that is the
67+
// first and only item. If it returns success, the directory entries are
68+
// the items.
69+
let (first, second) = match std::fs::read_dir(input_dir) {
70+
Ok(it) => (Some(it), None),
71+
Err(e) => (None, Some(Err(e))),
72+
};
73+
let entries = first.into_iter().flat_map(|it| it).chain(second.into_iter());
74+
75+
// match filenames that are generated by compose_filename() above
76+
entries.filter_map(move |item| {
77+
let prefix = prefix.as_ref();
78+
match item {
79+
Ok(entry) => {
80+
let fname = entry.file_name();
81+
let fname = fname.to_str()?;
82+
if !(fname.starts_with(prefix) && fname.ends_with(extension)) {
83+
return None;
84+
}
85+
if fname.len() != prefix.len() + extension.len() {
86+
let middle = &fname[(prefix.len())..(fname.len() - extension.len())];
87+
if !middle.starts_with("-") {
88+
return None;
89+
}
90+
let _: u64 = middle[1..].parse().ok()?;
91+
}
92+
Some(Ok(entry))
93+
},
94+
Err(e) => Some(Err(e.into()))
95+
}
96+
})
97+
}
98+
99+
#[cfg(test)]
100+
mod tests {
101+
#[cfg(not(target_env = "sgx"))]
102+
#[test]
103+
fn test_all_files() {
104+
use std::{collections::HashSet, ffi::OsString};
105+
106+
let a: HashSet<_> = Result::unwrap(super::all_files("./tests/data/read-dir-test", "prefix", ".ext").map(|i| i.map(|entry| entry.file_name()) ).collect());
107+
let b: HashSet<_> = <_>::into_iter(["prefix-1.ext", "prefix-2.ext", "prefix-3.ext", "prefix.ext"]).map(|i| OsString::from(i)).collect();
108+
assert_eq!(a, b);
109+
}
110+
}

intel-sgx/pcs/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,16 @@ pub enum TcbStatus {
295295
Revoked,
296296
}
297297

298+
impl TcbStatus {
299+
pub(crate) fn drop_sw_hardening_needed(self) -> Self {
300+
match self {
301+
Self::SWHardeningNeeded => Self::UpToDate,
302+
Self::ConfigurationAndSWHardeningNeeded => Self::ConfigurationNeeded,
303+
v => v,
304+
}
305+
}
306+
}
307+
298308
impl fmt::Display for TcbStatus {
299309
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300310
match self {

intel-sgx/pcs/src/qe_identity.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,16 +73,16 @@ impl Display for EnclaveIdentity {
7373
}
7474

7575
#[derive(Clone, Serialize, Deserialize, Debug)]
76-
struct Tcb {
77-
isvsvn: u16,
76+
pub(crate) struct Tcb {
77+
pub(crate) isvsvn: u16,
7878
}
7979

8080
#[derive(Clone, Serialize, Deserialize, Debug)]
8181
#[serde(rename_all = "camelCase")]
8282
pub struct TcbLevel {
83-
tcb: Tcb,
83+
pub(crate) tcb: Tcb,
8484
tcb_date: String,
85-
tcb_status: TcbStatus,
85+
pub(crate) tcb_status: TcbStatus,
8686
#[serde(default, rename = "advisoryIDs", skip_serializing_if = "Vec::is_empty")]
8787
advisory_ids: Vec<String>,
8888
}
@@ -224,10 +224,16 @@ impl QeIdentity {
224224
pub fn miscselect_mask(&self) -> Miscselect {
225225
Miscselect::from_bits_truncate(self.miscselect_mask)
226226
}
227+
}
227228

229+
impl<V: VerificationType> QeIdentity<V> {
228230
pub fn tcb_evaluation_data_number(&self) -> u64 {
229231
self.tcb_evaluation_data_number
230232
}
233+
234+
pub(crate) fn tcb_levels(&self) -> &[TcbLevel] {
235+
&self.tcb_levels
236+
}
231237
}
232238

233239
impl TryFrom<&QeIdentitySigned> for QeIdentity<Unverified> {
@@ -303,7 +309,8 @@ pub struct QeIdentitySigned {
303309
}
304310

305311
impl QeIdentitySigned {
306-
const DEFAULT_FILENAME: &'static str = "qe3_identity.id";
312+
const FILENAME_PREFIX: &'static str = "qe3_identity";
313+
const FILENAME_EXTENSION: &'static str = ".id";
307314

308315
pub fn parse(body: &String, ca_chain: Vec<String>) -> Result<Self, Error> {
309316
#[derive(Deserialize)]
@@ -330,11 +337,7 @@ impl QeIdentitySigned {
330337
}
331338

332339
pub fn create_filename(evaluation_data_number: Option<u64>) -> String {
333-
if let Some(evaluation_data_number) = evaluation_data_number {
334-
format!("qe3_identity-{evaluation_data_number}.id")
335-
} else {
336-
Self::DEFAULT_FILENAME.into()
337-
}
340+
io::compose_filename(Self::FILENAME_PREFIX, Self::FILENAME_EXTENSION, evaluation_data_number)
338341
}
339342

340343
pub fn write_to_file(&self, output_dir: &str) -> Result<String, Error> {
@@ -356,6 +359,11 @@ impl QeIdentitySigned {
356359
Ok(identity)
357360
}
358361

362+
pub fn read_all<'a>(input_dir: &'a str) -> impl Iterator<Item = Result<Self, Error>> + 'a {
363+
io::all_files(input_dir, Self::FILENAME_PREFIX, Self::FILENAME_EXTENSION)
364+
.map(move |i| i.and_then(|entry| io::read_from_file(input_dir, entry.file_name())) )
365+
}
366+
359367
pub fn raw_qe_identity(&self) -> &String {
360368
&self.raw_enclave_identity
361369
}

intel-sgx/pcs/src/tcb_evaluation_data_numbers.rs

Lines changed: 124 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use chrono::{DateTime, Duration, Utc};
2-
use crate::{io, Error, Platform, Unverified, VerificationType, Verified};
2+
use crate::{io, Error, Fmspc, Platform, pckcrt::TcbComponents, QeIdentity, QeIdentitySigned, TcbData, TcbInfo, TcbStatus, Unverified, VerificationType, Verified};
33
use serde::{Deserialize, Deserializer, Serialize};
44
use serde_json::value::RawValue;
55
use std::marker::PhantomData;
@@ -71,6 +71,84 @@ impl<V: VerificationType> TcbEvaluationDataNumbers<V> {
7171
}
7272
}
7373

74+
impl TcbEvaluationDataNumbers<Unverified> {
75+
/// Given a particular TCB level, select the best available TCB eval number.
76+
/// That is the one that gives the most favorable TCB status, and the higher
77+
/// one if there's a tie.
78+
pub fn select_best(input_dir: &str, fmspc: &Fmspc, tcb_components: &TcbComponents, qesvn: u16) -> Result<TcbEvalNumber, Error> {
79+
let evalnums = RawTcbEvaluationDataNumbers::read_from_file(input_dir)?.evaluation_data_numbers()?;
80+
let mut tcb_levels: std::collections::HashMap<_, _> = evalnums.numbers().map(|num| (num.number as u64, (num, None, None))).collect();
81+
82+
for tcbinfo in TcbInfo::read_all(input_dir, fmspc) {
83+
let tcb_data = TcbData::parse(tcbinfo?.raw_tcb_info())?;
84+
if let Some(level) = tcb_data.tcb_levels()
85+
.iter()
86+
.find(|level| level.tcb <= *tcb_components)
87+
{
88+
if let Some(entry) = tcb_levels.get_mut(&tcb_data.tcb_evaluation_data_number()) {
89+
entry.1 = Some(level.tcb_status);
90+
}
91+
}
92+
};
93+
94+
for qeid in QeIdentitySigned::read_all(input_dir) {
95+
let qeid: QeIdentity::<Unverified> = serde_json::from_str(&qeid?.raw_qe_identity()).map_err(|e| Error::ParseError(e))?;
96+
if let Some(level) = qeid.tcb_levels()
97+
.iter()
98+
.find(|level| level.tcb.isvsvn <= qesvn)
99+
{
100+
if let Some(entry) = tcb_levels.get_mut(&qeid.tcb_evaluation_data_number()) {
101+
entry.2 = Some(level.tcb_status);
102+
}
103+
}
104+
};
105+
106+
// NB: QE Identity TCB status can only be UpToDate, OutOfDate, or Revoked
107+
fn tcb_total_order(platform_status: Option<TcbStatus>, qe_status: Option<TcbStatus>) -> i8 {
108+
use std::ops::Neg;
109+
use self::TcbStatus::*;
110+
// Since we don't have any information here to judge the enclave
111+
// has the needed SW hardening, we assume that it does and we
112+
// upgrade SWHardeningNeeded to the next level
113+
match (platform_status.map(TcbStatus::drop_sw_hardening_needed), qe_status) {
114+
(Some(UpToDate), Some(UpToDate)) => 0i8,
115+
(Some(UpToDate), Some(OutOfDate)) => 1,
116+
(Some(UpToDate), Some(Revoked)) => 1,
117+
(Some(ConfigurationNeeded), Some(UpToDate)) => 2,
118+
(Some(ConfigurationNeeded), Some(OutOfDate)) => 3,
119+
(Some(ConfigurationNeeded), Some(Revoked)) => 3,
120+
(Some(OutOfDate), Some(UpToDate)) => 4,
121+
(Some(OutOfDateConfigurationNeeded), Some(UpToDate)) => 4,
122+
(Some(Revoked), Some(UpToDate)) => 4,
123+
(Some(OutOfDate), Some(OutOfDate)) => 5,
124+
(Some(OutOfDate), Some(Revoked)) => 5,
125+
(Some(Revoked), Some(OutOfDate)) => 5,
126+
(Some(Revoked), Some(Revoked)) => 5,
127+
(Some(OutOfDateConfigurationNeeded), Some(OutOfDate)) => 5,
128+
(Some(OutOfDateConfigurationNeeded), Some(Revoked)) => 5,
129+
(Some(UpToDate), None) => 6,
130+
(Some(ConfigurationNeeded), None) => 7,
131+
(Some(OutOfDate), None) => 8,
132+
(Some(OutOfDateConfigurationNeeded), None) => 8,
133+
(Some(Revoked), None) => 8,
134+
(None, Some(UpToDate)) => 9,
135+
(None, Some(OutOfDate)) => 10,
136+
(None, Some(Revoked)) => 10,
137+
_ => 11,
138+
}.neg()
139+
}
140+
141+
tcb_levels.into_iter()
142+
.max_by(|&(a_num, (_, a_platform_status, a_qe_status)), &(b_num, (_, b_platform_status, b_qe_status))| {
143+
tcb_total_order(a_platform_status, a_qe_status)
144+
.cmp(&tcb_total_order(b_platform_status, b_qe_status))
145+
.then_with(|| a_num.cmp(&b_num) )
146+
})
147+
.map(|(_, (num, _, _))| num.clone())
148+
.ok_or(Error::InvalidTcbEvaluationDataNumbers("Empty TCB evaluation data numbers".into()))
149+
}
150+
}
151+
74152
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
75153
#[serde(rename_all = "camelCase")]
76154
pub struct TcbEvalNumber {
@@ -282,13 +360,21 @@ impl TcbPolicy {
282360
}
283361
}
284362

285-
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
286363
#[cfg(test)]
287364
mod tests {
288-
use super::{RawTcbEvaluationDataNumbers, TcbEvaluationDataNumbers, TcbEvalNumber, TcbPolicy};
289-
use crate::{Error, Platform, Unverified};
365+
#[cfg(not(target_env = "sgx"))]
366+
use {
367+
super::TcbEvaluationDataNumbers,
368+
crate::{Error, Unverified}
369+
};
370+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
371+
use super::{RawTcbEvaluationDataNumbers, TcbPolicy, TcbEvalNumber};
372+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
373+
use crate::Platform;
374+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
290375
use chrono::{Duration, TimeZone, Utc};
291376

377+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
292378
#[test]
293379
fn parse_tcb_evaluation_data_numbers() {
294380
let numbers = RawTcbEvaluationDataNumbers::read_from_file("./tests/data").unwrap();
@@ -297,6 +383,7 @@ mod tests {
297383
numbers.verify_ex(&root_certificates, Platform::SGX, &Utc.with_ymd_and_hms(2025, 6, 4, 12, 0, 0).unwrap()).unwrap();
298384
}
299385

386+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
300387
#[test]
301388
fn parse_tcb_evaluation_data_numbers_incorrect_signature() {
302389
let numbers = RawTcbEvaluationDataNumbers::read_from_file("./tests/data").unwrap();
@@ -313,6 +400,7 @@ mod tests {
313400
}
314401
}
315402

403+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
316404
#[test]
317405
fn tcb_eval_number() {
318406
let april_8_2025 = Utc.with_ymd_and_hms(2025, 4, 8, 14, 55, 0).unwrap();
@@ -334,6 +422,7 @@ mod tests {
334422
assert!(policy.needs_to_be_enforced(&number, &april_17_2025));
335423
}
336424

425+
#[cfg(all(not(target_env = "sgx"), feature = "verify"))]
337426
#[test]
338427
fn minimum_tcb_evaluation_data_number() {
339428
let numbers = RawTcbEvaluationDataNumbers::read_from_file("./tests/data").unwrap();
@@ -363,4 +452,35 @@ mod tests {
363452
assert_eq!(policy.minimum_tcb_evaluation_data_number_ex(&numbers, &november_20_2024),
364453
Some(number_17));
365454
}
455+
456+
#[cfg(not(target_env = "sgx"))]
457+
#[test]
458+
fn select_best() {
459+
use crate::pckcrt::TcbComponents;
460+
fn select(tcb_components: &TcbComponents, qesvn: u16) -> Result<u16, Error> {
461+
use std::convert::TryInto;
462+
TcbEvaluationDataNumbers::<Unverified>::select_best("./tests/data/eval-num-select-best", &"00606a000000".try_into().unwrap(), tcb_components, qesvn)
463+
.map(|num| num.number)
464+
}
465+
// platform and QE are nonsensical: just choose highest
466+
assert_eq!(select(&TcbComponents::from_raw([0; 16], 0), 0).unwrap(), 19);
467+
// platform is nonsensical: choose eval nums based on QE up-to-date
468+
assert_eq!(select(&TcbComponents::from_raw([0; 16], 0), 8).unwrap(), 19);
469+
assert_eq!(select(&TcbComponents::from_raw([0; 16], 0), 6).unwrap(), 8);
470+
// QE is nonsensical: choose eval nums based on platform up-to-date
471+
assert_eq!(select(&TcbComponents::from_raw([16, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 0).unwrap(), 19);
472+
assert_eq!(select(&TcbComponents::from_raw([15, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 0).unwrap(), 18);
473+
assert_eq!(select(&TcbComponents::from_raw([14, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 0).unwrap(), 17);
474+
assert_eq!(select(&TcbComponents::from_raw([7, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 0).unwrap(), 14);
475+
// platform and QE are fully up to date: choose highest
476+
assert_eq!(select(&TcbComponents::from_raw([16, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 8).unwrap(), 19);
477+
// QE is up to date: choose up-to-date eval nums based on platform
478+
assert_eq!(select(&TcbComponents::from_raw([15, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 8).unwrap(), 18);
479+
assert_eq!(select(&TcbComponents::from_raw([14, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 8).unwrap(), 17);
480+
assert_eq!(select(&TcbComponents::from_raw([7, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 8).unwrap(), 8);
481+
// platform is up to date: choose up-to-date eval nums based on QE
482+
assert_eq!(select(&TcbComponents::from_raw([16, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 6).unwrap(), 8);
483+
// neither platform and QE are up to date: choose highest eval nums where they were both up to date
484+
assert_eq!(select(&TcbComponents::from_raw([4, 16, 3, 3, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 13), 5).unwrap(), 8);
485+
}
366486
}

0 commit comments

Comments
 (0)