pcs/
tcb_info.rs

1/* Copyright (c) Fortanix, Inc.
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 */
7
8use std::convert::TryFrom;
9use std::marker::PhantomData;
10use std::path::PathBuf;
11
12use chrono::{DateTime, Utc};
13use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
14use serde_json::value::RawValue;
15#[cfg(feature = "verify")]
16use {
17    mbedtls::alloc::List as MbedtlsList, mbedtls::x509::certificate::Certificate, mbedtls::error::{codes, Error as ErrMbed}, pkix::oid,
18    pkix::pem::PEM_CERTIFICATE, pkix::x509::GenericCertificate, pkix::FromBer, std::ops::Deref,
19};
20
21use crate::pckcrt::{TcbComponent, TcbComponents};
22use crate::{io, CpuSvn, Error, PceIsvsvn, Platform, TcbStatus, Unverified, VerificationType, Verified};
23
24#[derive(Debug, Clone, Hash, PartialEq, Eq)]
25pub struct Fmspc([u8; 6]);
26
27#[derive(Debug)]
28pub enum FmspcDecodeError {
29    InvalidHex,
30    InvalidFmspcLength,
31}
32
33impl From<base16::DecodeError> for FmspcDecodeError {
34    fn from(_value: base16::DecodeError) -> FmspcDecodeError {
35        FmspcDecodeError::InvalidHex
36    }
37}
38
39impl Fmspc {
40    pub const fn new(value: [u8; 6]) -> Self {
41        Fmspc(value)
42    }
43
44    pub fn as_bytes(&self) -> &[u8; 6] {
45        &self.0
46    }
47}
48
49impl From<[u8; 6]> for Fmspc {
50    fn from(value: [u8; 6]) -> Fmspc {
51        Fmspc::new(value)
52    }
53}
54
55impl TryFrom<&[u8]> for Fmspc {
56    type Error = FmspcDecodeError;
57
58    fn try_from(value: &[u8]) -> Result<Fmspc, FmspcDecodeError> {
59        let value = <[u8; 6]>::try_from(value).map_err(|_| FmspcDecodeError::InvalidFmspcLength)?;
60        Ok(Fmspc::new(value))
61    }
62}
63
64impl TryFrom<&Vec<u8>> for Fmspc {
65    type Error = FmspcDecodeError;
66
67    fn try_from(value: &Vec<u8>) -> Result<Fmspc, FmspcDecodeError> {
68        Fmspc::try_from(value.as_slice())
69    }
70}
71
72impl TryFrom<&str> for Fmspc {
73    type Error = FmspcDecodeError;
74
75    fn try_from(value: &str) -> Result<Fmspc, FmspcDecodeError> {
76        let value = base16::decode(value)?;
77        Fmspc::try_from(value.as_slice())
78    }
79}
80
81impl TryFrom<&String> for Fmspc {
82    type Error = FmspcDecodeError;
83
84    fn try_from(value: &String) -> Result<Fmspc, FmspcDecodeError> {
85        Fmspc::try_from(value.as_str())
86    }
87}
88
89impl ToString for Fmspc {
90    fn to_string(&self) -> String {
91        base16::encode_lower(&self.0)
92    }
93}
94
95impl Serialize for Fmspc {
96    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97    where
98        S: Serializer,
99    {
100        self.to_string().serialize(serializer)
101    }
102}
103
104impl<'de> Deserialize<'de> for Fmspc {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: Deserializer<'de>,
108    {
109        let fmspc = <&str>::deserialize(deserializer)?;
110        Fmspc::try_from(fmspc).map_err(|_| de::Error::custom("Bad fmspc format"))
111    }
112}
113
114#[derive(Serialize, Deserialize, Clone, Debug)]
115#[serde(rename_all = "camelCase")]
116pub struct TcbLevel {
117    pub(crate) tcb: TcbComponents,
118    #[serde(with = "crate::iso8601")]
119    tcb_date: DateTime<Utc>,
120    pub(crate) tcb_status: TcbStatus,
121    #[serde(default, rename = "advisoryIDs", skip_serializing_if = "Vec::is_empty")]
122    advisory_ids: Vec<AdvisoryID>,
123}
124
125impl TcbLevel {
126    pub fn components(&self) -> &TcbComponents {
127        &self.tcb
128    }
129
130    pub fn tcb_status(&self) -> TcbStatus {
131        self.tcb_status
132    }
133
134    pub fn advisory_ids(&self) -> &Vec<AdvisoryID> {
135        &self.advisory_ids
136    }
137
138    pub fn tcb_date(&self) -> &DateTime<Utc> {
139        &self.tcb_date
140    }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub enum AdvisoryID {
145    /// Security Advisory ID - "INTEL-SA-XXXXX" (where XXXXX is a placeholder for a 5-digit
146    /// number) - representing Security Advisories that can be searched on IntelĀ® Product
147    /// Security Center Advisories page
148    /// (<https://www.intel.com/content/www/us/en/security-center/default.html>)
149    Security(u32),
150    /// Document Advisory ID - "INTEL-DOC-XXXXX" (where XXXXX is a placeholder for a 5-digit
151    /// number) - representing articles containing additional information about the attested
152    /// platform. The articles can be found under the following URL:
153    /// <https://api.trustedservices.intel.com/documents/{docID}>
154    Documentation(u32)
155}
156
157impl ToString for AdvisoryID {
158    fn to_string(&self) -> String {
159        match self {
160            AdvisoryID::Security(v) => format!("INTEL-SA-{:05}", v),
161            AdvisoryID::Documentation(v) => format!("INTEL-DOC-{:05}", v),
162        }
163    }
164}
165
166impl TryFrom<&str> for AdvisoryID {
167    type Error = &'static str;
168
169    fn try_from(s: &str) -> Result<AdvisoryID, &'static str> {
170        fn tokenize(s: &str) -> Result<(String, String, u32), &'static str> {
171            let mut chunks = s.trim().split('-');
172
173            let intel = chunks.next().ok_or("Couldn't parse INTEL part of advisory ID number")?;
174            let typ = chunks.next().ok_or("Couldn't parse type of advisory ID number")?;
175            let value = chunks.next().ok_or("Couldn't parse value of advisory ID number")?;
176            let value = u32::from_str_radix(value, 10).map_err(|_| "Couldn't parse advisory ID number")?;
177
178            if chunks.next().is_some() {
179                return Err("Failed to parse Advisory ID");
180            }
181
182            Ok((intel.to_string(), typ.to_string(), value))
183        }
184
185        let (intel, typ, value) = tokenize(s)?;
186        if intel.to_uppercase() != "INTEL" {
187            return Err("Advisory IDs must start with INTEL");
188        }
189
190        match typ.to_uppercase().as_str() {
191            "SA" => Ok(AdvisoryID::Security(value)),
192            "DOC" => Ok(AdvisoryID::Documentation(value)),
193            _ => Err("Not a security nor document advisory ID"),
194        }
195    }
196}
197
198impl Serialize for AdvisoryID {
199    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
200    where
201        S: Serializer,
202    {
203        self.to_string().serialize(serializer)
204    }
205}
206
207impl<'de> Deserialize<'de> for AdvisoryID {
208    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
209    where
210        D: Deserializer<'de>,
211    {
212        let advisory = <&str>::deserialize(deserializer)?;
213        AdvisoryID::try_from(advisory).map_err(|e| de::Error::custom(format!("Bad AdvisoryID format: {e}")))
214    }
215}
216
217#[allow(dead_code)]
218#[derive(Clone, Debug)]
219pub struct TcbData<V: VerificationType = Verified> {
220    id: Platform,
221    version: u16,
222    issue_date: DateTime<Utc>,
223    next_update: DateTime<Utc>,
224    fmspc: Fmspc,
225    pce_id: String,
226    tcb_type: u16,
227    tcb_evaluation_data_number: u64,
228    tcb_levels: Vec<TcbLevel>,
229    type_: PhantomData<V>,
230}
231
232impl<'de> Deserialize<'de> for TcbData<Unverified> {
233    fn deserialize<D>(deserializer: D) -> Result<TcbData<Unverified>, D::Error>
234    where
235        D: Deserializer<'de>,
236    {
237        #[derive(Deserialize)]
238        #[serde(rename_all = "camelCase")]
239        struct Dummy {
240            #[serde(default = "crate::sgx_platform")]
241            id: Platform,
242            version: u16,
243            #[serde(with = "crate::iso8601")]
244            issue_date: DateTime<Utc>,
245            #[serde(with = "crate::iso8601")]
246            next_update: DateTime<Utc>,
247            fmspc: Fmspc,
248            pce_id: String,
249            tcb_type: u16,
250            tcb_evaluation_data_number: u64,
251            #[serde(rename = "tcbLevels")]
252            tcb_levels: Vec<TcbLevel>,
253        }
254
255        let Dummy {
256            id,
257            version,
258            issue_date,
259            next_update,
260            fmspc,
261            pce_id,
262            tcb_type,
263            tcb_evaluation_data_number,
264            tcb_levels,
265        } = Dummy::deserialize(deserializer)?;
266        Ok(TcbData {
267            id,
268            version,
269            issue_date,
270            next_update,
271            fmspc,
272            pce_id,
273            tcb_type,
274            tcb_evaluation_data_number,
275            tcb_levels,
276            type_: PhantomData,
277        })
278    }
279}
280
281impl TcbData<Verified> {
282    pub fn version(&self) -> u16 {
283        self.version
284    }
285
286    pub fn fmspc(&self) -> &Fmspc {
287        &self.fmspc
288    }
289}
290
291impl TcbData<Unverified> {
292    pub(crate) fn parse(raw_tcb_data: &String) -> Result<TcbData<Unverified>, Error> {
293        let data: TcbData<Unverified> = serde_json::from_str(&raw_tcb_data).map_err(|e| Error::ParseError(e))?;
294        if data.version != 2 && data.version != 3 {
295            return Err(Error::UnknownTcbInfoVersion(data.version));
296        }
297
298        // Only tcb_type 0 is known at the moment, verify that it has that expected value
299        if data.tcb_type != 0 {
300            return Err(Error::UnknownTcbType(data.tcb_type));
301        }
302        Ok(data)
303    }
304
305    /// Returns the index of the TCB component
306    pub fn tcb_component_index(&self, comp: TcbComponent) -> Option<usize> {
307        if let Some(c) = self.tcb_levels.first() {
308            c.components().tcb_component_index(comp)
309        } else {
310            None
311        }
312    }
313
314}
315
316impl<V: VerificationType> TcbData<V> {
317    pub fn tcb_evaluation_data_number(&self) -> u64 {
318        self.tcb_evaluation_data_number
319    }
320
321    // NOTE: don't make this publicly available. We want to prevent people from
322    // accessing the TCB levels without checking whether the TcbInfo is valid.
323    pub(crate) fn tcb_levels(&self) -> &Vec<TcbLevel> {
324        &self.tcb_levels
325    }
326
327    pub(crate) fn decompose_raw_cpusvn(&self, raw_cpusvn: &[u8; 16], pce_svn: u16) -> Result<TcbComponents, Error> {
328        if self.tcb_type != 0 {
329            return Err(Error::UnknownTcbType(self.tcb_type));
330        }
331
332        // TCB Type 0 simply copies cpu svn
333        Ok(TcbComponents::from_raw(*raw_cpusvn, pce_svn))
334    }
335
336    pub fn iter_tcb_components(&self) -> impl Iterator<Item = (CpuSvn, PceIsvsvn)> + '_ {
337        self.tcb_levels.iter().map(|tcb_level| (tcb_level.tcb.cpu_svn(), tcb_level.tcb.pce_svn()))
338    }
339}
340
341#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
342pub struct TcbInfo {
343    raw_tcb_info: String,
344    signature: Vec<u8>,
345    ca_chain: Vec<String>,
346}
347
348impl TcbInfo {
349    const FILENAME_EXTENSION: &'static str = ".tcb";
350
351    pub fn new(raw_tcb_info: String, signature: Vec<u8>, ca_chain: Vec<String>) -> Self {
352        TcbInfo {
353            raw_tcb_info,
354            signature,
355            ca_chain,
356        }
357    }
358
359    pub fn parse(body: &String, ca_chain: Vec<String>) -> Result<Self, Error> {
360        #[derive(Deserialize)]
361        #[serde(rename_all = "camelCase")]
362        struct IntelTcbInfoSigned<'a> {
363            #[serde(borrow, rename = "tcbInfo")]
364            raw_tcb_info: &'a RawValue,
365            #[serde(deserialize_with = "crate::intel_signature_deserializer")]
366            signature: Vec<u8>,
367        }
368        let IntelTcbInfoSigned { raw_tcb_info, signature } = serde_json::from_str(&body)?;
369
370        Ok(TcbInfo::new(raw_tcb_info.to_string(), signature, ca_chain))
371    }
372
373    pub fn create_filename(fmspc: &str, evaluation_data_number: Option<u64>) -> String {
374        io::compose_filename(fmspc, Self::FILENAME_EXTENSION, evaluation_data_number)
375    }
376
377    pub fn store(&self, output_dir: &str) -> Result<String, Error> {
378        let data = TcbData::<Unverified>::parse(&self.raw_tcb_info)?;
379        let filename = Self::create_filename(&data.fmspc.to_string(), Some(data.tcb_evaluation_data_number));
380        io::write_to_file(&self, output_dir, &filename)?;
381        Ok(filename)
382    }
383
384    pub fn store_if_not_exist(&self, output_dir: &str) -> Result<Option<PathBuf>, Error> {
385        let data = TcbData::<Unverified>::parse(&self.raw_tcb_info)?;
386        let filename = Self::create_filename(&data.fmspc.to_string(), Some(data.tcb_evaluation_data_number));
387        io::write_to_file_if_not_exist(&self, output_dir, &filename)
388    }
389
390    pub fn restore(input_dir: &str, fmspc: &Fmspc, evaluation_data_number: Option<u64>) -> Result<Self, Error> {
391        let filename = TcbInfo::create_filename(&fmspc.to_string(), evaluation_data_number);
392        let info: TcbInfo = io::read_from_file(input_dir, &filename)?;
393        Ok(info)
394    }
395
396    pub fn read_all<'a>(input_dir: &'a str, fmspc: &'a Fmspc) -> impl Iterator<Item = Result<Self, Error>> + 'a {
397        io::all_files(input_dir, fmspc.to_string(), Self::FILENAME_EXTENSION)
398            .map(move |i| i.and_then(|entry| io::read_from_file(input_dir, entry.file_name())) )
399    }
400
401    pub fn raw_tcb_info(&self) -> &String {
402        &self.raw_tcb_info
403    }
404
405    pub fn signature(&self) -> &Vec<u8> {
406        &self.signature
407    }
408
409    pub fn certificate_chain(&self) -> &Vec<String> {
410        &self.ca_chain
411    }
412
413    #[cfg(feature = "verify")]
414    pub fn verify<B: Deref<Target = [u8]>>(&self, trusted_root_certs: &[B], platform: Platform, min_version: u16) -> Result<TcbData<Verified>, Error> {
415        let now = Utc::now();
416        self.verify_ex(trusted_root_certs, platform, min_version, &now)
417    }
418
419    #[cfg(feature = "verify")]
420    fn verify_ex<B: Deref<Target = [u8]>>(&self, trusted_root_certs: &[B], platform: Platform, min_version: u16, now: &DateTime<Utc>) -> Result<TcbData<Verified>, Error> {
421        // Check cert chain
422        let (chain, root) = crate::create_cert_chain(&self.ca_chain)?;
423        let mut leaf = chain.first().unwrap_or(&root).clone();
424        let root_list = std::iter::once(root).collect();
425        if 0 < chain.len() {
426            let trust_ca: MbedtlsList<Certificate> = chain.into_iter().collect();
427            let mut err = String::default();
428            Certificate::verify(&trust_ca, &root_list, None, Some(&mut err))
429                .map_err(|_| Error::InvalidTcbInfo(format!("Invalid TcbInfo: {}", err)))?;
430        }
431
432        // Check signature on data
433        let mut hash = [0u8; 32];
434        mbedtls::hash::Md::hash(mbedtls::hash::Type::Sha256, self.raw_tcb_info.as_bytes(), &mut hash).unwrap();
435        leaf.public_key_mut()
436            .verify(mbedtls::hash::Type::Sha256, &hash, self.signature())
437            .map_err(|_| Error::InvalidTcbInfo("Signature verification failed".into()))?;
438
439        // Check common name TCB cert
440        let leaf = self.ca_chain.first().ok_or(Error::IncorrectCA)?;
441        let tcb =
442            &pkix::pem::pem_to_der(&leaf, Some(PEM_CERTIFICATE)).ok_or(Error::InvalidQe3Id(ErrMbed::HighLevel(codes::X509BadInputData)))?;
443        let tcb = GenericCertificate::from_ber(&tcb).map_err(|_| Error::InvalidQe3Id(ErrMbed::HighLevel(codes::X509BadInputData)))?;
444        let name = tcb
445            .tbscert
446            .subject
447            .get(&*oid::commonName)
448            .ok_or(Error::InvalidQe3Id(ErrMbed::HighLevel(codes::X509BadInputData)))?;
449        if String::from_utf8_lossy(&name.value()) != "Intel SGX TCB Signing" {
450            return Err(Error::IncorrectCA);
451        }
452
453        crate::check_root_ca(trusted_root_certs, &root_list)?;
454
455        let TcbData {
456            id,
457            version,
458            issue_date,
459            next_update,
460            fmspc,
461            pce_id,
462            tcb_type,
463            tcb_evaluation_data_number,
464            tcb_levels,
465            ..
466        } = TcbData::parse(&self.raw_tcb_info)?;
467
468        if id != platform {
469            return Err(Error::InvalidTcbInfo(format!("TCB Info belongs to the {id} platform, expected one for the {platform} platform")))
470        }
471
472        if min_version > version {
473            return Err(Error::UntrustedTcbInfoVersion(version, min_version))
474        }
475
476        if *now < issue_date {
477            return Err(Error::InvalidTcbInfo(format!("TCB Info only valid from {}", issue_date)))
478        }
479        if next_update < *now {
480            return Err(Error::InvalidTcbInfo(format!("TCB Info expired on {}", next_update)))
481        }
482
483        Ok(TcbData::<Verified> {
484            id,
485            version,
486            issue_date,
487            next_update,
488            fmspc,
489            pce_id,
490            tcb_type,
491            tcb_evaluation_data_number,
492            tcb_levels,
493            type_: PhantomData,
494        })
495    }
496
497    pub fn data(&self) -> Result<TcbData<Unverified>, Error> {
498        TcbData::parse(&self.raw_tcb_info)
499    }
500}
501
502#[cfg(feature = "verify")]
503#[cfg(test)]
504mod tests {
505    #[cfg(not(target_env = "sgx"))]
506    use {
507        chrono::{Utc, TimeZone},
508        crate::Error,
509        crate::tcb_info::{Fmspc, Platform, TcbInfo},
510        tempdir::TempDir,
511    };
512    use std::convert::TryFrom;
513    use super::AdvisoryID;
514
515    #[test]
516    #[cfg(not(target_env = "sgx"))]
517    fn read_tcb_info() {
518        let info =
519            TcbInfo::restore("./tests/data/", &Fmspc::try_from("00906ea10000").expect("static fmspc"), None).expect("validated");
520        let root_certificate = include_bytes!("../tests/data/root_SGX_CA_der.cert");
521        let root_certificates = [&root_certificate[..]];
522        match info.verify(&root_certificates, Platform::SGX, 2) {
523            Err(Error::InvalidTcbInfo(msg)) => assert_eq!(msg, String::from("TCB Info expired on 2020-06-17 17:49:24 UTC")),
524            e => assert!(false, "wrong result: {:?}", e),
525        }
526        let juni_5_2020 = Utc.with_ymd_and_hms(2020, 6, 5, 12, 0, 0).unwrap();
527        assert!(info.verify_ex(&root_certificates, Platform::SGX, 2, &juni_5_2020).is_ok());
528
529        match info.verify(&root_certificates, Platform::TDX, 2) {
530            Err(Error::InvalidTcbInfo(msg)) => assert_eq!(msg, String::from("TCB Info belongs to the SGX platform, expected one for the TDX platform")),
531            e => assert!(false, "wrong result: {:?}", e),
532        }
533
534        // Test serialization/deserialization
535        let temp_dir = TempDir::new("tempdir").unwrap();
536        let path = temp_dir.path().as_os_str().to_str().unwrap();
537        info.store(&path).unwrap();
538        let info2 = TcbInfo::restore(&path, &Fmspc::try_from("00906ea10000").expect("static fmspc"), Some(8)).unwrap();
539        assert_eq!(info, info2);
540    }
541
542    #[test]
543    #[cfg(not(target_env = "sgx"))]
544    fn read_corrupt_tcb_info() {
545        let tcb_info = TcbInfo::restore("./tests/data/corrupted", &Fmspc::try_from("00906ea10000").unwrap(), None).unwrap();
546        let root_certificate = include_bytes!("../tests/data/root_SGX_CA_der.cert");
547        let root_certificates = [&root_certificate[..]];
548        assert!(tcb_info.verify(&root_certificates, Platform::SGX, 2).is_err());
549    }
550
551    #[test]
552    #[cfg(not(target_env = "sgx"))]
553    fn read_tcb_info_v3() {
554        let tcb_info = TcbInfo::restore("./tests/data/", &Fmspc::try_from("00906ed50000").unwrap(), Some(19)).unwrap();
555        let root_certificate = include_bytes!("../tests/data/root_SGX_CA_der.cert");
556        let root_certificates = [&root_certificate[..]];
557        let june_5_2025 = Utc.with_ymd_and_hms(2025, 6, 5, 12, 0, 0).unwrap();
558        assert!(tcb_info.verify_ex(&root_certificates, Platform::SGX, 3, &june_5_2025).is_ok());
559        assert!(tcb_info.verify_ex(&root_certificates, Platform::SGX, 4, &june_5_2025).is_err());
560    }
561
562    #[test]
563    fn parse_advisory_ids() {
564        assert_eq!(AdvisoryID::try_from("INTEL-SA-00123"), Ok(AdvisoryID::Security(123)));
565        assert_eq!(AdvisoryID::try_from("INTEL-SA-10123"), Ok(AdvisoryID::Security(10123)));
566        assert_eq!(AdvisoryID::try_from("INTEL-SA-00001"), Ok(AdvisoryID::Security(1)));
567        assert_eq!(AdvisoryID::try_from("INTEL-DOC-00123"), Ok(AdvisoryID::Documentation(123)));
568        assert_eq!(AdvisoryID::try_from("INTEL-DOC-10123"), Ok(AdvisoryID::Documentation(10123)));
569        assert_eq!(AdvisoryID::try_from("INTEL-DOC-00001"), Ok(AdvisoryID::Documentation(1)));
570    }
571
572    #[test]
573    fn advisory_ids_to_string() {
574        assert_eq!(AdvisoryID::Security(123).to_string(), "INTEL-SA-00123");
575        assert_eq!(AdvisoryID::Security(1).to_string(), "INTEL-SA-00001");
576        assert_eq!(AdvisoryID::Security(99999).to_string(), "INTEL-SA-99999");
577        assert_eq!(AdvisoryID::Documentation(123).to_string(), "INTEL-DOC-00123");
578        assert_eq!(AdvisoryID::Documentation(1).to_string(), "INTEL-DOC-00001");
579        assert_eq!(AdvisoryID::Documentation(99999).to_string(), "INTEL-DOC-99999");
580    }
581}