feat(quote): add FMSPC and CPUSVN extraction support

- Introduced new types `Fmspc`, `CpuSvn`, and `Svn` for SGX metadata.
- Added methods to extract raw certificate chains and FMSPC from SGX quotes.
- Created new test file for validating FMSPC extraction with example quotes.

Signed-off-by: Harald Hoyer <harald@matterlabs.dev>
This commit is contained in:
Harald Hoyer 2025-05-06 11:22:33 +02:00
parent fca60adc1a
commit 2bbfb2415c
Signed by: harald
GPG key ID: F519A1143B3FBE32
6 changed files with 1547 additions and 0 deletions

1
Cargo.lock generated
View file

@ -4936,6 +4936,7 @@ name = "teepot"
version = "0.3.0"
dependencies = [
"anyhow",
"asn1_der",
"async-trait",
"base64",
"bytemuck",

View file

@ -19,6 +19,7 @@ homepage = "https://github.com/matter-labs/teepot"
[workspace.dependencies]
actix-web = { version = "4.5", features = ["rustls-0_23"] }
anyhow = "1.0.82"
asn1_der = { version = "0.7", default-features = false, features = ["native_types"] }
async-trait = "0.1.86"
awc = { version = "3.5", features = ["rustls-0_23-webpki-roots"] }
base64 = "0.22.0"

View file

@ -21,6 +21,7 @@ bytes.workspace = true
[dependencies]
anyhow.workspace = true
asn1_der.workspace = true
async-trait.workspace = true
bytemuck.workspace = true
clap.workspace = true

View file

@ -16,6 +16,7 @@ pub mod tcblevel;
path = "phala.rs"
)]
mod os;
mod utils;
use crate::quote::{
error::{QuoteContext as _, QuoteError},
@ -547,6 +548,23 @@ impl Decode for Quote {
}
}
/// FMSPC (Family-Model-Stepping-Platform-CustomSKU) is a 6-byte identifier
/// that uniquely identifies a platform's SGX TCB level.
/// It is extracted from the PCK certificate in the SGX quote and is used to
/// fetch TCB information from Intel's Provisioning Certification Service.
pub type Fmspc = [u8; 6];
/// CPU Security Version Number (CPUSVN) is a 16-byte value representing
/// the security version of the CPU microcode and firmware.
/// It is used in SGX attestation to determine the security patch level
/// of the platform.
pub type CpuSvn = [u8; 16];
/// Security Version Number (SVN) is a 16-bit value representing the
/// security version of a component (like PCE or QE).
/// Higher values indicate newer security patches have been applied.
pub type Svn = u16;
impl Quote {
/// Parse a TEE quote from a byte slice.
pub fn parse(quote: &[u8]) -> Result<Self, QuoteError> {
@ -555,6 +573,32 @@ impl Quote {
Ok(quote)
}
/// Get the raw certificate chain from the quote.
pub fn raw_cert_chain(&self) -> Result<&[u8], QuoteError> {
let cert_data = match &self.auth_data {
AuthData::V3(data) => &data.certification_data,
AuthData::V4(data) => &data.qe_report_data.certification_data,
};
if cert_data.cert_type != 5 {
QuoteError::QuoteCertificationDataUnsupported(format!(
"Unsupported cert type: {}",
cert_data.cert_type
));
}
Ok(&cert_data.body.data)
}
/// Get the FMSPC from the quote.
pub fn fmspc(&self) -> Result<Fmspc, QuoteError> {
let raw_cert_chain = self.raw_cert_chain()?;
let certs = utils::extract_certs(raw_cert_chain)?;
let cert = certs
.first()
.ok_or(QuoteError::Unexpected("Invalid certificate".into()))?;
let extension_section = utils::get_intel_extension(cert)?;
utils::get_fmspc(&extension_section)
}
/// Get the report data
pub fn get_report_data(&self) -> &[u8] {
match &self.report {

View file

@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2025 Matter Labs
// Parts of it are Copyright (c) 2024 Phala Network
// and copied from https://github.com/Phala-Network/dcap-qvl
use crate::quote::{error::QuoteError, Fmspc};
use asn1_der::{
typed::{DerDecodable, Sequence},
DerObject,
};
use x509_cert::certificate::CertificateInner;
pub mod oids {
use const_oid::ObjectIdentifier as OID;
const fn oid(s: &str) -> OID {
OID::new_unwrap(s)
}
pub const SGX_EXTENSION: OID = oid("1.2.840.113741.1.13.1");
pub const FMSPC: OID = oid("1.2.840.113741.1.13.1.4");
#[test]
fn const_oid_works() {
assert_eq!(
SGX_EXTENSION.as_bytes(),
oid("1.2.840.113741.1.13.1").as_bytes()
);
}
}
pub fn get_intel_extension(cert: &CertificateInner) -> Result<Vec<u8>, QuoteError> {
let mut extension_iter = cert
.tbs_certificate
.extensions
.as_deref()
.unwrap_or(&[])
.iter()
.filter(|e| e.extn_id == oids::SGX_EXTENSION)
.map(|e| e.extn_value.clone());
let extension = extension_iter
.next()
.ok_or_else(|| QuoteError::Unexpected("Intel extension not found".into()))?;
if extension_iter.next().is_some() {
//"There should only be one section containing Intel extensions"
return Err(QuoteError::Unexpected("Intel extension ambiguity".into()));
}
Ok(extension.into_bytes())
}
pub fn find_extension(path: &[&[u8]], raw: &[u8]) -> Result<Vec<u8>, QuoteError> {
let obj = DerObject::decode(raw)
.map_err(|_| QuoteError::Unexpected("Failed to decode DER object".into()))?;
let subobj =
get_obj(path, obj).map_err(|_| QuoteError::Unexpected("Failed to get subobject".into()))?;
Ok(subobj.value().to_vec())
}
fn get_obj<'a>(path: &[&[u8]], mut obj: DerObject<'a>) -> Result<DerObject<'a>, QuoteError> {
for oid in path {
let seq = Sequence::load(obj)
.map_err(|_| QuoteError::Unexpected("Failed to load sequence".into()))?;
obj = sub_obj(oid, seq)
.map_err(|_| QuoteError::Unexpected("Failed to get subobject".into()))?;
}
Ok(obj)
}
fn sub_obj<'a>(oid: &[u8], seq: Sequence<'a>) -> Result<DerObject<'a>, QuoteError> {
for i in 0..seq.len() {
let entry = seq
.get(i)
.map_err(|_| QuoteError::Unexpected("Failed to get entry".into()))?;
let entry = Sequence::load(entry)
.map_err(|_| QuoteError::Unexpected("Failed to load sequence".into()))?;
let name = entry
.get(0)
.map_err(|_| QuoteError::Unexpected("Failed to get name".into()))?;
let value = entry
.get(1)
.map_err(|_| QuoteError::Unexpected("Failed to get value".into()))?;
if name.value() == oid {
return Ok(value);
}
}
Err(QuoteError::Unexpected("Oid is missing".into()))
}
pub fn get_fmspc(extension_section: &[u8]) -> Result<Fmspc, QuoteError> {
let data = find_extension(&[oids::FMSPC.as_bytes()], extension_section)
.map_err(|_| QuoteError::Unexpected("Failed to find Fmspc".into()))?;
if data.len() != 6 {
return Err(QuoteError::Unexpected("Fmspc length mismatch".into()));
}
data.try_into()
.map_err(|_| QuoteError::Unexpected("Failed to decode Fmspc".into()))
}
pub fn extract_certs(cert_chain: &[u8]) -> Result<Vec<CertificateInner>, QuoteError> {
let cert_chain = cert_chain.strip_suffix(&[0]).unwrap_or(cert_chain);
CertificateInner::<x509_cert::certificate::Rfc5280>::load_pem_chain(cert_chain)
.map_err(|e| QuoteError::Unexpected(format!("Could not load a PEM chain: {}", e)))
}

File diff suppressed because it is too large Load diff