Skip to content

Commit

Permalink
attester: add tsm_report module
Browse files Browse the repository at this point in the history
Linux 6.7 added a common ABI for CVMs to provide their attestation
reports. It's based on configfs and the quotes can be generated
using the 'TSM reports'.

Documentation:
https://www.kernel.org/doc/Documentation/ABI/testing/configfs-tsm

Add a tsm_report module that CoCo attesters can use to generate
quotes to be included in their attestation evidence.

Signed-off-by: Mikko Ylinen <[email protected]>
  • Loading branch information
mythi committed Feb 9, 2024
1 parent 2d6906b commit 8cf42d8
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions attestation-agent/attester/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -54,6 +56,10 @@ all-attesters = [
"cca-attester",
]

# 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", "tdx-attest-rs"]
sgx-attester = ["occlum_dcap"]
az-snp-vtpm-attester = ["az-snp-vtpm"]
Expand Down
3 changes: 3 additions & 0 deletions attestation-agent/attester/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Attester + Send + Sync>;

impl TryFrom<Tee> for BoxedAttester {
Expand Down
196 changes: 196 additions & 0 deletions attestation-agent/attester/src/tsm_report/mod.rs
Original file line number Diff line number Diff line change
@@ -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<u8>),
Sev(u8, Vec<u8>),
}

/// 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<Self, TsmReportError> {
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<Vec<u8>, 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<Vec<u8>, 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::<u32>()
.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!(),
}
}
}

0 comments on commit 8cf42d8

Please sign in to comment.