feat(quote): add FMSPC extraction and related utils

- Introduced FMSPC extraction methods and associated utility functions.
- Updated `Quote` structure with parsing enhancements and FMSPC support.
- Added `quote_op` feature gating for SGX/TDX-specific functionality.
This commit is contained in:
Harald Hoyer 2025-05-05 14:27:18 +02:00
parent 2118466a8a
commit 6e65cf2f47
Signed by: harald
GPG key ID: F519A1143B3FBE32
14 changed files with 1585 additions and 46 deletions

View file

@ -14,3 +14,4 @@ serde = { version = "1", features = ["derive", "rc"] }
[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
intel-tee-quote-verification-sys = { version = "0.2.1" }
tdx-attest-rs = { version = "0.1.2", git = "https://github.com/intel/SGXDataCenterAttestationPrimitives.git", rev = "aa239d25a437a28f3f4de92c38f5b6809faac842" }

View file

@ -38,6 +38,10 @@
//!
//! This is a safe wrapper for **sgx-dcap-quoteverify-sys**.
pub mod tdx_attest_rs {
pub use tdx_attest_rs::*;
}
use serde::{Deserialize, Serialize};
use std::{marker::PhantomData, ops::Deref, slice};

View file

@ -10,16 +10,17 @@ edition.workspace = true
authors.workspace = true
repository.workspace = true
[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
tdx-attest-rs = { version = "0.1.2", git = "https://github.com/intel/SGXDataCenterAttestationPrimitives.git", rev = "aa239d25a437a28f3f4de92c38f5b6809faac842" }
teepot-tee-quote-verification-rs = { path = "../teepot-tee-quote-verification-rs" }
[target.'cfg(not(all(target_os = "linux", target_arch = "x86_64")))'.dependencies]
dcap-qvl = "0.2.3"
chrono = "0.4.40"
bytes.workspace = true
[features]
default = ["quote_op"]
quote_op = ["dep:teepot-tee-quote-verification-rs"]
[dependencies]
teepot-tee-quote-verification-rs = { path = "../teepot-tee-quote-verification-rs", optional = true }
anyhow.workspace = true
async-trait.workspace = true
bytemuck.workspace = true
@ -56,6 +57,7 @@ tracing-log.workspace = true
tracing-subscriber.workspace = true
x509-cert.workspace = true
zeroize.workspace = true
asn1_der.workspace = true
[dev-dependencies]
base64.workspace = true

View file

@ -9,6 +9,7 @@
pub mod config;
pub mod ethereum;
pub mod log;
#[cfg(feature = "quote_op")]
pub mod pki;
pub mod prover;
pub mod quote;

View file

@ -4,8 +4,8 @@
//! Quote Error type
use std::io;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use tdx_attest_rs::tdx_attest_error_t;
#[cfg(all(feature = "quote_op", target_os = "linux", target_arch = "x86_64"))]
use teepot_tee_quote_verification_rs::tdx_attest_rs::tdx_attest_error_t;
use thiserror::Error;
/// Quote parsing error
@ -22,7 +22,7 @@ pub enum QuoteError {
InvalidTeeType,
#[error("unsupported body type")]
UnsupportedBodyType,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(all(feature = "quote_op", target_os = "linux", target_arch = "x86_64"))]
#[error("tdx_att_get_quote error {msg}: {inner:?}")]
TdxAttGetQuote {
inner: tdx_attest_error_t,
@ -58,7 +58,7 @@ pub enum QuoteError {
CrlUnsupportedFormat(String),
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(all(feature = "quote_op", target_os = "linux", target_arch = "x86_64"))]
impl From<tdx_attest_error_t> for QuoteError {
fn from(code: tdx_attest_error_t) -> Self {
Self::TdxAttGetQuote {
@ -108,7 +108,7 @@ impl<T, E: std::fmt::Display> QuoteContextErr for Result<T, E> {
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(all(feature = "quote_op", target_os = "linux", target_arch = "x86_64"))]
impl<T> QuoteContext for Result<T, tdx_attest_error_t> {
type Ok = T;
fn context<I: Into<String>>(self, msg: I) -> Result<T, QuoteError> {

View file

@ -11,9 +11,9 @@ use crate::{
};
use bytemuck::cast_slice;
use std::{ffi::CStr, mem, mem::MaybeUninit, pin::Pin};
use tdx_attest_rs::{tdx_att_get_quote, tdx_attest_error_t, tdx_report_data_t};
use teepot_tee_quote_verification_rs::{
quote3_error_t as _quote3_error_t, sgx_ql_qv_result_t, sgx_ql_qv_supplemental_t,
tdx_attest_rs::{tdx_att_get_quote, tdx_attest_error_t, tdx_report_data_t},
tee_get_supplemental_data_version_and_size, tee_qv_get_collateral, tee_supp_data_descriptor_t,
tee_verify_quote, Collateral as IntelCollateral,
};

View file

@ -6,6 +6,7 @@
//! Get a quote from a TEE
#[cfg(feature = "quote_op")]
pub mod attestation;
pub mod error;
pub mod tcblevel;
@ -15,7 +16,9 @@ pub mod tcblevel;
not(all(target_os = "linux", target_arch = "x86_64")),
path = "phala.rs"
)]
#[cfg(feature = "quote_op")]
mod os;
mod utils;
use crate::quote::{
error::{QuoteContext as _, QuoteError},
@ -547,6 +550,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 +575,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 {
@ -600,6 +646,7 @@ impl FromStr for TEEType {
}
/// Get the attestation quote from a TEE
#[cfg(feature = "quote_op")]
pub fn get_quote(report_data: &[u8]) -> Result<(TEEType, Box<[u8]>), QuoteError> {
os::get_quote(report_data)
}
@ -646,11 +693,13 @@ pub struct Collateral {
}
/// Get the collateral data from an SGX or TDX quote
#[cfg(feature = "quote_op")]
pub fn get_collateral(quote: &[u8]) -> Result<Collateral, QuoteError> {
os::get_collateral(quote)
}
/// Verifies a quote with optional collateral material
#[cfg(feature = "quote_op")]
pub fn verify_quote_with_collateral(
quote: &[u8],
collateral: Option<&Collateral>,

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)))
}

View file

@ -3,7 +3,7 @@
//! Intel TDX helper functions.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(all(feature = "quote_op", target_os = "linux", target_arch = "x86_64"))]
pub mod rtmr;
/// The sha384 digest of 0u32, which is used in the UEFI TPM protocol

View file

@ -4,6 +4,7 @@
//! rtmr event data
use crate::sgx::QuoteError;
use teepot_tee_quote_verification_rs::tdx_attest_rs::{tdx_att_extend, tdx_attest_error_t};
/// The actual rtmr event data handled in DCAP
#[repr(C, packed)]
@ -59,8 +60,8 @@ impl TdxRtmrEvent {
pub fn extend(self) -> Result<(), QuoteError> {
let event: Vec<u8> = self.into();
match tdx_attest_rs::tdx_att_extend(&event) {
tdx_attest_rs::tdx_attest_error_t::TDX_ATTEST_SUCCESS => Ok(()),
match tdx_att_extend(&event) {
tdx_attest_error_t::TDX_ATTEST_SUCCESS => Ok(()),
error_code => Err(error_code.into()),
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2024-2025 Matter Labs
#[cfg(feature = "quote_op")]
mod sgx {
use anyhow::{Context, Result};
use std::time::{Duration, UNIX_EPOCH};