use super::AttestationError;
use crate::AttestationParams;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub const CONFIGFS_TSM_REPORT: &str = "/sys/kernel/config/tsm/report";
pub const REPORT_DATA_LEN: usize = 64;
static ENTRY_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone)]
pub struct TsmReport {
pub outblob: Vec<u8>,
pub auxblob: Option<Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct TsmRoot {
root: PathBuf,
}
impl TsmRoot {
pub fn open_at(root: impl Into<PathBuf>, guest: &str) -> Result<Self, AttestationError> {
let root = root.into();
if !root.is_dir() {
return Err(AttestationError::DeviceOpenFailed(format!(
"configfs-tsm report interface not found at {} (requires Linux 6.7+ in {guest} \
guest with configfs mounted)",
root.display()
)));
}
Ok(Self { root })
}
pub fn request(
&self,
provider: &str,
report_data: &[u8; REPORT_DATA_LEN],
with_auxblob: bool,
) -> Result<TsmReport, AttestationError> {
let name = format!(
"ttk-{}-{}",
std::process::id(),
ENTRY_COUNTER.fetch_add(1, Ordering::Relaxed)
);
let entry = EntryGuard::create(self.root.join(name))?;
request_in_entry(&entry.0, provider, report_data, with_auxblob)
}
}
pub fn request_in_entry(
entry: &Path,
provider: &str,
report_data: &[u8; REPORT_DATA_LEN],
with_auxblob: bool,
) -> Result<TsmReport, AttestationError> {
let actual = read_attribute(entry, "provider")?;
if actual != provider {
return Err(AttestationError::UnexpectedResponse(format!(
"configfs-tsm provider is '{actual}', expected '{provider}'"
)));
}
fs::write(entry.join("inblob"), report_data)?;
let generation = read_attribute(entry, "generation")?;
let outblob = fs::read(entry.join("outblob")).map_err(|e| {
AttestationError::Driver(format!("failed to obtain evidence from configfs-tsm: {e}"))
})?;
let auxblob = if with_auxblob {
let aux = fs::read(entry.join("auxblob")).map_err(|e| {
AttestationError::Driver(format!("failed to read configfs-tsm auxblob: {e}"))
})?;
Some(aux).filter(|aux| !aux.is_empty())
} else {
None
};
if read_attribute(entry, "generation")? != generation {
return Err(AttestationError::UnexpectedResponse(
"configfs-tsm entry was modified while the evidence was generated".into(),
));
}
Ok(TsmReport { outblob, auxblob })
}
pub fn report_data(params: &AttestationParams) -> Result<[u8; REPORT_DATA_LEN], AttestationError> {
if params.nonce().is_some() || params.public_key().is_some() {
return Err(AttestationError::InvalidInput(
"report data can only carry user_data; hash the nonce or public key into it".into(),
));
}
let user_data = params.user_data().unwrap_or_default();
if user_data.len() > REPORT_DATA_LEN {
return Err(AttestationError::InvalidInput(format!(
"user_data is {} bytes, at most {REPORT_DATA_LEN} are supported",
user_data.len()
)));
}
let mut report_data = [0u8; REPORT_DATA_LEN];
report_data[..user_data.len()].copy_from_slice(user_data);
Ok(report_data)
}
fn read_attribute(entry: &Path, name: &str) -> Result<String, AttestationError> {
let value = fs::read_to_string(entry.join(name)).map_err(|e| {
AttestationError::Driver(format!(
"failed to read configfs-tsm attribute '{name}': {e}"
))
})?;
Ok(value.trim_end().to_string())
}
struct EntryGuard(PathBuf);
impl EntryGuard {
fn create(path: PathBuf) -> Result<Self, AttestationError> {
fs::create_dir(&path).map_err(|e| {
AttestationError::DeviceOpenFailed(format!(
"failed to create configfs-tsm entry {}: {e}",
path.display()
))
})?;
Ok(Self(path))
}
}
impl Drop for EntryGuard {
fn drop(&mut self) {
if let Err(e) = fs::remove_dir(&self.0) {
log::warn!(
"failed to remove configfs-tsm entry {}: {e}",
self.0.display()
);
}
}
}