use core::time::Duration;
use anyhow::{bail, ensure, Context, Result};
use rustls_pki_types::UnixTime;
use scale::Decode;
#[cfg(feature = "default-x509")]
use crate::policy::{PckIdentity, PlatformInfo, Policy, QeInfo, QuoteClaims, TcbVerdict};
#[cfg(feature = "default-x509")]
use crate::utils::{parse_crl_info, CrlInfo};
use {
crate::constants::*,
crate::policy::PckCertFlag,
crate::qe_identity::{QeIdentity, QeTcbLevel},
crate::tcb_info::{
TcbInfo, TcbLevel, TcbStatus, TcbStatusWithAdvisory, TdxModuleIdentity, TdxModuleTcbLevel,
},
alloc::string::{String, ToString},
alloc::vec::Vec,
};
pub use crate::quote::{AuthData, EnclaveReport, Quote};
use crate::{
config::{Config, CryptoProvider},
quote::{Report, TDAttributes},
utils::{
encode_as_der_with, extract_certs, parse_crls, parse_rfc3339_unix_secs,
verify_certificate_chain,
},
};
use crate::{
quote::{TDReport10, TDReport15},
QuoteCollateralV3,
};
use rustls_pki_types::CertificateDer;
use serde::{Deserialize, Serialize};
pub struct CryptoBackend {
pub sig_algo: &'static dyn rustls_pki_types::SignatureVerificationAlgorithm,
pub sha256: fn(&[u8]) -> [u8; 32],
pub sha384: fn(&[u8]) -> [u8; 48],
pub encode_ecdsa: fn(&[u8]) -> Result<Vec<u8>>,
pub parse_pck_extension: fn(&[u8]) -> Result<crate::intel::PckExtension>,
}
fn backend_for<C: Config>() -> CryptoBackend {
CryptoBackend {
sig_algo: C::Crypto::sig_algo(),
sha256: C::Crypto::sha256,
sha384: C::Crypto::sha384,
encode_ecdsa: encode_as_der_with::<C>,
parse_pck_extension: crate::intel::parse_pck_extension_with::<C>,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TeeType {
Sgx,
Tdx,
}
impl TeeType {
fn from_u32(value: u32) -> Result<Self> {
match value {
TEE_TYPE_SGX => Ok(TeeType::Sgx),
TEE_TYPE_TDX => Ok(TeeType::Tdx),
_ => bail!("Unsupported TEE type: {value}"),
}
}
fn is_tdx(&self) -> bool {
matches!(self, TeeType::Tdx)
}
}
#[cfg(feature = "js")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "js")]
fn format_error_chain(e: &anyhow::Error) -> String {
use alloc::format;
let mut msg = format!("{}", e);
let mut source = e.source();
while let Some(err) = source {
msg.push_str(&format!("\n Caused by: {}", err));
source = err.source();
}
msg
}
#[cfg(feature = "borsh_schema")]
use borsh::BorshSchema;
#[cfg(feature = "borsh")]
use borsh::{BorshDeserialize, BorshSerialize};
use core::marker::PhantomData;
#[cfg_attr(not(feature = "default-x509"), allow(dead_code))]
struct QuoteVerificationResult {
header: crate::quote::Header,
report: Report,
collateral: QuoteCollateralV3,
tcb_info: TcbInfo,
qe_identity: QeIdentity,
tcb_info_path: Vec<CertificateDer<'static>>,
qe_identity_path: Vec<CertificateDer<'static>>,
root_ca_der: Vec<u8>,
tee_type: u32,
tcb_status: TcbStatus,
advisory_ids: Vec<String>,
platform_tcb_level: TcbLevel,
current_tcb_level: Option<TcbLevel>,
qe_tcb_level: QeTcbLevel,
pck_ext: PckCertChainResult,
qe_report: EnclaveReport,
tcb_eval_data_number: u32,
qe_tcb_eval_data_number: u32,
root_key_id: [u8; 48],
}
impl QuoteVerificationResult {
#[cfg(feature = "default-x509")]
pub fn claims(&self) -> Result<QuoteClaims> {
let root_ca_crl = parse_crl_info(&self.collateral.root_ca_crl)
.context("Failed to parse root CA CRL for claims")?;
let pck_crl = parse_crl_info(&self.collateral.pck_crl)
.context("Failed to parse PCK CRL for claims")?;
let root_ca = &self.root_ca_der[..];
let collateral_dates = compute_collateral_time_window(
&self.tcb_info,
&self.qe_identity,
&[&root_ca_crl, &pck_crl],
self.pck_ext
.pck_cert_chain_der
.iter()
.map(|cert| &cert[..])
.chain(self.tcb_info_path.iter().map(|cert| &cert[..])),
self.qe_identity_path
.iter()
.map(|cert| &cert[..])
.chain([root_ca]),
)?;
let root_key_id = self.root_key_id;
let tcb_date_tag = parse_rfc3339_unix_secs(&self.platform_tcb_level.tcb_date)
.context("Failed to parse platform TCB date")?;
Ok(QuoteClaims {
claims_version: 1,
header: self.header,
tee_type: self.tee_type,
tcb: TcbVerdict {
status: self.tcb_status,
advisory_ids: self.advisory_ids.clone(),
eval_data_number: self.tcb_eval_data_number,
},
platform: PlatformInfo {
tcb_level: self.platform_tcb_level.clone(),
tcb_date_tag,
current_tcb_level: self.current_tcb_level.clone(),
pck: PckIdentity {
ppid: self.pck_ext.ppid.clone(),
cpu_svn: self.pck_ext.cpu_svn,
pce_svn: self.pck_ext.pce_svn,
pce_id: self.pck_ext.pce_id.clone(),
fmspc: self.pck_ext.fmspc,
sgx_type: self.pck_ext.sgx_type,
platform_instance_id: self.pck_ext.platform_instance_id,
dynamic_platform: self.pck_ext.dynamic_platform,
cached_keys: self.pck_ext.cached_keys,
smt_enabled: self.pck_ext.smt_enabled,
platform_provider_id: None,
},
root_key_id: root_key_id.to_vec(),
pck_crl_num: pck_crl.number,
root_ca_crl_num: root_ca_crl.number,
},
qe: QeInfo {
tcb_level: self.qe_tcb_level.clone(),
report: self.qe_report,
tcb_eval_data_number: self.qe_tcb_eval_data_number,
},
report: self.report.clone(),
earliest_issue_date: collateral_dates.all.earliest_issue,
latest_issue_date: collateral_dates.all.latest_issue,
earliest_expiration_date: collateral_dates.all.earliest_expiration,
qe_iden_earliest_issue_date: collateral_dates.qe_identity.earliest_issue,
qe_iden_latest_issue_date: collateral_dates.qe_identity.latest_issue,
qe_iden_earliest_expiration_date: collateral_dates.qe_identity.earliest_expiration,
})
}
pub fn into_report_unchecked(self) -> VerifiedReport {
let platform_status = TcbStatusWithAdvisory::new(
self.platform_tcb_level.tcb_status,
self.platform_tcb_level.advisory_ids.clone(),
);
let qe_status = TcbStatusWithAdvisory::new(
self.qe_tcb_level.tcb_status,
self.qe_tcb_level.advisory_ids.clone(),
);
VerifiedReport {
status: self.tcb_status.to_string(),
advisory_ids: self.advisory_ids,
report: self.report,
ppid: self.pck_ext.ppid,
platform_status,
qe_status,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
pub struct VerifiedReport {
pub status: String,
pub advisory_ids: Vec<String>,
pub report: Report,
#[serde(with = "serde_bytes")]
pub ppid: Vec<u8>,
pub qe_status: TcbStatusWithAdvisory,
pub platform_status: TcbStatusWithAdvisory,
}
#[cfg(all(feature = "default-x509", feature = "_anycrypto"))]
pub struct QuoteVerifier<C: Config = crate::configs::DefaultConfig> {
root_ca_der: Vec<u8>,
allow_service_td: bool,
allow_debug: bool,
config: PhantomData<C>,
}
#[cfg(not(all(feature = "default-x509", feature = "_anycrypto")))]
pub struct QuoteVerifier<C: Config> {
root_ca_der: Vec<u8>,
allow_service_td: bool,
allow_debug: bool,
config: PhantomData<C>,
}
#[cfg(all(feature = "default-x509", feature = "_anycrypto"))]
impl QuoteVerifier<crate::configs::DefaultConfig> {
pub fn new(root_ca_der: Vec<u8>) -> Self {
Self {
root_ca_der,
allow_service_td: false,
allow_debug: false,
config: PhantomData,
}
}
pub fn new_prod() -> Self {
Self::new(TRUSTED_ROOT_CA_DER.to_vec())
}
}
impl<C: Config> QuoteVerifier<C> {
pub fn new_with_config(root_ca_der: Vec<u8>) -> Self {
Self {
root_ca_der,
allow_service_td: false,
allow_debug: false,
config: PhantomData,
}
}
pub fn with_config<D: Config>(self) -> QuoteVerifier<D> {
QuoteVerifier {
root_ca_der: self.root_ca_der,
allow_service_td: self.allow_service_td,
allow_debug: self.allow_debug,
config: PhantomData,
}
}
pub fn allow_service_td(mut self, allow: bool) -> Self {
self.allow_service_td = allow;
self
}
pub fn allow_debug(mut self, allow: bool) -> Self {
self.allow_debug = allow;
self
}
#[cfg(feature = "default-x509")]
pub fn verify_with_policy<P: Policy + ?Sized>(
&self,
raw_quote: &[u8],
collateral: impl Into<QuoteCollateralV3>,
now_secs: u64,
policy: &P,
) -> Result<QuoteClaims> {
let claims = self
.verify_result(raw_quote, collateral, now_secs)?
.claims()?;
policy.validate(&claims)?;
Ok(claims)
}
fn verify_result(
&self,
raw_quote: &[u8],
collateral: impl Into<QuoteCollateralV3>,
now_secs: u64,
) -> Result<QuoteVerificationResult> {
let backend = backend_for::<C>();
verify_impl(
raw_quote,
collateral.into(),
now_secs,
&self.root_ca_der,
&backend,
self.allow_service_td,
self.allow_debug,
#[cfg(feature = "danger-allow-tcb-override")]
None::<fn(TcbInfo) -> TcbInfo>,
)
}
pub fn verify(
&self,
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
) -> Result<VerifiedReport> {
self.verify_result(raw_quote, collateral, now_secs)
.map(QuoteVerificationResult::into_report_unchecked)
}
#[cfg(all(feature = "danger-allow-tcb-override", feature = "default-x509"))]
pub fn dangerous_verify_claims_with_tcb_override(
&self,
raw_quote: &[u8],
collateral: impl Into<QuoteCollateralV3>,
now_secs: u64,
override_tcb_info: impl FnOnce(TcbInfo) -> TcbInfo,
) -> Result<QuoteClaims> {
verify_impl(
raw_quote,
collateral.into(),
now_secs,
&self.root_ca_der,
&backend_for::<C>(),
self.allow_service_td,
self.allow_debug,
Some(override_tcb_info),
)?
.claims()
}
#[cfg(feature = "danger-allow-tcb-override")]
fn dangerous_verify_result_with_tcb_override<F: FnOnce(TcbInfo) -> TcbInfo>(
&self,
raw_quote: &[u8],
collateral: impl Into<QuoteCollateralV3>,
now_secs: u64,
override_tcb_info: F,
) -> Result<QuoteVerificationResult> {
let backend = backend_for::<C>();
verify_impl(
raw_quote,
collateral.into(),
now_secs,
&self.root_ca_der,
&backend,
self.allow_service_td,
self.allow_debug,
Some(override_tcb_info),
)
}
#[cfg(feature = "danger-allow-tcb-override")]
pub fn dangerous_verify_with_tcb_override(
&self,
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
override_tcb_info: impl FnOnce(TcbInfo) -> TcbInfo,
) -> Result<VerifiedReport> {
self.dangerous_verify_result_with_tcb_override(
raw_quote,
collateral,
now_secs,
override_tcb_info,
)
.map(QuoteVerificationResult::into_report_unchecked)
}
}
#[cfg(all(feature = "default-x509", feature = "_anycrypto"))]
pub fn verify(
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
) -> Result<VerifiedReport> {
QuoteVerifier::<crate::configs::DefaultConfig>::new_prod()
.verify(raw_quote, collateral, now_secs)
}
#[cfg(all(
feature = "default-x509",
feature = "_anycrypto",
feature = "danger-allow-tcb-override"
))]
pub fn dangerous_verify_with_tcb_override(
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
override_tcb_info: impl FnOnce(TcbInfo) -> TcbInfo,
) -> Result<VerifiedReport> {
QuoteVerifier::<crate::configs::DefaultConfig>::new_prod().dangerous_verify_with_tcb_override(
raw_quote,
collateral,
now_secs,
override_tcb_info,
)
}
#[cfg(feature = "js")]
#[wasm_bindgen(js_name = "QuotePolicy")]
pub struct JsQuotePolicy {
inner: crate::policy::QuotePolicy,
}
#[cfg(feature = "js")]
fn js_parse_tcb_status(s: &str) -> Result<TcbStatus, JsValue> {
match s {
"UpToDate" => Ok(TcbStatus::UpToDate),
"SWHardeningNeeded" => Ok(TcbStatus::SWHardeningNeeded),
"ConfigurationNeeded" => Ok(TcbStatus::ConfigurationNeeded),
"ConfigurationAndSWHardeningNeeded" => Ok(TcbStatus::ConfigurationAndSWHardeningNeeded),
"OutOfDate" => Ok(TcbStatus::OutOfDate),
"OutOfDateConfigurationNeeded" => Ok(TcbStatus::OutOfDateConfigurationNeeded),
"Revoked" => Ok(TcbStatus::Revoked),
"TDRelaunchAdvised" => Ok(TcbStatus::TDRelaunchAdvised),
"TDRelaunchAdvisedConfigurationNeeded" => {
Ok(TcbStatus::TDRelaunchAdvisedConfigurationNeeded)
}
_ => Err(JsValue::from_str(&alloc::format!(
"Unknown TCB status: {s}"
))),
}
}
#[cfg(feature = "js")]
#[wasm_bindgen(js_class = "QuotePolicy")]
impl JsQuotePolicy {
#[wasm_bindgen(constructor)]
pub fn strict(now_secs: u64) -> Self {
Self {
inner: crate::policy::QuotePolicy::strict(now_secs),
}
}
#[wasm_bindgen(js_name = "claimsOnly")]
pub fn claims_only(now_secs: u64) -> Self {
Self {
inner: crate::policy::QuotePolicy::claims_only(now_secs),
}
}
pub fn allow_status(self, status: &str) -> Result<JsQuotePolicy, JsValue> {
let s = js_parse_tcb_status(status)?;
Ok(Self {
inner: self.inner.allow_status(s),
})
}
pub fn reject_advisory(self, id: &str) -> Self {
Self {
inner: self.inner.reject_advisory(id),
}
}
pub fn reject_advisories(self, ids: Vec<String>) -> Self {
Self {
inner: self.inner.reject_advisories(&ids),
}
}
pub fn platform_grace_period(self, secs: u64) -> Self {
Self {
inner: self.inner.platform_grace_period(Duration::from_secs(secs)),
}
}
pub fn qe_grace_period(self, secs: u64) -> Self {
Self {
inner: self.inner.qe_grace_period(Duration::from_secs(secs)),
}
}
pub fn min_tcb_eval_data_number(self, min: u32) -> Self {
Self {
inner: self.inner.min_tcb_eval_data_number(min),
}
}
pub fn allow_dynamic_platform(self, allow: bool) -> Self {
Self {
inner: self.inner.allow_dynamic_platform(allow),
}
}
pub fn allow_cached_keys(self, allow: bool) -> Self {
Self {
inner: self.inner.allow_cached_keys(allow),
}
}
pub fn allow_smt(self, allow: bool) -> Self {
Self {
inner: self.inner.allow_smt(allow),
}
}
pub fn accepted_sgx_types(self, types: Vec<u8>) -> Self {
Self {
inner: self.inner.accepted_sgx_types(&types),
}
}
}
#[cfg(feature = "js")]
#[wasm_bindgen(js_name = "QuoteVerifier")]
pub struct JsQuoteVerifier {
inner: QuoteVerifier,
}
#[cfg(feature = "js")]
#[wasm_bindgen(js_class = "QuoteVerifier")]
impl JsQuoteVerifier {
#[wasm_bindgen(constructor)]
pub fn new(root_ca_der: Option<Vec<u8>>) -> Self {
let inner = match root_ca_der {
Some(der) => QuoteVerifier::new(der),
None => QuoteVerifier::new_prod(),
};
Self { inner }
}
pub fn verify(
&self,
raw_quote: JsValue,
quote_collateral: JsValue,
now: u64,
) -> Result<JsValue, JsValue> {
let raw_quote: Vec<u8> = serde_wasm_bindgen::from_value(raw_quote)
.map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?;
let quote_collateral =
serde_wasm_bindgen::from_value::<QuoteCollateralV3>(quote_collateral)?;
let report = self
.inner
.verify(&raw_quote, "e_collateral, now)
.map_err(|e| JsValue::from_str(&format_error_chain(&e)))?;
serde_wasm_bindgen::to_value(&report)
.map_err(|_| JsValue::from_str("Failed to encode verified report"))
}
pub fn verify_with_policy(
&self,
raw_quote: JsValue,
quote_collateral: JsValue,
now: u64,
policy: &JsQuotePolicy,
) -> Result<JsValue, JsValue> {
let raw_quote: Vec<u8> = serde_wasm_bindgen::from_value(raw_quote)
.map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?;
let quote_collateral =
serde_wasm_bindgen::from_value::<QuoteCollateralV3>(quote_collateral)?;
let claims = self
.inner
.verify_with_policy(&raw_quote, quote_collateral, now, &policy.inner)
.map_err(|e| JsValue::from_str(&format_error_chain(&e)))?;
serde_wasm_bindgen::to_value(&claims)
.map_err(|_| JsValue::from_str("Failed to encode quote claims"))
}
pub async fn get_collateral(pccs_url: &str, raw_quote: JsValue) -> Result<JsValue, JsValue> {
let raw_quote: Vec<u8> = serde_wasm_bindgen::from_value(raw_quote)
.map_err(|_| JsValue::from_str("Failed to decode raw_quote"))?;
let collateral: QuoteCollateralV3 =
crate::collateral::CollateralClient::with_default_http(pccs_url)
.map_err(|e| JsValue::from_str(&format_error_chain(&e)))?
.fetch(&raw_quote)
.await
.map_err(|e| JsValue::from_str(&format_error_chain(&e)))?;
serde_wasm_bindgen::to_value(&collateral)
.map_err(|_| JsValue::from_str("Failed to encode collateral"))
}
}
fn verify_tcb_info_signature(
collateral: &QuoteCollateralV3,
tcb_certs: &[CertificateDer<'_>],
now: UnixTime,
crls: &[webpki::CertRevocationList<'_>],
trust_anchor: rustls_pki_types::TrustAnchor,
backend: &CryptoBackend,
) -> Result<(TcbInfo, Vec<CertificateDer<'static>>)> {
let tcb_info = serde_json::from_str::<TcbInfo>(&collateral.tcb_info)
.context("Failed to decode TcbInfo")?;
let issue_date = parse_rfc3339_unix_secs(&tcb_info.issue_date)
.context("Failed to parse TCB Info issue date")?;
let next_update = parse_rfc3339_unix_secs(&tcb_info.next_update)
.context("Failed to parse TCB Info next update")?;
if now.as_secs() < issue_date {
bail!("TCBInfo issue date is in the future");
}
if now.as_secs() > next_update {
bail!("TCBInfo expired");
}
let [tcb_leaf, tcb_chain @ ..] = tcb_certs else {
bail!("Certificate chain is too short for TCB Info");
};
let tcb_leaf_cert = webpki::EndEntityCert::try_from(tcb_leaf)
.map_err(anyhow::Error::msg)
.context("Failed to parse TCB Info leaf certificate")?;
let tcb_path = verify_certificate_chain(
&tcb_leaf_cert,
tcb_chain,
now,
crls,
trust_anchor,
backend.sig_algo,
)?;
let asn1_signature = (backend.encode_ecdsa)(&collateral.tcb_info_signature)?;
if tcb_leaf_cert
.verify_signature(
backend.sig_algo,
collateral.tcb_info.as_bytes(),
&asn1_signature,
)
.is_err()
{
bail!("Signature is invalid for tcb_info in quote_collateral");
}
Ok((tcb_info, tcb_path))
}
fn verify_qe_identity_signature(
collateral: &QuoteCollateralV3,
qe_id_certs: &[CertificateDer<'_>],
tcb_info_certs: &[CertificateDer<'_>],
now: UnixTime,
crls: &[webpki::CertRevocationList<'_>],
trust_anchor: rustls_pki_types::TrustAnchor,
backend: &CryptoBackend,
) -> Result<(QeIdentity, Option<Vec<CertificateDer<'static>>>)> {
let qe_identity = serde_json::from_str::<QeIdentity>(&collateral.qe_identity)
.context("Failed to decode QeIdentity")?;
let issue_date = parse_rfc3339_unix_secs(&qe_identity.issue_date)
.context("Failed to parse QE Identity issue date")?;
let next_update = parse_rfc3339_unix_secs(&qe_identity.next_update)
.context("Failed to parse QE Identity next update")?;
if now.as_secs() < issue_date {
bail!("QE Identity issue date is in the future");
}
if now.as_secs() > next_update {
bail!("QE Identity expired");
}
let [qe_id_leaf, qe_id_chain @ ..] = qe_id_certs else {
bail!("Certificate chain is too short for QE Identity");
};
let qe_id_leaf_cert = webpki::EndEntityCert::try_from(qe_id_leaf)
.map_err(anyhow::Error::msg)
.context("Failed to parse QE Identity leaf certificate")?;
let qe_id_path = if qe_id_certs == tcb_info_certs {
None
} else {
Some(verify_certificate_chain(
&qe_id_leaf_cert,
qe_id_chain,
now,
crls,
trust_anchor,
backend.sig_algo,
)?)
};
let qe_id_asn1_signature = (backend.encode_ecdsa)(&collateral.qe_identity_signature)?;
if qe_id_leaf_cert
.verify_signature(
backend.sig_algo,
collateral.qe_identity.as_bytes(),
&qe_id_asn1_signature,
)
.is_err()
{
bail!("Signature is invalid for qe_identity in quote_collateral");
}
Ok((qe_identity, qe_id_path))
}
fn verify_pck_cert_chain(
collateral: &QuoteCollateralV3,
certification_data: &crate::quote::CertificationData,
now: UnixTime,
crls: &[webpki::CertRevocationList<'_>],
trust_anchor: rustls_pki_types::TrustAnchor,
backend: &CryptoBackend,
) -> Result<PckCertChainResult> {
let certification_certs = if let Some(pem_chain) = &collateral.pck_certificate_chain {
extract_certs(pem_chain.as_bytes())
.context("Failed to extract PCK certificates from collateral")?
} else {
if certification_data.cert_type != PCK_CERT_CHAIN {
bail!("Unsupported DCAP PCK cert format: {}. Use get_collateral() to fetch PCK certificate.", certification_data.cert_type);
}
extract_certs(&certification_data.body.data)
.context("Failed to extract PCK certificates from quote")?
};
let [pck_leaf, pck_chain @ ..] = &certification_certs[..] else {
bail!("Certificate chain is too short in quote");
};
let pck_leaf_cert = webpki::EndEntityCert::try_from(pck_leaf)
.map_err(anyhow::Error::msg)
.context("Failed to parse PCK certificate")?;
let pck_path = verify_certificate_chain(
&pck_leaf_cert,
pck_chain,
now,
crls,
trust_anchor,
backend.sig_algo,
)?;
let pck_ext = (backend.parse_pck_extension)(pck_leaf)?;
let pce_id = pck_ext.pce_id.clone();
let platform_instance_id = pck_ext.platform_instance_id.as_ref().and_then(|v| {
let arr: [u8; 16] = v.as_slice().try_into().ok()?;
Some(arr)
});
Ok(PckCertChainResult {
pck_cert_chain_der: pck_path.iter().map(|cert| cert.to_vec()).collect(),
pck_leaf_der: pck_leaf.as_ref().to_vec(),
ppid: pck_ext.ppid,
cpu_svn: pck_ext.cpu_svn,
pce_svn: pck_ext.pce_svn,
fmspc: pck_ext.fmspc,
pce_id,
sgx_type: pck_ext.sgx_type as u8,
platform_instance_id,
dynamic_platform: pck_ext.dynamic_platform.into(),
cached_keys: pck_ext.cached_keys.into(),
smt_enabled: pck_ext.smt_enabled.into(),
})
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))]
#[cfg_attr(feature = "borsh_schema", derive(BorshSchema))]
struct PckCertChainResult {
#[serde(with = "crate::utils::serde_vec_bytes")]
pck_cert_chain_der: Vec<Vec<u8>>,
#[serde(with = "serde_bytes")]
pck_leaf_der: Vec<u8>,
#[serde(with = "serde_bytes")]
ppid: Vec<u8>,
#[serde(with = "serde_bytes")]
cpu_svn: [u8; 16],
pce_svn: u16,
#[serde(with = "serde_bytes")]
fmspc: [u8; 6],
#[serde(with = "serde_bytes")]
pce_id: Vec<u8>,
sgx_type: u8,
platform_instance_id: Option<[u8; 16]>,
dynamic_platform: PckCertFlag,
cached_keys: PckCertFlag,
smt_enabled: PckCertFlag,
}
fn verify_qe_report_signature(
pck_leaf: &CertificateDer,
auth_data: &crate::quote::AuthDataV3,
backend: &CryptoBackend,
) -> Result<EnclaveReport> {
let pck_leaf_cert = webpki::EndEntityCert::try_from(pck_leaf)
.map_err(anyhow::Error::msg)
.context("Failed to parse PCK certificate")?;
let qe_report_signature = (backend.encode_ecdsa)(&auth_data.qe_report_signature)?;
if pck_leaf_cert
.verify_signature(backend.sig_algo, &auth_data.qe_report, &qe_report_signature)
.is_err()
{
bail!("Signature is invalid for qe_report in quote");
}
let mut qe_report_slice = auth_data.qe_report.as_slice();
let qe_report = EnclaveReport::decode(&mut qe_report_slice)
.map_err(anyhow::Error::msg)
.context("Failed to decode QE report")?;
Ok(qe_report)
}
fn verify_qe_report_data(
qe_report: &EnclaveReport,
auth_data: &crate::quote::AuthDataV3,
backend: &CryptoBackend,
) -> Result<()> {
use crate::constants::{ATTESTATION_KEY_LEN, AUTHENTICATION_DATA_LEN};
ensure!(
auth_data.qe_auth_data.data.len() == AUTHENTICATION_DATA_LEN,
"Invalid QE auth data length"
);
let mut qe_hash_data = [0u8; ATTESTATION_KEY_LEN + AUTHENTICATION_DATA_LEN];
qe_hash_data[..ATTESTATION_KEY_LEN].copy_from_slice(&auth_data.ecdsa_attestation_key);
qe_hash_data[ATTESTATION_KEY_LEN..].copy_from_slice(&auth_data.qe_auth_data.data);
let qe_hash = (backend.sha256)(&qe_hash_data);
if qe_hash[..] != qe_report.report_data[..32] {
bail!("QE report hash mismatch");
}
Ok(())
}
fn verify_isv_report_signature(
raw_quote: &[u8],
quote: &Quote,
auth_data: &crate::quote::AuthDataV3,
backend: &CryptoBackend,
) -> Result<()> {
let mut pub_key = [0x04u8; 65];
pub_key[1..].copy_from_slice(&auth_data.ecdsa_attestation_key);
let der_sig = (backend.encode_ecdsa)(&auth_data.ecdsa_signature)?;
let signed_data = raw_quote
.get(..quote.signed_length())
.context("Failed to get signed quote scope")?;
backend
.sig_algo
.verify_signature(&pub_key, signed_data, &der_sig)
.map_err(|_| anyhow::anyhow!("ISV enclave report signature is invalid"))
}
fn match_platform_tcb(
tcb_info: &TcbInfo,
quote: &Quote,
tee_type: TeeType,
cpu_svn: &[u8],
pce_svn: u16,
fmspc: &[u8],
) -> Result<(TcbLevel, Option<TcbLevel>)> {
let tcb_fmspc = hex::decode(&tcb_info.fmspc)
.ok()
.context("Failed to decode TCB FMSPC")?;
if fmspc[..] != tcb_fmspc[..] {
bail!("Fmspc mismatch");
}
match tee_type {
TeeType::Tdx => {
if tcb_info.version < 3 || tcb_info.id != "TDX" {
bail!("TDX quote with non-TDX TCB info in the collateral");
}
}
TeeType::Sgx => {
if tcb_info.version < 2 || tcb_info.id != "SGX" {
bail!("SGX quote with non-SGX TCB info in the collateral");
}
}
}
if !tee_type.is_tdx() {
let tcb_level = find_tcb_level(tcb_info, cpu_svn, pce_svn, None)?.clone();
return Ok((tcb_level, None));
}
let td_report = quote
.report
.as_td10()
.context("Failed to get TD10 report")?;
let tcb_level = evaluate_tdx_tcb(tcb_info, cpu_svn, pce_svn, &td_report.tee_tcb_svn)?;
verify_tdx_module_identity(tcb_info, td_report).context("TDX module identity check")?;
let current_tcb_level = quote
.report
.as_td15()
.map(|td15| {
evaluate_tdx_tcb(tcb_info, cpu_svn, pce_svn, &td15.tee_tcb_svn2)
.context("Failed to evaluate the current TDX TCB (TEE_TCB_SVN2)")
})
.transpose()?;
Ok((tcb_level, current_tcb_level))
}
const TCB_INFO_STATUSES: &[TcbStatus] = &[
TcbStatus::UpToDate,
TcbStatus::OutOfDate,
TcbStatus::ConfigurationNeeded,
TcbStatus::Revoked,
TcbStatus::OutOfDateConfigurationNeeded,
TcbStatus::SWHardeningNeeded,
TcbStatus::ConfigurationAndSWHardeningNeeded,
];
const TDX_MODULE_STATUSES: &[TcbStatus] = &[
TcbStatus::UpToDate,
TcbStatus::OutOfDate,
TcbStatus::Revoked,
];
const QE_IDENTITY_STATUSES: &[TcbStatus] = &[
TcbStatus::UpToDate,
TcbStatus::OutOfDate,
TcbStatus::ConfigurationNeeded,
TcbStatus::Revoked,
TcbStatus::OutOfDateConfigurationNeeded,
];
fn ensure_valid_status(status: TcbStatus, valid: &[TcbStatus], structure: &str) -> Result<()> {
ensure!(
valid.contains(&status),
"Unrecognized TCB status {status} in {structure}"
);
Ok(())
}
fn find_tcb_level<'a>(
tcb_info: &'a TcbInfo,
cpu_svn: &[u8],
pce_svn: u16,
tee_tcb_svn: Option<&[u8; 16]>,
) -> Result<&'a TcbLevel> {
for tcb_level in &tcb_info.tcb_levels {
if pce_svn < tcb_level.tcb.pce_svn {
continue;
}
let sgx_components: Vec<u8> = tcb_level.tcb.sgx_components.iter().map(|c| c.svn).collect();
if sgx_components.len() != cpu_svn.len() {
bail!(
"SGX component count mismatch: expected {}, got {}",
cpu_svn.len(),
sgx_components.len()
);
}
if cpu_svn.iter().zip(&sgx_components).any(|(a, b)| a < b) {
continue;
}
if let Some(tee_tcb_svn) = tee_tcb_svn {
let tdx_components: Vec<u8> =
tcb_level.tcb.tdx_components.iter().map(|c| c.svn).collect();
if tdx_components.len() != tee_tcb_svn.len() {
bail!(
"TDX component count mismatch: expected {}, got {}",
tee_tcb_svn.len(),
tdx_components.len()
);
}
let skip = if tee_tcb_svn[1] > 0 { 2 } else { 0 };
if tee_tcb_svn
.iter()
.zip(&tdx_components)
.skip(skip)
.any(|(a, b)| a < b)
{
continue;
}
}
ensure_valid_status(tcb_level.tcb_status, TCB_INFO_STATUSES, "TCB Info")?;
return Ok(tcb_level);
}
bail!("No matching TCB level found");
}
fn evaluate_tdx_tcb(
tcb_info: &TcbInfo,
cpu_svn: &[u8],
pce_svn: u16,
tee_tcb_svn: &[u8; 16],
) -> Result<TcbLevel> {
let mut tcb_level = find_tcb_level(tcb_info, cpu_svn, pce_svn, Some(tee_tcb_svn))?.clone();
let Some(module_level) =
match_tdx_module_level(tcb_info, tee_tcb_svn).context("TDX module TCB level check")?
else {
return Ok(tcb_level);
};
tcb_level.tcb_status = tcb_level
.tcb_status
.converge_with_component(module_level.tcb_status);
for advisory in &module_level.advisory_ids {
if !tcb_level.advisory_ids.contains(advisory) {
tcb_level.advisory_ids.push(advisory.clone());
}
}
if parse_rfc3339_unix_secs(&module_level.tcb_date).context("Invalid TDX module tcbDate")?
< parse_rfc3339_unix_secs(&tcb_level.tcb_date).context("Invalid TCB level tcbDate")?
{
tcb_level.tcb_date = module_level.tcb_date.clone();
}
Ok(tcb_level)
}
fn tdx_module_identity<'a>(
tcb_info: &'a TcbInfo,
tee_tcb_svn: &[u8; 16],
) -> Result<Option<&'a TdxModuleIdentity>> {
let module_version = tee_tcb_svn[1];
if module_version == 0 {
return Ok(None);
}
let wanted_id = format!("TDX_{:02X}", module_version);
tcb_info
.tdx_module_identities
.iter()
.find(|id| id.id.eq_ignore_ascii_case(&wanted_id))
.with_context(|| format!("No TDX module identity with id {wanted_id} found in TCB Info"))
.map(Some)
}
fn match_tdx_module_level<'a>(
tcb_info: &'a TcbInfo,
tee_tcb_svn: &[u8; 16],
) -> Result<Option<&'a TdxModuleTcbLevel>> {
let Some(identity) = tdx_module_identity(tcb_info, tee_tcb_svn)? else {
return Ok(None);
};
let module_isvsvn = tee_tcb_svn[0];
let level = identity
.tcb_levels
.iter()
.find(|level| module_isvsvn >= level.tcb.isvsvn)
.with_context(|| {
format!(
"TDX module ISVSVN {module_isvsvn} is below minimum required from TDX module TCB levels"
)
})?;
ensure_valid_status(level.tcb_status, TDX_MODULE_STATUSES, "TDX module identity")?;
Ok(Some(level))
}
fn verify_tdx_module_identity(tcb_info: &TcbInfo, td_report: &TDReport10) -> Result<()> {
let base_module = match &tcb_info.tdx_module {
Some(m) => m,
None => {
bail!("TDX TCB Info is missing tdxModule field");
}
};
fn decode_hex_array<const N: usize>(hex_str: &str, field: &str) -> Result<[u8; N]> {
let bytes = hex::decode(hex_str)
.map_err(|e| anyhow::anyhow!("Failed to decode {field} as hex: {e}"))?;
ensure!(
bytes.len() == N,
"{field} has invalid length {}, expected {N}",
bytes.len()
);
let mut arr = [0u8; N];
arr.copy_from_slice(&bytes);
Ok(arr)
}
let mut expected_mrsigner =
decode_hex_array::<48>(&base_module.mrsigner, "tdxModule.mrsigner")?;
let mut expected_attributes =
decode_hex_array::<8>(&base_module.attributes, "tdxModule.attributes")?;
let mut attributes_mask =
decode_hex_array::<8>(&base_module.attributes_mask, "tdxModule.attributesMask")?;
if let Some(identity) = tdx_module_identity(tcb_info, &td_report.tee_tcb_svn)? {
expected_mrsigner =
decode_hex_array::<48>(&identity.mrsigner, "tdxModuleIdentity.mrsigner")?;
expected_attributes =
decode_hex_array::<8>(&identity.attributes, "tdxModuleIdentity.attributes")?;
attributes_mask = decode_hex_array::<8>(
&identity.attributes_mask,
"tdxModuleIdentity.attributesMask",
)?;
}
if td_report.mr_signer_seam != expected_mrsigner {
bail!(
"TDX module MRSIGNER mismatch: expected {}, got {}",
hex::encode_upper(expected_mrsigner),
hex::encode_upper(td_report.mr_signer_seam)
);
}
for (i, ((expected, mask), actual)) in expected_attributes
.iter()
.zip(attributes_mask.iter())
.zip(td_report.seam_attributes.iter())
.enumerate()
{
let expected_masked = expected & mask;
let actual_masked = actual & mask;
if expected_masked != actual_masked {
bail!(
"TDX module SEAMATTRIBUTES mismatch at byte {}: expected {:02X} (masked), got {:02X} (masked)",
i,
expected_masked,
actual_masked
);
}
if actual & !mask != 0 {
bail!(
"TDX module SEAMATTRIBUTES has bits set outside mask at byte {}",
i
);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn verify_impl(
raw_quote: &[u8],
collateral: QuoteCollateralV3,
now_secs: u64,
root_ca_der: &[u8],
backend: &CryptoBackend,
allow_service_td: bool,
allow_debug: bool,
#[cfg(feature = "danger-allow-tcb-override")] override_tcb_info: Option<
impl FnOnce(TcbInfo) -> TcbInfo,
>,
) -> Result<QuoteVerificationResult> {
let root_ca = CertificateDer::from_slice(root_ca_der);
let trust_anchor = webpki::anchor_from_trusted_cert(&root_ca)
.map_err(anyhow::Error::msg)
.context("Failed to load root ca")?;
let now = UnixTime::since_unix_epoch(Duration::from_secs(now_secs));
let crls = parse_crls(
&collateral.root_ca_crl,
&collateral.pck_crl,
&trust_anchor.subject_public_key_info,
backend.sig_algo,
)?;
webpki::check_single_cert_revocation(root_ca_der, &crls.each_ref(), &[backend.sig_algo], now)
.map_err(anyhow::Error::msg)?;
let mut quote_slice = raw_quote;
let quote = Quote::decode(&mut quote_slice)
.map_err(anyhow::Error::msg)
.context("Failed to decode quote")?;
if !ALLOWED_QUOTE_VERSIONS.contains("e.header.version) {
bail!("Unsupported DCAP quote version");
}
if quote.header.qe_vendor_id != INTEL_QE_VENDOR_ID {
bail!("Unknown QE vendor ID");
}
let tee_type = TeeType::from_u32(quote.header.tee_type)?;
match tee_type {
TeeType::Sgx => {
if quote.header.version != 3 {
bail!("SGX TEE quote must have version 3");
}
}
TeeType::Tdx => {
if ![4, 5].contains("e.header.version) {
bail!("TDX TEE quote must have version 4 or 5");
}
}
}
if quote.header.attestation_key_type != ATTESTATION_KEY_TYPE_ECDSA256_WITH_P256_CURVE {
bail!("Unsupported DCAP attestation key type");
}
let auth_data = quote.auth_data.clone().into_v3();
let tcb_info_chain = extract_certs(collateral.tcb_info_issuer_chain.as_bytes())?;
let (mut tcb_info, tcb_info_path) = verify_tcb_info_signature(
&collateral,
&tcb_info_chain,
now,
&crls,
trust_anchor.clone(),
backend,
)?;
#[cfg(feature = "danger-allow-tcb-override")]
if let Some(override_tcb_info) = override_tcb_info {
tcb_info = override_tcb_info(tcb_info);
}
tcb_info.canonicalize_tcb_levels();
let qe_identity_chain = extract_certs(collateral.qe_identity_issuer_chain.as_bytes())?;
let (qe_identity, qe_identity_path) = verify_qe_identity_signature(
&collateral,
&qe_identity_chain,
&tcb_info_chain,
now,
&crls,
trust_anchor.clone(),
backend,
)?;
let (expected_qe_id, allowed_qe_versions): (&str, &[u8]) = match tee_type {
TeeType::Sgx => ("QE", &[2]),
TeeType::Tdx => ("TD_QE", &[2, 3]),
};
if qe_identity.id != expected_qe_id || !allowed_qe_versions.contains(&qe_identity.version) {
bail!(
"Unsupported QE Identity id/version for the quote TEE type: {} version {} (expected {} version {:?})",
qe_identity.id,
qe_identity.version,
expected_qe_id,
allowed_qe_versions
);
}
let pck_result = verify_pck_cert_chain(
&collateral,
&auth_data.certification_data,
now,
&crls,
trust_anchor,
backend,
)?;
let pck_leaf = CertificateDer::from(pck_result.pck_leaf_der.as_slice());
let qe_report = verify_qe_report_signature(&pck_leaf, &auth_data, backend)?;
verify_qe_report_data(&qe_report, &auth_data, backend)?;
let qe_tcb_level = verify_qe_identity_policy(&qe_report, &qe_identity)?;
verify_isv_report_signature(raw_quote, "e, &auth_data, backend)?;
let (platform_tcb_level, current_tcb_level) = match_platform_tcb(
&tcb_info,
"e,
tee_type,
&pck_result.cpu_svn,
pck_result.pce_svn,
&pck_result.fmspc,
)?;
let platform_status = TcbStatusWithAdvisory::new(
platform_tcb_level.tcb_status,
platform_tcb_level.advisory_ids.clone(),
);
let qe_status =
TcbStatusWithAdvisory::new(qe_tcb_level.tcb_status, qe_tcb_level.advisory_ids.clone());
let mut final_status = platform_status.merge(&qe_status);
let current_status = current_tcb_level.as_ref().map(|level| {
level
.tcb_status
.converge_with_component(qe_tcb_level.tcb_status)
});
if final_status.status == TcbStatus::Revoked || current_status == Some(TcbStatus::Revoked) {
bail!("TCB status is invalid: Revoked");
}
if let Some(current_status) = current_status {
final_status.status = final_status.status.check_for_relaunch(current_status);
}
#[cfg(feature = "default-x509")]
let root_key_id = {
let root_cert: x509_cert::Certificate =
der::Decode::from_der(root_ca_der).context("Failed to parse root CA certificate")?;
let raw_key = root_cert
.tbs_certificate()
.subject_public_key_info()
.subject_public_key
.raw_bytes();
(backend.sha384)(raw_key)
};
#[cfg(not(feature = "default-x509"))]
let root_key_id = [0u8; 48];
validate_attrs("e.report, allow_service_td, allow_debug)?;
Ok(QuoteVerificationResult {
header: quote.header,
report: quote.report,
collateral,
tee_type: quote.header.tee_type,
tcb_status: final_status.status,
advisory_ids: final_status.advisory_ids,
platform_tcb_level,
current_tcb_level,
qe_tcb_level,
pck_ext: pck_result,
qe_report,
tcb_eval_data_number: tcb_info
.tcb_evaluation_data_number
.min(qe_identity.tcb_evaluation_data_number),
qe_tcb_eval_data_number: qe_identity.tcb_evaluation_data_number,
root_key_id,
tcb_info,
qe_identity,
qe_identity_path: qe_identity_path.unwrap_or_else(|| tcb_info_path.clone()),
tcb_info_path,
root_ca_der: root_ca_der.to_vec(),
})
}
#[cfg(feature = "default-x509")]
#[derive(Clone, Copy)]
struct DateWindow {
earliest_issue: u64,
latest_issue: u64,
earliest_expiration: u64,
}
#[cfg(feature = "default-x509")]
impl DateWindow {
const EMPTY: Self = Self {
earliest_issue: u64::MAX,
latest_issue: 0,
earliest_expiration: u64::MAX,
};
fn add(&mut self, issue: u64, expiration: Option<u64>) {
self.earliest_issue = self.earliest_issue.min(issue);
self.latest_issue = self.latest_issue.max(issue);
if let Some(expiration) = expiration {
self.earliest_expiration = self.earliest_expiration.min(expiration);
}
}
}
#[cfg(feature = "default-x509")]
struct CollateralDates {
all: DateWindow,
qe_identity: DateWindow,
}
#[cfg(feature = "default-x509")]
fn compute_collateral_time_window<'a>(
tcb_info: &TcbInfo,
qe_identity: &QeIdentity,
crls: &[&CrlInfo],
certs: impl Iterator<Item = &'a [u8]>,
qe_identity_chain: impl Iterator<Item = &'a [u8]>,
) -> Result<CollateralDates> {
let mut validity_cache = alloc::collections::BTreeMap::<&[u8], (u64, u64)>::new();
let mut validity = |cert_der: &'a [u8]| -> Result<(u64, u64)> {
use der::Decode as _;
if let Some(validity) = validity_cache.get(cert_der) {
return Ok(*validity);
}
let cert = x509_cert::Certificate::from_der(cert_der)
.context("Failed to parse certificate for time window")?;
let validity = cert.tbs_certificate().validity();
let validity = (
validity.not_before.to_unix_duration().as_secs(),
validity.not_after.to_unix_duration().as_secs(),
);
validity_cache.insert(cert_der, validity);
Ok(validity)
};
let mut qe = DateWindow::EMPTY;
qe.add(
parse_rfc3339_unix_secs(&qe_identity.issue_date).context("QEIdentity issueDate")?,
Some(parse_rfc3339_unix_secs(&qe_identity.next_update).context("QEIdentity nextUpdate")?),
);
for cert in qe_identity_chain {
let (not_before, not_after) = validity(cert)?;
qe.add(not_before, Some(not_after));
}
let mut all = qe;
all.add(
parse_rfc3339_unix_secs(&tcb_info.issue_date).context("TCBInfo issueDate")?,
Some(parse_rfc3339_unix_secs(&tcb_info.next_update).context("TCBInfo nextUpdate")?),
);
for crl in crls {
all.add(crl.this_update, crl.next_update);
}
for cert in certs {
let (not_before, not_after) = validity(cert)?;
all.add(not_before, Some(not_after));
}
Ok(CollateralDates {
all,
qe_identity: qe,
})
}
fn validate_sgx_attrs(report: &EnclaveReport, allow_debug: bool) -> Result<()> {
let is_debug = report.attributes[0] & 0x02 != 0;
if is_debug && !allow_debug {
bail!("Debug mode is enabled");
}
Ok(())
}
fn validate_attrs(report: &Report, allow_service_td: bool, allow_debug: bool) -> Result<()> {
fn validate_td10(report: &TDReport10, allow_servtd_ext: bool, allow_debug: bool) -> Result<()> {
let td_attrs = TDAttributes::parse(report.td_attributes)
.map_err(anyhow::Error::msg)
.context("Failed to parse TD attributes")?;
if td_attrs.tud & 0x01 != 0 && !allow_debug {
bail!("Debug mode is enabled");
}
if td_attrs.tud & 0x70 != 0 && !allow_debug {
bail!("TD profiling is enabled");
}
if td_attrs.reserved != 0 {
bail!("Reserved bits in TD attributes are set");
}
if td_attrs.sec.migratable {
bail!("TD migration is enabled");
}
if td_attrs.sec.servtd_ext && !allow_servtd_ext {
bail!("SERVTD_EXT is enabled");
}
if !td_attrs.sec.sept_ve_disable {
bail!("SEPT_VE_DISABLE is not enabled");
}
Ok(())
}
fn validate_td15(report: &TDReport15, allow_service_td: bool, allow_debug: bool) -> Result<()> {
if !allow_service_td && report.mr_service_td != [0u8; 48] {
bail!("Invalid MR service TD");
}
validate_td10(&report.base, allow_service_td, allow_debug)
}
match &report {
Report::TD15(report) => validate_td15(report, allow_service_td, allow_debug),
Report::TD15Ex(report) => validate_td15(&report.base, allow_service_td, allow_debug),
Report::TD10(report) => validate_td10(report, false, allow_debug),
Report::SgxEnclave(report) => validate_sgx_attrs(report, allow_debug),
}
}
#[cfg(all(feature = "ring", feature = "default-x509"))]
pub mod ring {
use super::*;
fn ring_sha256(data: &[u8]) -> [u8; 32] {
let digest = ::ring::digest::digest(&::ring::digest::SHA256, data);
let mut out = [0u8; 32];
out.copy_from_slice(digest.as_ref());
out
}
fn ring_sha384(data: &[u8]) -> [u8; 48] {
let digest = ::ring::digest::digest(&::ring::digest::SHA384, data);
let mut out = [0u8; 48];
out.copy_from_slice(digest.as_ref());
out
}
pub fn backend() -> CryptoBackend {
CryptoBackend {
sig_algo: webpki::ring::ECDSA_P256_SHA256,
sha256: ring_sha256,
sha384: ring_sha384,
encode_ecdsa: encode_as_der_with::<crate::configs::RingConfig>,
parse_pck_extension: crate::intel::parse_pck_extension_with::<crate::configs::RingConfig>,
}
}
pub fn verify(
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
) -> Result<VerifiedReport> {
QuoteVerifier::new_prod()
.with_config::<crate::configs::RingConfig>()
.verify(raw_quote, collateral, now_secs)
}
#[cfg(feature = "danger-allow-tcb-override")]
pub fn dangerous_verify_with_tcb_override(
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
override_tcb_info: impl FnOnce(TcbInfo) -> TcbInfo,
) -> Result<VerifiedReport> {
QuoteVerifier::new_prod()
.with_config::<crate::configs::RingConfig>()
.dangerous_verify_with_tcb_override(raw_quote, collateral, now_secs, override_tcb_info)
}
}
#[cfg(all(feature = "rustcrypto", feature = "default-x509"))]
pub mod rustcrypto {
use super::*;
fn rustcrypto_sha256(data: &[u8]) -> [u8; 32] {
use sha2::Digest;
sha2::Sha256::digest(data).into()
}
fn rustcrypto_sha384(data: &[u8]) -> [u8; 48] {
use sha2::Digest;
sha2::Sha384::digest(data).into()
}
pub fn backend() -> CryptoBackend {
CryptoBackend {
sig_algo: webpki::rustcrypto::ECDSA_P256_SHA256,
sha256: rustcrypto_sha256,
sha384: rustcrypto_sha384,
encode_ecdsa: encode_as_der_with::<crate::configs::RustCryptoConfig>,
parse_pck_extension: crate::intel::parse_pck_extension_with::<
crate::configs::RustCryptoConfig,
>,
}
}
pub fn verify(
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
) -> Result<VerifiedReport> {
QuoteVerifier::new_prod()
.with_config::<crate::configs::RustCryptoConfig>()
.verify(raw_quote, collateral, now_secs)
}
#[cfg(feature = "danger-allow-tcb-override")]
pub fn dangerous_verify_with_tcb_override(
raw_quote: &[u8],
collateral: &QuoteCollateralV3,
now_secs: u64,
override_tcb_info: impl FnOnce(TcbInfo) -> TcbInfo,
) -> Result<VerifiedReport> {
QuoteVerifier::new_prod()
.with_config::<crate::configs::RustCryptoConfig>()
.dangerous_verify_with_tcb_override(raw_quote, collateral, now_secs, override_tcb_info)
}
}
fn verify_qe_identity_policy(
qe_report: &EnclaveReport,
qe_identity: &QeIdentity,
) -> Result<QeTcbLevel> {
if qe_report.mr_signer != qe_identity.mrsigner {
bail!(
"QE MRSIGNER mismatch: expected {}, got {}",
hex::encode_upper(qe_identity.mrsigner),
hex::encode_upper(qe_report.mr_signer)
);
}
validate_sgx_attrs(qe_report, false).context("QE report validation failed")?;
if qe_report.isv_prod_id != qe_identity.isvprodid {
bail!(
"QE ISVPRODID mismatch: expected {}, got {}",
qe_identity.isvprodid,
qe_report.isv_prod_id
);
}
let expected_miscselect_u32 = u32::from_le_bytes(qe_identity.miscselect);
let miscselect_mask_u32 = u32::from_le_bytes(qe_identity.miscselect_mask);
let qe_miscselect_masked = qe_report.misc_select & miscselect_mask_u32;
let expected_miscselect_masked = expected_miscselect_u32 & miscselect_mask_u32;
if qe_miscselect_masked != expected_miscselect_masked {
bail!(
"QE MISCSELECT mismatch: expected {:08X} (masked), got {:08X} (masked)",
expected_miscselect_masked,
qe_miscselect_masked
);
}
for (i, ((expected, mask), qe_attr)) in qe_identity
.attributes
.iter()
.zip(qe_identity.attributes_mask.iter())
.zip(qe_report.attributes.iter())
.enumerate()
{
let expected_masked = expected & mask;
let qe_masked = qe_attr & mask;
if expected_masked != qe_masked {
bail!(
"QE ATTRIBUTES mismatch at byte {}: expected {:02X} (masked), got {:02X} (masked)",
i,
expected_masked,
qe_masked
);
}
}
match_qe_tcb_level(qe_report.isv_svn, &qe_identity.tcb_levels)
}
fn match_qe_tcb_level(
isv_svn: u16,
tcb_levels: &[crate::qe_identity::QeTcbLevel],
) -> Result<QeTcbLevel> {
for tcb_level in tcb_levels {
if isv_svn >= tcb_level.tcb.isvsvn {
ensure_valid_status(tcb_level.tcb_status, QE_IDENTITY_STATUSES, "QE Identity")?;
return Ok(tcb_level.clone());
}
}
match tcb_levels.last().map(|l| l.tcb.isvsvn) {
Some(min_required) => {
bail!("QE ISVSVN {isv_svn} is below minimum required {min_required} from QE Identity");
}
None => {
bail!("No TCB levels found in QE Identity");
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tcb_info::TcbStatus::*;
use hex_literal::hex;
fn make_test_qe_report() -> EnclaveReport {
EnclaveReport {
cpu_svn: [0u8; 16],
misc_select: 0x00000000,
reserved1: [0u8; 28],
attributes: [
0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
],
mr_enclave: [0u8; 32],
reserved2: [0u8; 32],
mr_signer: hex::decode(
"8C4F5775D796503E96137F77C68A829A0056AC8DED70140B081B094490C57BFF",
)
.unwrap()
.try_into()
.unwrap(),
reserved3: [0u8; 96],
isv_prod_id: 1,
isv_svn: 8,
reserved4: [0u8; 60],
report_data: [0u8; 64],
}
}
fn make_test_qe_identity() -> QeIdentity {
use crate::qe_identity::{QeTcb, QeTcbLevel};
QeIdentity {
id: "QE".to_string(),
version: 2,
issue_date: "2025-06-19T10:01:18Z".to_string(),
next_update: "2025-07-19T10:01:18Z".to_string(),
tcb_evaluation_data_number: 17,
miscselect: hex!("00000000"),
miscselect_mask: hex!("FFFFFFFF"),
attributes: hex!("11000000000000000000000000000000"),
attributes_mask: hex!("FBFFFFFFFFFFFFFF0000000000000000"),
mrsigner: hex!("8C4F5775D796503E96137F77C68A829A0056AC8DED70140B081B094490C57BFF"),
isvprodid: 1,
tcb_levels: vec![
QeTcbLevel {
tcb: QeTcb { isvsvn: 8 },
tcb_date: "2024-03-13T00:00:00Z".to_string(),
tcb_status: UpToDate,
advisory_ids: vec![],
},
QeTcbLevel {
tcb: QeTcb { isvsvn: 6 },
tcb_date: "2021-11-10T00:00:00Z".to_string(),
tcb_status: OutOfDate,
advisory_ids: vec!["INTEL-SA-00615".to_string()],
},
QeTcbLevel {
tcb: QeTcb { isvsvn: 5 },
tcb_date: "2020-11-11T00:00:00Z".to_string(),
tcb_status: OutOfDate,
advisory_ids: vec!["INTEL-SA-00477".to_string(), "INTEL-SA-00615".to_string()],
},
],
}
}
#[test]
fn test_qe_identity_policy_valid() {
let qe_report = make_test_qe_report();
let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_ok(), "Expected success, got: {:?}", result);
}
#[test]
fn test_qe_identity_policy_mrsigner_mismatch() {
let qe_report = make_test_qe_report();
let mut qe_identity = make_test_qe_identity();
qe_identity.mrsigner =
hex!("0000000000000000000000000000000000000000000000000000000000000000");
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("MRSIGNER mismatch"),
"Expected MRSIGNER mismatch error, got: {}",
err_msg
);
}
#[test]
fn test_qe_identity_policy_isvprodid_mismatch() {
let qe_report = make_test_qe_report();
let mut qe_identity = make_test_qe_identity();
qe_identity.isvprodid = 999;
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("ISVPRODID mismatch"),
"Expected ISVPRODID mismatch error, got: {}",
err_msg
);
}
#[test]
fn test_qe_identity_policy_miscselect_mismatch() {
let mut qe_report = make_test_qe_report();
qe_report.misc_select = 0x00000001; let mut qe_identity = make_test_qe_identity();
qe_identity.miscselect = hex!("00000000");
qe_identity.miscselect_mask = hex!("FFFFFFFF");
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("MISCSELECT mismatch"),
"Expected MISCSELECT mismatch error, got: {}",
err_msg
);
}
#[test]
fn test_qe_identity_policy_miscselect_masked() {
let mut qe_report = make_test_qe_report();
qe_report.misc_select = 0x000000FF; let mut qe_identity = make_test_qe_identity();
qe_identity.miscselect = hex!("00000000");
qe_identity.miscselect_mask = hex!("00000000");
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(
result.is_ok(),
"Expected success with zero mask, got: {:?}",
result
);
}
#[test]
fn test_qe_identity_policy_attributes_mismatch() {
let mut qe_report = make_test_qe_report();
qe_report.attributes[0] = 0;
let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("ATTRIBUTES mismatch"),
"Expected ATTRIBUTES mismatch error, got: {}",
err_msg
);
}
#[test]
fn test_qe_identity_policy_attributes_masked() {
let mut qe_report = make_test_qe_report();
qe_report.attributes[8] = 0xFF;
qe_report.attributes[15] = 0xFF;
let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(
result.is_ok(),
"Expected success with masked attributes, got: {:?}",
result
);
}
#[test]
fn test_qe_identity_policy_isvsvn_up_to_date() {
let qe_report = make_test_qe_report(); let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_ok());
let tcb_level = result.unwrap();
assert_eq!(tcb_level.tcb_status, UpToDate);
assert!(tcb_level.advisory_ids.is_empty());
}
#[test]
fn test_qe_identity_policy_isvsvn_out_of_date() {
let mut qe_report = make_test_qe_report();
qe_report.isv_svn = 6; let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_ok());
let tcb_level = result.unwrap();
assert_eq!(tcb_level.tcb_status, OutOfDate);
assert_eq!(tcb_level.advisory_ids, vec!["INTEL-SA-00615"]);
}
#[test]
fn test_qe_identity_policy_isvsvn_higher_than_required() {
let mut qe_report = make_test_qe_report();
qe_report.isv_svn = 10; let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_ok());
let tcb_level = result.unwrap();
assert_eq!(tcb_level.tcb_status, UpToDate); }
#[test]
fn test_qe_identity_policy_isvsvn_too_low() {
let mut qe_report = make_test_qe_report();
qe_report.isv_svn = 4; let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("ISVSVN") && err_msg.contains("below minimum"),
"Expected ISVSVN below minimum error, got: {}",
err_msg
);
}
#[test]
fn test_qe_identity_policy_isvsvn_between_levels() {
let mut qe_report = make_test_qe_report();
qe_report.isv_svn = 7; let qe_identity = make_test_qe_identity();
let result = verify_qe_identity_policy(&qe_report, &qe_identity);
assert!(result.is_ok());
let tcb_level = result.unwrap();
assert_eq!(tcb_level.tcb_status, OutOfDate);
assert_eq!(tcb_level.advisory_ids, vec!["INTEL-SA-00615"]);
}
fn outdated_tdx_tcb_info() -> TcbInfo {
let collateral: crate::QuoteCollateralV3 =
serde_json::from_str(include_str!("../sample/tdx_quote_outdated_collateral.json"))
.unwrap();
serde_json::from_str(&collateral.tcb_info).unwrap()
}
const OUTDATED_CPU_SVN: [u8; 16] = [3, 3, 2, 2, 4, 1, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0];
fn tee_tcb_svn(module_isvsvn: u8, module_version: u8) -> [u8; 16] {
let mut svn = [0; 16];
svn[..3].copy_from_slice(&[module_isvsvn, module_version, 3]);
svn
}
#[test]
fn test_tdx_tcb_converges_module_status_and_date() {
let tcb_info = outdated_tdx_tcb_info();
let eval = |isvsvn| {
evaluate_tdx_tcb(&tcb_info, &OUTDATED_CPU_SVN, 13, &tee_tcb_svn(isvsvn, 1)).unwrap()
};
let level = eval(6);
assert_eq!(level.tcb_status, UpToDate);
assert_eq!(level.tcb_date, "2024-11-13T00:00:00Z");
for (isvsvn, date) in [(4, "2024-03-13T00:00:00Z"), (2, "2023-08-09T00:00:00Z")] {
let level = eval(isvsvn);
assert_eq!(level.tcb_status, OutOfDate);
assert_eq!(level.tcb_date, date);
assert_eq!(level.advisory_ids, ["INTEL-SA-01036", "INTEL-SA-01099"]);
}
}
#[test]
fn test_tdx_tcb_rejects_unappraisable_svns() {
let mut tcb_info = outdated_tdx_tcb_info();
for svn in [
tee_tcb_svn(6, 2),
tee_tcb_svn(2, 3),
tee_tcb_svn(1, 1),
tee_tcb_svn(4, 0),
] {
assert!(evaluate_tdx_tcb(&tcb_info, &OUTDATED_CPU_SVN, 13, &svn).is_err());
}
tcb_info.tdx_module_identities.clear();
assert!(evaluate_tdx_tcb(&tcb_info, &OUTDATED_CPU_SVN, 13, &tee_tcb_svn(6, 1)).is_err());
}
}