diff --git a/kernel/src/device/mod.rs b/kernel/src/device/mod.rs index 25f690fd4..863e2ec72 100644 --- a/kernel/src/device/mod.rs +++ b/kernel/src/device/mod.rs @@ -10,7 +10,7 @@ mod urandom; mod zero; #[cfg(all(target_arch = "x86_64", feature = "cvm_guest"))] -mod tdxguest; +pub mod tdxguest; use alloc::format; diff --git a/kernel/src/device/tdxguest/mod.rs b/kernel/src/device/tdxguest/mod.rs index d5d3ae296..6ac78c07c 100644 --- a/kernel/src/device/tdxguest/mod.rs +++ b/kernel/src/device/tdxguest/mod.rs @@ -1,7 +1,18 @@ // SPDX-License-Identifier: MPL-2.0 -use ostd::mm::{DmaCoherent, FrameAllocOptions, HasPaddr, VmIo}; -use tdx_guest::tdcall::{get_report, TdCallError}; +use core::{mem::size_of, time::Duration}; + +use align_ext::AlignExt; +use aster_util::{field_ptr, safe_ptr::SafePtr}; +use ostd::{ + mm::{DmaCoherent, FrameAllocOptions, HasPaddr, HasSize, VmIo, PAGE_SIZE}, + sync::WaitQueue, +}; +use tdx_guest::{ + tdcall::{get_report, TdCallError}, + tdvmcall::{get_quote, TdVmcallError}, + SHARED_MASK, +}; use crate::{ events::IoEvents, @@ -60,6 +71,26 @@ impl From for Error { } } +impl From for Error { + fn from(err: TdVmcallError) -> Self { + match err { + TdVmcallError::TdxRetry => { + Error::with_message(Errno::EINVAL, "TdVmcallError::TdxRetry") + } + TdVmcallError::TdxOperandInvalid => { + Error::with_message(Errno::EINVAL, "TdVmcallError::TdxOperandInvalid") + } + TdVmcallError::TdxGpaInuse => { + Error::with_message(Errno::EINVAL, "TdVmcallError::TdxGpaInuse") + } + TdVmcallError::TdxAlignError => { + Error::with_message(Errno::EINVAL, "TdVmcallError::TdxAlignError") + } + TdVmcallError::Other => Error::with_message(Errno::EAGAIN, "TdVmcallError::Other"), + } + } +} + impl Pollable for TdxGuest { fn poll(&self, mask: IoEvents, _poller: Option<&mut PollHandle>) -> IoEvents { let events = IoEvents::IN | IoEvents::OUT; @@ -84,37 +115,102 @@ impl FileIo for TdxGuest { } } +pub fn tdx_get_quote(inblob: &[u8]) -> Result> { + const GET_QUOTE_IN_FLIGHT: u64 = 0xFFFF_FFFF_FFFF_FFFF; + const GET_QUOTE_BUF_SIZE: usize = 8 * 1024; + + let report = tdx_get_report(inblob)?; + let buf = alloc_dma_buf(GET_QUOTE_BUF_SIZE)?; + let report_ptr: SafePtr = SafePtr::new(&buf, 0); + + field_ptr!(&report_ptr, TdxQuoteHdr, version).write(&1u64)?; + field_ptr!(&report_ptr, TdxQuoteHdr, status).write(&0u64)?; + field_ptr!(&report_ptr, TdxQuoteHdr, in_len).write(&(TDX_REPORT_LEN as u32))?; + field_ptr!(&report_ptr, TdxQuoteHdr, out_len).write(&0u32)?; + buf.write_bytes(size_of::(), &report)?; + + // FIXME: The `get_quote` API from the `tdx_guest` crate should have been marked `unsafe` + // because it has no way to determine if the input physical address is safe or not. + get_quote((buf.paddr() as u64) | SHARED_MASK, buf.size() as u64)?; + + // Poll for the quote to be ready. + let status_ptr = field_ptr!(&report_ptr, TdxQuoteHdr, status); + let sleep_queue = WaitQueue::new(); + let sleep_duration = Duration::from_millis(100); + loop { + let status = status_ptr.read()?; + if status != GET_QUOTE_IN_FLIGHT { + break; + } + let _ = sleep_queue.wait_until_or_timeout(|| -> Option<()> { None }, &sleep_duration); + } + + // Note: We cannot convert `DmaCoherent` to `USegment` here. When shared memory is converted back + // to private memory in TDX, `TDG.MEM.PAGE.ACCEPT` will zero out all content. + // TDX Module Specification - `TDG.MEM.PAGE.ACCEPT` Leaf: + // "Accept a pending private page and initialize it to all-0 using the TD ephemeral private key." + let out_len = field_ptr!(&report_ptr, TdxQuoteHdr, out_len).read()?; + let mut outblob = vec![0u8; out_len as usize].into_boxed_slice(); + buf.read_bytes(size_of::(), outblob.as_mut())?; + Ok(outblob) +} + +#[repr(C)] +struct TdxQuoteHdr { + // Quote version, filled by TD + version: u64, + // Status code of quote request, filled by VMM + status: u64, + // Length of TDREPORT, filled by TD + in_len: u32, + // Length of quote, filled by VMM + out_len: u32, +} + fn handle_get_report(arg: usize) -> Result { - const SHARED_BIT: u8 = 51; - const SHARED_MASK: u64 = 1u64 << SHARED_BIT; let current_task = ostd::task::Task::current().unwrap(); let user_space = CurrentUserSpace::new(current_task.as_thread_local().unwrap()); let user_request: TdxReportRequest = user_space.read_val(arg)?; - let segment = FrameAllocOptions::new().alloc_segment(2).unwrap(); - let dma_coherent = DmaCoherent::map(segment.into(), false).unwrap(); - dma_coherent - .write_bytes(0, &user_request.report_data) - .unwrap(); - // 1024-byte alignment. - dma_coherent - .write_bytes(1024, &user_request.tdx_report) - .unwrap(); - - if let Err(err) = get_report( - ((dma_coherent.paddr() + 1024) as u64) | SHARED_MASK, - (dma_coherent.paddr() as u64) | SHARED_MASK, - ) { - println!("[kernel]: get TDX report error: {:?}", err); - return Err(err.into()); - } + let report = tdx_get_report(&user_request.report_data)?; let tdx_report_vaddr = arg + TDX_REPORTDATA_LEN; - let mut generated_report = vec![0u8; TDX_REPORT_LEN]; - dma_coherent - .read_bytes(1024, &mut generated_report) - .unwrap(); - let report_slice: &[u8] = &generated_report; - user_space.write_bytes(tdx_report_vaddr, &mut VmReader::from(report_slice))?; + user_space.write_bytes(tdx_report_vaddr, &mut VmReader::from(report.as_ref()))?; Ok(0) } + +fn tdx_get_report(inblob: &[u8]) -> Result> { + if inblob.len() != TDX_REPORTDATA_LEN { + return_errno_with_message!(Errno::EINVAL, "Invalid inblob length"); + } + + let segment = FrameAllocOptions::new().alloc_segment(2)?; + let dma_coherent = DmaCoherent::map(segment.into(), false).unwrap(); + dma_coherent.write_bytes(0, &inblob).unwrap(); + + // FIXME: The `get_report` API from the `tdx_guest` crate should have been marked `unsafe` + // because it has no way to determine if the input physical address is safe or not. + get_report( + ((dma_coherent.paddr() + 1024) as u64) | SHARED_MASK, + (dma_coherent.paddr() as u64) | SHARED_MASK, + )?; + + // Note: We cannot convert `DmaCoherent` to `USegment` here. When shared memory is converted back + // to private memory in TDX, `TDG.MEM.PAGE.ACCEPT` will zero out all content. + // TDX Module Specification - `TDG.MEM.PAGE.ACCEPT` Leaf: + // "Accept a pending private page and initialize it to all-0 using the TD ephemeral private key." + let mut generated_report = Box::new([0u8; TDX_REPORT_LEN]); + dma_coherent + .read_bytes(1024, generated_report.as_mut()) + .unwrap(); + + Ok(generated_report) +} + +fn alloc_dma_buf(buf_len: usize) -> Result { + let aligned_buf_len = buf_len.align_up(PAGE_SIZE); + let segment = FrameAllocOptions::new().alloc_segment(aligned_buf_len / PAGE_SIZE)?; + + let dma_buf = DmaCoherent::map(segment.into(), false).unwrap(); + Ok(dma_buf) +} diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 7cb2c170a..e89843768 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -66,6 +66,7 @@ mod net; mod prelude; mod process; mod sched; +mod security; mod syscall; mod thread; mod time; @@ -102,6 +103,7 @@ fn init() { sched::init(); process::init(); fs::init(); + security::init(); } fn init_on_each_cpu() { diff --git a/kernel/src/security/mod.rs b/kernel/src/security/mod.rs new file mode 100644 index 000000000..5b4853c3b --- /dev/null +++ b/kernel/src/security/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MPL-2.0 + +#[cfg(all(target_arch = "x86_64", feature = "cvm_guest"))] +mod tsm; + +pub(super) fn init() { + #[cfg(all(target_arch = "x86_64", feature = "cvm_guest"))] + tsm::init(); +} diff --git a/kernel/src/security/tsm.rs b/kernel/src/security/tsm.rs new file mode 100644 index 000000000..73cd49ab9 --- /dev/null +++ b/kernel/src/security/tsm.rs @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Trusted Security Manager (TSM). +//! +//! This module provides a cross-vendor ConfigFS interface +//! for generating confidential-computing attestation reports. +//! Userspace writes a request blob under `/sys/kernel/config/tsm/report/$name/inblob` +//! and reads back the signed report from `outblob`, +//! with optional knobs like `provider` and `generation`. +//! +//! For more information about the interface, +//! checkout Linux's [documentation](https://www.kernel.org/doc/Documentation/ABI/testing/configfs-tsm). + +use alloc::{boxed::Box, string::ToString, sync::Arc}; +use core::fmt::Debug; + +use aster_systree::{ + inherit_sys_branch_node, inherit_sys_leaf_node, BranchNodeFields, Error, NormalNodeFields, + Result, SysAttrSetBuilder, SysObj, SysPerms, SysStr, +}; +use aster_util::printer::VmPrinter; +use inherit_methods_macro::inherit_methods; +use ostd::{ + mm::{FallibleVmRead, FallibleVmWrite, VmReader, VmWriter}, + sync::RwMutex, +}; + +use crate::{device::tdxguest::tdx_get_quote, fs::configfs}; + +#[derive(Debug)] +struct Tsm { + fields: BranchNodeFields, +} + +impl Tsm { + fn new() -> Arc { + let name = SysStr::from("tsm"); + let attrs = SysAttrSetBuilder::new().build().unwrap(); + Arc::new_cyclic(|weak_self| { + let fields = BranchNodeFields::new(name, attrs, weak_self.clone()); + fields.add_child(ReportSet::new()).unwrap(); + Tsm { fields } + }) + } +} + +inherit_sys_branch_node!(Tsm, fields, { + fn perms(&self) -> SysPerms { + SysPerms::DEFAULT_RW_PERMS + } +}); + +#[derive(Debug)] +struct ReportSet { + fields: BranchNodeFields, +} + +#[inherit_methods(from = "self.fields")] +impl ReportSet { + fn new() -> Arc { + let name = SysStr::from("report"); + let attrs = SysAttrSetBuilder::new().build().unwrap(); + Arc::new_cyclic(|weak_self| ReportSet { + fields: BranchNodeFields::new(name, attrs, weak_self.clone()), + }) + } + + fn add_child(&self, new_child: Arc) -> Result<()>; +} + +inherit_sys_branch_node!(ReportSet, fields, { + fn perms(&self) -> SysPerms { + SysPerms::DEFAULT_RW_PERMS + } + + fn create_child(&self, name: &str) -> Result> { + let report = ReportNode::new(SysStr::from(name.to_string())); + self.add_child(report.clone())?; + Ok(report) + } +}); + +const TSM_INBLOB_MAX: usize = 64; + +/// The state machine of a report directory. +/// +/// The states and their transitions are summarized in the table below. +/// +/// ``` +/// ┌────────────────┐ +/// │InblobNeeded │ +/// │(initial) │ +/// └────────┬───────┘ +/// │ +/// │ write inblob +/// ┌────────▼───────┐ +/// │InblobProvided ◄─────────┐ +/// └────────┬───────┘ │ +/// │ │ +/// │ read outblob │ write inblob +/// ┌────────▼───────┐ │ +/// │OutblobGenerated├─────────┘ +/// └────────────────┘ +/// ``` +#[derive(Debug)] +enum TsmProviderState { + /// Inblob is needed. + /// + /// This is the initial state. + /// + /// The behaviors of the files under a report directory + /// are summarized in the table below: + /// + /// | Files | Write behaviors| Read behaviors | + /// |--------------|----------------|-----------------| + /// | `inblob` | Transist to `InblobProvided` | Error due to WO | + /// | `outblob` | Error due to RO| Error (-EINVAL) | + /// | `generation` | Error due to RO| Returns 0 | + /// + /// The initial value of `generation` is 0. + InblobNeeded, + /// Inblob is provided by the userspace, but outblob is not generated. + /// + /// The behaviors of the files under a report directory + /// are summarized in the table below: + /// + /// | Files | Write behaviors| Read behaviors | + /// |--------------|----------------|-----------------| + /// | `inblob` | Update inblob | Error due to WO | + /// | `outblob` | Error due to RO| Transist to `OutblobGenerated` | + /// | `generation` | Error due to RO| Returns `generation` | + /// + /// Whenever @inblob or any option is written, the value of `generation` + /// increases by 1. + InblobProvided { + generation: u32, + inblob: [u8; TSM_INBLOB_MAX], + }, + /// Outblob is generated and up-to-date. + /// + /// The behaviors of the files under a report directory + /// are summarized in the table below: + /// + /// | Files | Write behaviors| Read behaviors | + /// |--------------|----------------|-----------------| + /// | `inblob` | Transist to `InblobProvided` | Error due to WO | + /// | `outblob` | Error due to RO| Returns `quote_buf` | + /// | `generation` | Error due to RO| Returns `generation`| + OutblobGenerated { + generation: u32, + quote_buf: Box<[u8]>, + }, +} + +#[derive(Debug)] +struct TsmProvider { + provider: SysStr, + state: TsmProviderState, +} + +impl TsmProvider { + pub fn new() -> Self { + TsmProvider { + provider: SysStr::from("tdx_guest"), + state: TsmProviderState::InblobNeeded, + } + } +} + +#[derive(Debug)] +struct ReportNode { + fields: NormalNodeFields, + data: RwMutex, +} + +impl ReportNode { + fn new(name: SysStr) -> Arc { + let mut builder = SysAttrSetBuilder::new(); + builder.add(SysStr::from("inblob"), SysPerms::DEFAULT_RW_ATTR_PERMS); + builder.add(SysStr::from("outblob"), SysPerms::DEFAULT_RO_ATTR_PERMS); + builder.add(SysStr::from("provider"), SysPerms::DEFAULT_RO_ATTR_PERMS); + builder.add(SysStr::from("generation"), SysPerms::DEFAULT_RO_ATTR_PERMS); + let attrs = builder.build().unwrap(); + + Arc::new_cyclic(|weak_self| { + let fields = NormalNodeFields::new(name, attrs, weak_self.clone()); + ReportNode { + fields, + data: RwMutex::new(TsmProvider::new()), + } + }) + } + + fn read_inblob(&self, reader: &mut VmReader, inblob: &mut [u8]) -> Result { + let mut writer = VmWriter::from(&mut inblob[..]); + reader + .read_fallible(&mut writer) + .map_err(|_| Error::AttributeError) + } + + fn write_outblob(&self, writer: &mut VmWriter, outblob: &[u8]) -> Result { + let mut reader = VmReader::from(outblob); + writer + .write_fallible(&mut reader) + .map_err(|_| Error::AttributeError) + } +} + +inherit_sys_leaf_node!(ReportNode, fields, { + fn perms(&self) -> SysPerms { + SysPerms::DEFAULT_RW_PERMS + } + + fn read_attr_at(&self, name: &str, offset: usize, writer: &mut VmWriter) -> Result { + match name { + "provider" => { + let data = self.data.read(); + let mut printer = VmPrinter::new_skip(writer, offset); + write!(printer, "{}\n", data.provider)?; + Ok(printer.bytes_written()) + } + + "outblob" => { + let mut data = self.data.write(); + match data.state { + TsmProviderState::InblobNeeded => Err(Error::InvalidOperation), + + TsmProviderState::InblobProvided { + ref inblob, + generation, + } => match tdx_get_quote(inblob) { + Ok(quote) => { + let res = self.write_outblob(writer, "e[offset..]); + + // Transition to `OutblobGenerated` + data.state = TsmProviderState::OutblobGenerated { + generation, + quote_buf: quote, + }; + res + } + Err(_) => Err(Error::InvalidOperation), + }, + + TsmProviderState::OutblobGenerated { ref quote_buf, .. } => { + self.write_outblob(writer, "e_buf[offset..]) + } + } + } + + "generation" => { + let data = self.data.read(); + let mut printer = VmPrinter::new_skip(writer, offset); + let generation = match data.state { + TsmProviderState::InblobNeeded => 0, + TsmProviderState::InblobProvided { generation, .. } => generation, + TsmProviderState::OutblobGenerated { generation, .. } => generation, + }; + write!(printer, "{}\n", generation)?; + Ok(printer.bytes_written()) + } + + _ => Err(Error::AttributeError), + } + } + + fn write_attr(&self, name: &str, reader: &mut VmReader) -> Result { + match name { + "inblob" => { + let mut data = self.data.write(); + match data.state { + TsmProviderState::InblobNeeded => { + let mut inblob = [0u8; TSM_INBLOB_MAX]; + let read_len = self.read_inblob(reader, &mut inblob)?; + + // Transition to `InblobProvided` + data.state = TsmProviderState::InblobProvided { + generation: 1, + inblob, + }; + Ok(read_len) + } + + TsmProviderState::InblobProvided { + ref mut inblob, + ref mut generation, + } => { + let read_len = self.read_inblob(reader, inblob)?; + *generation = generation.wrapping_add(1); + Ok(read_len) + } + + TsmProviderState::OutblobGenerated { + ref mut generation, .. + } => { + let mut inblob = [0u8; TSM_INBLOB_MAX]; + let read_len = self.read_inblob(reader, &mut inblob)?; + *generation = generation.wrapping_add(1); + + // Transition to `InblobProvided` + data.state = TsmProviderState::InblobProvided { + generation: *generation, + inblob, + }; + Ok(read_len) + } + } + } + + _ => Err(Error::AttributeError), + } + } +}); + +pub(super) fn init() { + configfs::register_subsystem(Tsm::new()).unwrap(); +}