diff --git a/Cargo.lock b/Cargo.lock index 81f888909..14e223460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,12 +287,14 @@ dependencies = [ "log", "nix 0.26.4", "occlum_dcap", + "rstest", "scroll", "serde", "serde_json", "sev 1.2.1", "strum", "tdx-attest-rs", + "tempfile", "thiserror", "tokio", ] diff --git a/attestation-agent/attester/Cargo.toml b/attestation-agent/attester/Cargo.toml index b27a2450d..5244029aa 100644 --- a/attestation-agent/attester/Cargo.toml +++ b/attestation-agent/attester/Cargo.toml @@ -34,9 +34,11 @@ codicon = { version = "3.0", optional = true } hyper = { version = "0.14", features = ["full"], optional = true } hyper-tls = { version = "0.5", optional = true } tokio = { version = "1", features = ["full"], optional = true } +tempfile = { workspace = true, optional = true } [dev-dependencies] tokio.workspace = true +rstest.workspace = true [[bin]] name = "evidence_getter" @@ -54,7 +56,11 @@ all-attesters = [ "cca-attester", ] -tdx-attester = ["scroll", "tdx-attest-rs"] +# tsm-report enables a module that helps attesters to use Linux TSM_REPORTS for generating +# quotes. It's an unconditional dependency for tdx-attester since that is the only way to +# generate TDX quotes with upstream kernels. +tsm-report = ["tempfile"] +tdx-attester = ["scroll", "tsm-report", "tdx-attest-rs"] sgx-attester = ["occlum_dcap"] az-snp-vtpm-attester = ["az-snp-vtpm"] az-tdx-vtpm-attester = ["az-tdx-vtpm"] diff --git a/attestation-agent/attester/src/lib.rs b/attestation-agent/attester/src/lib.rs index 41e8b2368..43eb7dfbc 100644 --- a/attestation-agent/attester/src/lib.rs +++ b/attestation-agent/attester/src/lib.rs @@ -30,6 +30,9 @@ pub mod snp; #[cfg(feature = "csv-attester")] pub mod csv; +#[cfg(feature = "tsm-report")] +pub mod tsm_report; + pub type BoxedAttester = Box; impl TryFrom for BoxedAttester { diff --git a/attestation-agent/attester/src/tdx/mod.rs b/attestation-agent/attester/src/tdx/mod.rs index 78aa619d2..5a7e9dfda 100644 --- a/attestation-agent/attester/src/tdx/mod.rs +++ b/attestation-agent/attester/src/tdx/mod.rs @@ -3,9 +3,9 @@ // SPDX-License-Identifier: Apache-2.0 // -use crate::utils::pad; - +use super::tsm_report::*; use super::Attester; +use crate::utils::pad; use anyhow::*; use base64::Engine; use log::debug; @@ -20,9 +20,29 @@ const TDX_REPORT_DATA_SIZE: usize = 64; const CCEL_PATH: &str = "/sys/firmware/acpi/tables/data/CCEL"; pub fn detect_platform() -> bool { + TsmReportPath::new(TsmReportProvider::Tdx).is_ok() || tdx_getquote_ioctl_is_available() +} + +fn tdx_getquote_ioctl_is_available() -> bool { Path::new("/dev/tdx-attest").exists() || Path::new("/dev/tdx-guest").exists() } +fn get_quote_ioctl(report_data: &Vec) -> Result> { + let tdx_report_data = tdx_attest_rs::tdx_report_data_t { + // report_data.resize() ensures copying report_data to + // tdx_attest_rs::tdx_report_data_t cannot panic. + d: report_data.as_slice().try_into().unwrap(), + }; + + match tdx_attest_rs::tdx_att_get_quote(Some(&tdx_report_data), None, None, 0) { + (tdx_attest_rs::tdx_attest_error_t::TDX_ATTEST_SUCCESS, Some(q)) => Ok(q), + (error_code, _) => Err(anyhow!( + "TDX getquote ioctl: failed with error code: {:?}", + error_code + )), + } +} + #[derive(Serialize, Deserialize)] struct TdxEvidence { // Base64 encoded CC Eventlog ACPI table @@ -44,22 +64,19 @@ impl Attester for TdxAttester { report_data.resize(TDX_REPORT_DATA_SIZE, 0); - let tdx_report_data = tdx_attest_rs::tdx_report_data_t { - // report_data.resize() ensures copying report_data to - // tdx_attest_rs::tdx_report_data_t cannot panic. - d: report_data.as_slice().try_into().unwrap(), - }; + let quote_bytes = TsmReportPath::new(TsmReportProvider::Tdx).map_or_else( + |notsm| { + get_quote_ioctl(&report_data) + .context(format!("TDX Attester: quote generation using ioctl() fallback failed after a TSM report error ({notsm})")) + }, + |tsm| { + tsm.attestation_report(TsmReportData::Tdx(report_data.clone())) + .context("TDX Attester: quote generation using TSM reports failed") + }, + )?; let engine = base64::engine::general_purpose::STANDARD; - let quote = match tdx_attest_rs::tdx_att_get_quote(Some(&tdx_report_data), None, None, 0) { - (tdx_attest_rs::tdx_attest_error_t::TDX_ATTEST_SUCCESS, Some(q)) => engine.encode(q), - (error_code, _) => { - return Err(anyhow!( - "TDX Attester: Failed to get TD quote. Error code: {:?}", - error_code - )); - } - }; + let quote = engine.encode(quote_bytes); let cc_eventlog = match std::fs::read(CCEL_PATH) { Result::Ok(el) => Some(engine.encode(el)), diff --git a/attestation-agent/attester/src/tsm_report/mod.rs b/attestation-agent/attester/src/tsm_report/mod.rs new file mode 100644 index 000000000..504b5ccee --- /dev/null +++ b/attestation-agent/attester/src/tsm_report/mod.rs @@ -0,0 +1,196 @@ +// Copyright (c) 2024 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use strum::EnumString; +use tempfile::tempdir_in; +use thiserror::Error; + +const TSM_REPORT_PATH: &str = "/sys/kernel/config/tsm/report"; + +#[derive(Error, Debug)] +pub enum TsmReportError { + #[error("Failed to access TSM Report path")] + NoTsmReports, + #[error("Failed to create TSM Report path instance: {0}")] + Open(#[from] std::io::Error), + #[error("Failed to access TSM Report attribute: {0} ({1})")] + Access(&'static str, #[source] std::io::Error), + #[error("Failed to parse TSM Report attribute 'generation': {0}")] + Parse(#[source] std::num::ParseIntError), + #[error("Failed to open TSM Report path: missing provider {0:?} (provider={1:?})")] + MissingProvider(TsmReportProvider, TsmReportProvider), + #[error("Failed to open TSM Report path: unknown provider ({0})")] + UnknownProvider(#[from] strum::ParseError), + #[error("Failed to generate TSM Report: inblob write conflict (generation={0}, expected 1)")] + InblobConflict(u32), + #[error("Failed to generate TSM Report: missing inblob (len=0)")] + InblobLen, +} + +#[derive(PartialEq, Debug, EnumString)] +pub enum TsmReportProvider { + #[strum(serialize = "tdx_guest\n")] + Tdx, + #[strum(serialize = "sev_guest\n")] + Sev, +} + +pub enum TsmReportData { + Tdx(Vec), + Sev(u8, Vec), +} + +/// TsmReportPath instance represents a unique path on ConfigFS +/// provided by the TSM_REPORT attestation ABI. Currently, each +/// instance is a one-shot attestation request and the path is +/// automatically removed when the instance goes out of scope. +pub struct TsmReportPath { + path: PathBuf, +} + +impl Drop for TsmReportPath { + fn drop(&mut self) { + let _ = std::fs::remove_dir(self.path.as_path()) + .map_err(|e| log::error!("Failed to remove TSM Report directory: {}", e)); + } +} + +impl TsmReportPath { + pub fn new(wanted: TsmReportProvider) -> Result { + if !Path::new(TSM_REPORT_PATH).exists() { + return Err(TsmReportError::NoTsmReports); + } + + let p = tempdir_in(TSM_REPORT_PATH).map_err(TsmReportError::Open)?; + + // Remove the Drop set by tempdir_in() since it errors on ConfigFS + // and leaks the created path. We implement our own Drop that removes the + // path (rmdir way) when TsmReportPath instance goes out of scope. + let path = p.into_path(); + + check_tsm_report_provider(path.as_path(), wanted).map_err(|e| { + let _ = std::fs::remove_dir(path.as_path()); + e + })?; + + Ok(Self { path }) + } + pub fn attestation_report( + &self, + provider_data: TsmReportData, + ) -> Result, TsmReportError> { + let report_path = self.path.as_path(); + + let report_data = match provider_data { + TsmReportData::Tdx(inblob) => inblob, + TsmReportData::Sev(privlevel, inblob) => { + // TODO: untested + std::fs::write(report_path.join("privlevel"), vec![privlevel]) + .map_err(|e| TsmReportError::Access("privlevel", e))?; + inblob + } + }; + + if report_data.is_empty() { + return Err(TsmReportError::InblobLen); + } + + std::fs::write(report_path.join("inblob"), report_data) + .map_err(|e| TsmReportError::Access("inblob", e))?; + + let q = std::fs::read(report_path.join("outblob")) + .map_err(|e| TsmReportError::Access("outblob", e))?; + + check_inblob_write_race(report_path)?; + + Ok(q) + } + pub fn supplemental_data(&self) -> Result, TsmReportError> { + let report_path = self.path.as_path(); + + let aux = std::fs::read(report_path.join("auxblob")) + .map_err(|e| TsmReportError::Access("auxblob", e))?; + + check_inblob_write_race(report_path)?; + + Ok(aux) + } +} + +/// check_inblob_write_race checks that the returned outblob/auxblob +/// matches the quote generation request originally triggered when +/// inblob was written by the TsmReportPath instance. It prevents +/// the race condition that someone else could use the same temporary +/// directory to generate a quote. +fn check_inblob_write_race(report_path: &Path) -> Result<(), TsmReportError> { + let g = std::fs::read_to_string(report_path.join("generation")) + .map_err(|e| TsmReportError::Access("generation", e))?; + + let generation = g + .trim_matches('\n') + .to_string() + .parse::() + .map_err(TsmReportError::Parse)?; + + if generation > 1 { + return Err(TsmReportError::InblobConflict(generation)); + } + + Ok(()) +} + +/// check_tsm_report_provider checks that the TEE is +/// the requested TsmReportProvider. +fn check_tsm_report_provider( + report_path: &Path, + wanted: TsmReportProvider, +) -> Result<(), TsmReportError> { + let report_provider = std::fs::read_to_string(report_path.join("provider")) + .map_err(|e| TsmReportError::Access("provider", e))?; + + match TsmReportProvider::from_str(&report_provider) { + Ok(provider) => { + if provider == wanted { + Ok(()) + } else { + Err(TsmReportError::MissingProvider(wanted, provider)) + } + } + Err(e) => Err(TsmReportError::UnknownProvider(e)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + + #[rstest] + #[case("provider", "tdx_guest\n", false)] + #[case("provider", "sev_guest\n", true)] + #[case("provider", "foo_guest\n", true)] + #[case("generation", "1\n", false)] + #[case("generation", "2\n", true)] + #[case("generation", "parseerror\n", true)] + fn test_tsm_report(#[case] file: &str, #[case] file_data: &str, #[case] expect_error: bool) { + let tsm_dir = tempfile::tempdir().unwrap(); + + std::fs::write(tsm_dir.path().join(file), file_data).unwrap(); + + match file { + "provider" => assert_eq!( + expect_error, + check_tsm_report_provider(tsm_dir.path(), TsmReportProvider::Tdx).is_err() + ), + "generation" => assert_eq!( + expect_error, + check_inblob_write_race(tsm_dir.path()).is_err(), + ), + _ => unimplemented!(), + } + } +}