Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
81 changes: 41 additions & 40 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ url = "2.5"
# Cryptography/Security
aes-gcm = "0.10.3"
curve25519-dalek = "4.1.3"
dcap-qvl = "0.3.10"
dcap-qvl = { git = "https://github.com/Phala-Network/dcap-qvl", branch = "policy" }
elliptic-curve = { version = "0.13.8", features = ["pkcs8"] }
getrandom = "0.3.1"
hkdf = "0.12.4"
Expand Down
109 changes: 90 additions & 19 deletions dstack-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use anyhow::{anyhow, bail, Context, Result};
use cc_eventlog::{RuntimeEvent, TdxEvent};
use dcap_qvl::{
quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15},
verify::VerifiedReport as TdxVerifiedReport,
verify::{QuoteVerificationResult, VerifiedReport as TdxVerifiedReport},
Policy, SimplePolicy, TcbStatus,
};
#[cfg(feature = "quote")]
use dstack_types::SysConfig;
Expand Down Expand Up @@ -195,20 +196,46 @@ impl QuoteContentType<'_> {
}

#[allow(clippy::large_enum_variant)]
/// Represents a verified attestation
/// Represents a verified attestation report.
///
/// For TDX, holds the full [`QuoteVerificationResult`] so that callers
/// can apply additional (stricter) policies via [`QuoteVerificationResult::validate`]
/// before converting to a [`VerifiedReport`](TdxVerifiedReport).
#[derive(Clone)]
pub enum DstackVerifiedReport {
DstackTdx(TdxVerifiedReport),
DstackTdx(QuoteVerificationResult),
DstackGcpTdx,
DstackNitroEnclave,
}

impl DstackVerifiedReport {
pub fn tdx_report(&self) -> Option<&TdxVerifiedReport> {
/// Get the TDX [`QuoteVerificationResult`] for further policy validation.
pub fn tdx_qvr(&self) -> Option<&QuoteVerificationResult> {
match self {
DstackVerifiedReport::DstackTdx(report) => Some(report),
DstackVerifiedReport::DstackGcpTdx => None,
DstackVerifiedReport::DstackNitroEnclave => None,
DstackVerifiedReport::DstackTdx(qvr) => Some(qvr),
_ => None,
}
}

/// Consume self and apply a policy to the TDX quote, returning a [`TdxVerifiedReport`].
///
/// Returns `None` for non-TDX reports.
pub fn validate_tdx(self, policy: &dyn Policy) -> Result<Option<TdxVerifiedReport>> {
match self {
DstackVerifiedReport::DstackTdx(qvr) => {
Ok(Some(qvr.validate(policy)?))
}
_ => Ok(None),
}
}

/// Get a [`TdxVerifiedReport`] without additional policy checks.
///
/// Safe to call because the baseline policy was already validated during verification.
pub fn tdx_report(&self) -> Option<TdxVerifiedReport> {
Comment thread
kvinwang marked this conversation as resolved.
Outdated
match self {
DstackVerifiedReport::DstackTdx(qvr) => Some(qvr.clone().into_report_unchecked()),
_ => None,
}
}
}
Expand Down Expand Up @@ -375,7 +402,9 @@ impl GetDeviceId for () {
impl GetDeviceId for DstackVerifiedReport {
fn get_devide_id(&self) -> Vec<u8> {
match self {
DstackVerifiedReport::DstackTdx(tdx_report) => tdx_report.ppid.to_vec(),
DstackVerifiedReport::DstackTdx(qvr) => {
qvr.clone().into_report_unchecked().ppid
Comment thread
kvinwang marked this conversation as resolved.
Outdated
}
DstackVerifiedReport::DstackGcpTdx => Vec::new(),
DstackVerifiedReport::DstackNitroEnclave => Vec::new(),
}
Expand Down Expand Up @@ -638,12 +667,12 @@ impl Attestation {
pub async fn verify_with_time(
self,
pccs_url: Option<&str>,
_now: Option<SystemTime>,
now: Option<SystemTime>,
) -> Result<VerifiedAttestation> {
let report = match &self.quote {
AttestationQuote::DstackTdx(q) => {
let report = self.verify_tdx(pccs_url, &q.quote).await?;
DstackVerifiedReport::DstackTdx(report)
let qvr = self.verify_tdx(pccs_url, &q.quote, now).await?;
DstackVerifiedReport::DstackTdx(qvr)
}
AttestationQuote::DstackGcpTdx | AttestationQuote::DstackNitroEnclave => {
bail!("Unsupported attestation mode: {:?}", self.quote.mode());
Expand Down Expand Up @@ -682,7 +711,12 @@ impl Attestation {
self.verify_with_time(pccs_url, None).await
}

async fn verify_tdx(&self, pccs_url: Option<&str>, quote: &[u8]) -> Result<TdxVerifiedReport> {
async fn verify_tdx(
&self,
pccs_url: Option<&str>,
quote: &[u8],
now: Option<SystemTime>,
) -> Result<QuoteVerificationResult> {
let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default());
if pccs_url.is_empty() {
// try to read from PCCS_URL env var
Expand All @@ -691,13 +725,26 @@ impl Attestation {
Err(_) => Cow::Borrowed(""),
};
}
let tdx_report =
let now_secs = now
.unwrap_or_else(SystemTime::now)
.duration_since(SystemTime::UNIX_EPOCH)
.context("system time before epoch")?
.as_secs();
let qvr =
dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref()))
.await
.context("Failed to get collateral")?;
validate_tcb(&tdx_report)?;

let td_report = tdx_report.report.as_td10().context("no td report")?;
// Baseline policy validation (business layer can apply stricter policies on the returned QVR)
let supplemental = qvr.supplemental().context("Failed to build supplemental data")?;
default_policy(now_secs)
.validate(&supplemental)
.context("TCB policy validation failed")?;

// Validate TEE attributes (debug mode, signer, etc.)
validate_tcb(&supplemental.report)?;

let td_report = supplemental.report.as_td10().context("no td report")?;
let replayed_rtmr = self.replay_runtime_events::<Sha384>(None);
if replayed_rtmr != td_report.rt_mr3 {
bail!(
Expand All @@ -710,12 +757,12 @@ impl Attestation {
if td_report.report_data != self.report_data[..] {
bail!("tdx report_data mismatch");
}
Ok(tdx_report)
Ok(qvr)
}
}

/// Validate the TCB attributes
pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> {
/// Validate the TEE report attributes (debug mode, signer, etc.)
pub fn validate_tcb(report: &Report) -> Result<()> {
fn validate_td10(report: &TDReport10) -> Result<()> {
let is_debug = report.td_attributes[0] & 0x01 != 0;
if is_debug {
Expand All @@ -739,13 +786,37 @@ pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> {
}
Ok(())
}
match &report.report {
match report {
Report::TD15(report) => validate_td15(report),
Report::TD10(report) => validate_td10(report),
Report::SgxEnclave(report) => validate_sgx(report),
}
}

/// Default TCB policy for dstack attestation.
///
/// Accepts common non-critical TCB statuses (`SWHardeningNeeded`, `ConfigurationNeeded`,
/// `ConfigurationAndSWHardeningNeeded`, `OutOfDate`, `OutOfDateConfigurationNeeded`)
/// with a 90-day collateral grace period, and allows SMT (hyperthreading).
///
/// Rejects quotes carrying high-severity advisories that directly compromise TEE guarantees:
/// - INTEL-SA-01397: TDX migration → debuggable TD (CVSS 8.4, full TDX compromise)
/// - INTEL-SA-01367: OOB write in SGX/TDX memory subsystem (CVSS 7.2)
/// - INTEL-SA-01314: OOB write in TDX module
/// - INTEL-SA-00837: Unauthorized error injection in SGX/TDX (CVSS 7.2)
pub fn default_policy(now_secs: u64) -> SimplePolicy {
use core::time::Duration;
SimplePolicy::strict(now_secs)
.allow_status(TcbStatus::OutOfDate)
.platform_grace_period(Duration::from_secs(30 * 24 * 3600))
.qe_grace_period(Duration::from_secs(14 * 24 * 3600))
.allow_smt(true)
.reject_advisory("INTEL-SA-01397")
.reject_advisory("INTEL-SA-01367")
.reject_advisory("INTEL-SA-01314")
.reject_advisory("INTEL-SA-00837")
}

/// Information about the app extracted from event log
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppInfo {
Expand Down
Loading
Loading