use super::tsm::{self, TsmRoot};
use super::{AttestationError, AttestationProvider};
use crate::verifier::submod;
use crate::{AttestationParams, EatClaimsSet};
use ciborium::Value;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub use super::tsm::{report_data, CONFIGFS_TSM_REPORT, REPORT_DATA_LEN};
const DEVICE: &str = "/dev/tdx_guest";
const TSM_PROVIDER_TDX: &str = "tdx_guest";
const EAT_PROFILE: &str = "tag:lanetus.github.io,2026:tdx-nested-eat";
const QUOTE_HEADER_LEN: usize = 48;
const QUOTE_TEE_TYPE_TDX: u32 = 0x81;
const TD_REPORT_DATA_OFFSET: usize = 520;
#[derive(Debug)]
pub struct TdxSession {
tsm: TsmRoot,
}
impl TdxSession {
pub fn open() -> Result<Self, AttestationError> {
Self::open_at(CONFIGFS_TSM_REPORT)
}
pub fn open_at(report_root: impl Into<PathBuf>) -> Result<Self, AttestationError> {
Ok(Self {
tsm: TsmRoot::open_at(report_root, "a TDX")?,
})
}
pub fn get_quote(
&self,
report_data: &[u8; REPORT_DATA_LEN],
) -> Result<Vec<u8>, AttestationError> {
let quote = self
.tsm
.request(TSM_PROVIDER_TDX, report_data, false)?
.outblob;
check_quote("e, report_data)?;
Ok(quote)
}
}
impl AttestationProvider for TdxSession {
fn name(&self) -> &'static str {
"tdx"
}
fn is_available() -> bool {
Path::new(DEVICE).exists()
}
fn generate_document(
&self,
params: &AttestationParams,
) -> Result<EatClaimsSet, AttestationError> {
let quote = self.get_quote(&report_data(params)?)?;
Ok(wrap_quote_as_eat("e))
}
}
pub fn quote_from_entry(
entry: &Path,
report_data: &[u8; REPORT_DATA_LEN],
) -> Result<Vec<u8>, AttestationError> {
let quote = tsm::request_in_entry(entry, TSM_PROVIDER_TDX, report_data, false)?.outblob;
check_quote("e, report_data)?;
Ok(quote)
}
fn check_quote(quote: &[u8], report_data: &[u8; REPORT_DATA_LEN]) -> Result<(), AttestationError> {
let malformed =
|msg: &str| AttestationError::DocumentDecodingFailed(format!("TDX quote {msg}"));
let header = quote
.get(..QUOTE_HEADER_LEN)
.ok_or_else(|| malformed("is truncated"))?;
let version = u16::from_le_bytes([header[0], header[1]]);
let tee_type = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
if tee_type != QUOTE_TEE_TYPE_TDX {
return Err(malformed(&format!(
"has TEE type {tee_type:#x}, expected TDX"
)));
}
let body = match version {
4 => QUOTE_HEADER_LEN,
5 => QUOTE_HEADER_LEN + 6,
_ => return Err(malformed(&format!("has unsupported version {version}"))),
};
let offset = body + TD_REPORT_DATA_OFFSET;
let actual = quote
.get(offset..offset + REPORT_DATA_LEN)
.ok_or_else(|| malformed("is truncated"))?;
if actual != report_data {
return Err(AttestationError::UnexpectedResponse(
"TDX quote does not carry the requested REPORTDATA".into(),
));
}
Ok(())
}
pub fn wrap_quote_as_eat(quote: &[u8]) -> EatClaimsSet {
EatClaimsSet {
iat: Some(unix_now()),
eat_profile: Some(EAT_PROFILE.to_string()),
submods: Some(Value::Map(vec![(
Value::Text(submod::TDX.to_string()),
Value::Bytes(quote.to_vec()),
)])),
..EatClaimsSet::default()
}
}
fn unix_now() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or_default()
}