Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dstack/Cargo.lock

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

10 changes: 8 additions & 2 deletions dstack/cc-eventlog/src/tcg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
use crate::{codecs::VecOf, tdx::TdxEvent};
use anyhow::{Context, Result};
use scale::Decode;
use std::path::PathBuf;

/// The path to boottime ccel file.
const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL";
const CCEL_FILE_ENV: &str = "DSTACK_CCEL_FILE";

pub const TPM_ALG_ERROR: u16 = 0x0;
pub const TPM_ALG_RSA: u16 = 0x1;
Expand Down Expand Up @@ -300,8 +302,12 @@ impl TcgEventLog {
}

pub fn decode_from_ccel_file() -> Result<Self> {
let data = fs_err::read(CCEL_FILE).context("Failed to read CCEL")?;
Self::decode(&mut data.as_slice())
let path = std::env::var_os(CCEL_FILE_ENV)
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(CCEL_FILE));
let data = fs_err::read(&path)
.with_context(|| format!("failed to read CCEL from {}", path.display()))?;
Comment thread
kvinwang marked this conversation as resolved.
Self::decode(&mut data.as_slice()).context("failed to decode CCEL")
}

pub fn to_cc_event_log(&self) -> Result<Vec<TdxEvent>> {
Expand Down
16 changes: 16 additions & 0 deletions dstack/cc-eventlog/src/tdx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ pub fn label_tdx_acpi_data_events(event_logs: &mut [TdxEvent]) {
}
}

/// Decode a raw CCEL byte stream into dstack's TDX event representation.
pub fn decode_ccel(data: &[u8]) -> Result<Vec<TdxEvent>> {
let mut input = data;
let mut event_logs = TcgEventLog::decode(&mut input)?.to_cc_event_log()?;
label_tdx_acpi_data_events(&mut event_logs);
Ok(event_logs)
}

/// Read both boottime and runtime event logs.
pub fn read_event_log() -> Result<Vec<TdxEvent>> {
let mut event_logs = TcgEventLog::decode_from_ccel_file()?.to_cc_event_log()?;
Expand Down Expand Up @@ -172,4 +180,12 @@ mod tests {
assert_eq!(events[0].event, "");
assert_eq!(events[4].event, "app-id");
}

#[test]
fn decodes_the_bundled_ccel() {
let events = decode_ccel(include_bytes!("../samples/ccel.bin")).unwrap();
assert!(!events.is_empty());
assert!(events.iter().all(|event| event.imr <= 3));
assert!(events.iter().all(|event| event.digest().len() == 48));
}
}
2 changes: 1 addition & 1 deletion dstack/dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ fn choose_dstack_attestation_mode(has_tdx: bool, has_sev_snp: bool) -> Result<At
impl AttestationMode {
/// Detect attestation mode from system
pub fn detect() -> Result<Self> {
let has_tdx = std::path::Path::new("/dev/tdx_guest").exists();
let has_tdx = tdx_attest::is_tdx_available();
let has_sev_snp =
std::path::Path::new("/dev/sev-guest").exists() || has_sev_snp_tsm_provider();

Expand Down
1 change: 1 addition & 0 deletions dstack/tdx-attest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ insta.workspace = true
serde_json.workspace = true
dcap-qvl.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
tempfile.workspace = true
4 changes: 4 additions & 0 deletions dstack/tdx-attest/src/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ pub fn get_report(_report_data: &TdxReportData) -> Result<TdxReport> {
pub fn extend_rtmr(_index: u32, _event_type: u32, _digest: [u8; 48]) -> Result<()> {
Err(TdxAttestError::NotSupported)
}

pub fn is_tdx_available() -> bool {
false
}
117 changes: 99 additions & 18 deletions dstack/tdx-attest/src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const CONFIGFS_BASE: &str = "/sys/kernel/config/tsm/report";
const CONFIGFS_DEFAULT: &str = "/sys/kernel/config/tsm/report/com.intel.dcap";
const CONFIGFS_PATH_ENV: &str = "DCAP_TDX_QUOTE_CONFIGFS_PATH";
const RTMR_SYSFS_BASE: &str = "/sys/devices/virtual/misc/tdx_guest/measurements";
const RTMR_SYSFS_PATH_ENV: &str = "DCAP_TDX_RTMR_SYSFS_PATH";
const TDX_ATTEST_CONFIG_PATH: &str = "/etc/tdx-attest.conf";

const TDX_REPORT_DATA_SIZE: usize = 64;
Expand Down Expand Up @@ -166,7 +167,7 @@ static CONFIGFS_MKDIR_TRIED: Mutex<bool> = Mutex::new(false);
pub fn get_quote(report_data: &TdxReportData) -> Result<Vec<u8>> {
let _guard = TDX_LOCK.lock().map_err(|_| TdxAttestError::Busy)?;

if is_configfs_available() {
if should_try_configfs() {
return get_quote_via_configfs(report_data);
}

Expand All @@ -179,8 +180,35 @@ pub fn get_quote(report_data: &TdxReportData) -> Result<Vec<u8>> {
))
}

fn is_configfs_available() -> bool {
Path::new(CONFIGFS_DEFAULT).is_dir() || Path::new(CONFIGFS_BASE).is_dir()
fn should_try_configfs() -> bool {
// An explicit override is authoritative. Let prepare_configfs() report an
// invalid path instead of silently falling back to another quote method.
std::env::var_os(CONFIGFS_PATH_ENV).is_some()
|| Path::new(CONFIGFS_DEFAULT).is_dir()
|| Path::new(CONFIGFS_BASE).is_dir()
}

/// Return whether a TDX guest provider is available through either the legacy
/// misc device or the standard TSM report interface.
pub fn is_tdx_available() -> bool {
if Path::new(TDX_GUEST_DEVICE).exists() {
return true;
}

if let Some(path) = std::env::var_os(CONFIGFS_PATH_ENV) {
return verify_configfs_provider(Path::new(&path)).unwrap_or(false);
}
Comment thread
kvinwang marked this conversation as resolved.

if verify_configfs_provider(Path::new(CONFIGFS_DEFAULT)).unwrap_or(false) {
return true;
}

fs::read_dir(CONFIGFS_BASE)
.ok()
.into_iter()
.flatten()
.filter_map(std::result::Result::ok)
.any(|entry| verify_configfs_provider(&entry.path()).unwrap_or(false))
}

fn is_vsock_available() -> bool {
Expand Down Expand Up @@ -223,8 +251,8 @@ pub fn extend_rtmr(index: u32, _event_type: u32, digest: [u8; 48]) -> Result<()>
return Err(TdxAttestError::InvalidRtmrIndex(index));
}

if is_rtmr_sysfs_available() {
return extend_rtmr_via_sysfs(index, &digest);
if let Some(base) = rtmr_sysfs_base()? {
return extend_rtmr_via_sysfs(&base, index, &digest);
}

if Path::new(TDX_GUEST_DEVICE).exists() {
Expand All @@ -236,24 +264,37 @@ pub fn extend_rtmr(index: u32, _event_type: u32, digest: [u8; 48]) -> Result<()>
))
}

fn is_rtmr_sysfs_available() -> bool {
Path::new(RTMR_SYSFS_BASE).is_dir()
fn rtmr_sysfs_base() -> Result<Option<std::path::PathBuf>> {
if let Some(path) = std::env::var_os(RTMR_SYSFS_PATH_ENV) {
let path = std::path::PathBuf::from(path);
if path.as_os_str().len() < 240 && path.is_dir() {
return Ok(Some(path));
}
return Err(TdxAttestError::NotSupported(format!(
"invalid RTMR sysfs path from {RTMR_SYSFS_PATH_ENV}: {}",
path.display()
)));
}
Comment thread
kvinwang marked this conversation as resolved.

Ok(Path::new(RTMR_SYSFS_BASE)
.is_dir()
.then(|| std::path::PathBuf::from(RTMR_SYSFS_BASE)))
}

fn extend_rtmr_via_sysfs(index: u32, digest: &[u8; 48]) -> Result<()> {
let path = format!("{}/rtmr{}:sha384", RTMR_SYSFS_BASE, index);
fn extend_rtmr_via_sysfs(base: &Path, index: u32, digest: &[u8; 48]) -> Result<()> {
let path = base.join(format!("rtmr{index}:sha384"));

let mut file = OpenOptions::new()
.write(true)
.open(&path)
.map_err(|e| TdxAttestError::ExtendFailure(format!("open {path}: {e}")))?;
.map_err(|e| TdxAttestError::ExtendFailure(format!("open {}: {e}", path.display())))?;

file.write_all(digest).map_err(|e| match e.raw_os_error() {
Some(libc::EINVAL) => TdxAttestError::InvalidRtmrIndex(index),
Some(libc::EPERM) | Some(libc::EACCES) => {
TdxAttestError::ExtendFailure(format!("permission denied for RTMR {index}"))
}
_ => TdxAttestError::ExtendFailure(format!("write {path}: {e}")),
_ => TdxAttestError::ExtendFailure(format!("write {}: {e}", path.display())),
})
}

Expand Down Expand Up @@ -334,7 +375,10 @@ fn get_quote_via_configfs(report_data: &TdxReportData) -> Result<Vec<u8>> {

fn prepare_configfs() -> Result<String> {
if let Ok(path) = std::env::var(CONFIGFS_PATH_ENV) {
if path.len() < 240 && Path::new(&path).is_dir() && verify_configfs_provider(&path)? {
if path.len() < 240
&& Path::new(&path).is_dir()
&& verify_configfs_provider(Path::new(&path))?
{
Comment thread
kvinwang marked this conversation as resolved.
return Ok(path);
}
return Err(TdxAttestError::NotSupported(format!(
Expand All @@ -343,7 +387,7 @@ fn prepare_configfs() -> Result<String> {
}

let default_path = CONFIGFS_DEFAULT;
if Path::new(default_path).is_dir() && verify_configfs_provider(default_path)? {
if Path::new(default_path).is_dir() && verify_configfs_provider(Path::new(default_path))? {
return Ok(default_path.to_string());
}

Expand All @@ -369,7 +413,7 @@ fn prepare_configfs() -> Result<String> {
let provider_path = format!("{}/provider", default_path);
for i in 0..5 {
if Path::new(&provider_path).exists() {
if verify_configfs_provider(default_path)? {
if verify_configfs_provider(Path::new(default_path))? {
return Ok(default_path.to_string());
}
break;
Expand All @@ -383,10 +427,11 @@ fn prepare_configfs() -> Result<String> {
)))
}

fn verify_configfs_provider(path: &str) -> Result<bool> {
let provider_path = format!("{}/provider", path);
let provider = fs::read_to_string(&provider_path)
.map_err(|e| TdxAttestError::Unexpected(format!("read {provider_path}: {e}")))?;
fn verify_configfs_provider(path: &Path) -> Result<bool> {
let provider_path = path.join("provider");
let provider = fs::read_to_string(&provider_path).map_err(|e| {
TdxAttestError::Unexpected(format!("read {}: {e}", provider_path.display()))
})?;

Ok(provider.trim().starts_with("tdx_guest"))
}
Expand Down Expand Up @@ -647,3 +692,39 @@ fn parse_qgs_get_quote_response(data: &[u8]) -> Result<Vec<u8>> {

Ok(quote)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn recognizes_tdx_configfs_provider() {
let temp = tempfile::tempdir().unwrap();
fs::write(temp.path().join("provider"), "tdx_guest_sim\n").unwrap();
assert!(verify_configfs_provider(temp.path()).unwrap());

fs::write(temp.path().join("provider"), "sev_guest\n").unwrap();
assert!(!verify_configfs_provider(temp.path()).unwrap());
}

#[test]
fn invalid_configfs_override_does_not_fall_back() {
let previous = std::env::var_os(CONFIGFS_PATH_ENV);
std::env::set_var(
CONFIGFS_PATH_ENV,
"/definitely/not/a/real/tdx-configfs-provider",
);

let result = get_quote(&[0u8; 64]);

match previous {
Some(value) => std::env::set_var(CONFIGFS_PATH_ENV, value),
None => std::env::remove_var(CONFIGFS_PATH_ENV),
}
assert!(matches!(
result,
Err(TdxAttestError::NotSupported(message))
if message.contains("invalid configfs path from env")
));
}
}
17 changes: 14 additions & 3 deletions os/common/rootfs/dstack-prepare.sh
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,21 @@ log "Syncing system time..."
# Let the chronyd correct the system time immediately; keep booting if chronyd is not ready yet.
chronyc makestep || log "Warning: chronyc makestep failed; continuing"

if [[ -e /dev/sev-guest ]] || grep -qw sev_guest /sys/kernel/config/tsm/report/*/provider 2>/dev/null; then
has_tsm_provider() {
local prefix="$1"
local provider value
for provider in /sys/kernel/config/tsm/report/*/provider; do
[[ -r "$provider" ]] || continue
value=$(<"$provider")
[[ "$value" == "$prefix"* ]] && return 0
done
return 1
}

if [[ -e /dev/sev-guest ]] || has_tsm_provider sev_guest; then
log "SEV-SNP guest device/TSM provider detected"
elif [[ -e /dev/tdx_guest ]]; then
log "TDX guest device detected"
elif [[ -e /dev/tdx_guest ]] || has_tsm_provider tdx_guest; then
log "TDX guest device/TSM provider detected"
elif modprobe sev-guest 2>/dev/null; then
log "Loaded sev-guest module"
elif modprobe tdx-guest 2>/dev/null; then
Expand Down
Loading