Skip to content

Commit bc5c574

Browse files
committed
feat: upgrade dcap-qvl to policy branch and add on-chain TCB policy support
- Update dcap-qvl dependency to policy branch (two-phase verification API) - Propagate QuoteVerificationResult through DstackVerifiedReport for flexible policy validation by business layer - Add baseline TCB policy with high-risk advisory rejection (INTEL-SA-01397, INTEL-SA-01367, INTEL-SA-01314, INTEL-SA-00837) - Add IAppTcbPolicy interface for on-chain TCB policy storage - Update DstackApp.sol and DstackKms.sol to store versioned TCB policy JSON - auth-eth reads policy from contracts and passes in BootResponse - KMS validates QuoteVerificationResult against on-chain RegoPolicySet - Fail-close on unknown policy version (version != 1 is rejected)
1 parent 9c5c5e2 commit bc5c574

25 files changed

Lines changed: 766 additions & 81 deletions

File tree

Cargo.lock

Lines changed: 41 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ url = "2.5"
177177
# Cryptography/Security
178178
aes-gcm = "0.10.3"
179179
curve25519-dalek = "4.1.3"
180-
dcap-qvl = "0.3.10"
180+
dcap-qvl = { git = "https://github.com/Phala-Network/dcap-qvl", branch = "policy" }
181181
elliptic-curve = { version = "0.13.8", features = ["pkcs8"] }
182182
getrandom = "0.3.1"
183183
hkdf = "0.12.4"

dstack-attest/src/attestation.rs

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use anyhow::{anyhow, bail, Context, Result};
1010
use cc_eventlog::{RuntimeEvent, TdxEvent};
1111
use dcap_qvl::{
1212
quote::{EnclaveReport, Quote, Report, TDReport10, TDReport15},
13-
verify::VerifiedReport as TdxVerifiedReport,
13+
verify::{QuoteVerificationResult, VerifiedReport as TdxVerifiedReport},
14+
Policy, SimplePolicy, TcbStatus,
1415
};
1516
#[cfg(feature = "quote")]
1617
use dstack_types::SysConfig;
@@ -195,20 +196,46 @@ impl QuoteContentType<'_> {
195196
}
196197

197198
#[allow(clippy::large_enum_variant)]
198-
/// Represents a verified attestation
199+
/// Represents a verified attestation report.
200+
///
201+
/// For TDX, holds the full [`QuoteVerificationResult`] so that callers
202+
/// can apply additional (stricter) policies via [`QuoteVerificationResult::validate`]
203+
/// before converting to a [`VerifiedReport`](TdxVerifiedReport).
199204
#[derive(Clone)]
200205
pub enum DstackVerifiedReport {
201-
DstackTdx(TdxVerifiedReport),
206+
DstackTdx(QuoteVerificationResult),
202207
DstackGcpTdx,
203208
DstackNitroEnclave,
204209
}
205210

206211
impl DstackVerifiedReport {
207-
pub fn tdx_report(&self) -> Option<&TdxVerifiedReport> {
212+
/// Get the TDX [`QuoteVerificationResult`] for further policy validation.
213+
pub fn tdx_qvr(&self) -> Option<&QuoteVerificationResult> {
208214
match self {
209-
DstackVerifiedReport::DstackTdx(report) => Some(report),
210-
DstackVerifiedReport::DstackGcpTdx => None,
211-
DstackVerifiedReport::DstackNitroEnclave => None,
215+
DstackVerifiedReport::DstackTdx(qvr) => Some(qvr),
216+
_ => None,
217+
}
218+
}
219+
220+
/// Consume self and apply a policy to the TDX quote, returning a [`TdxVerifiedReport`].
221+
///
222+
/// Returns `None` for non-TDX reports.
223+
pub fn validate_tdx(self, policy: &dyn Policy) -> Result<Option<TdxVerifiedReport>> {
224+
match self {
225+
DstackVerifiedReport::DstackTdx(qvr) => {
226+
Ok(Some(qvr.validate(policy)?))
227+
}
228+
_ => Ok(None),
229+
}
230+
}
231+
232+
/// Get a [`TdxVerifiedReport`] without additional policy checks.
233+
///
234+
/// Safe to call because the baseline policy was already validated during verification.
235+
pub fn tdx_report(&self) -> Option<TdxVerifiedReport> {
236+
match self {
237+
DstackVerifiedReport::DstackTdx(qvr) => Some(qvr.clone().into_report_unchecked()),
238+
_ => None,
212239
}
213240
}
214241
}
@@ -375,7 +402,9 @@ impl GetDeviceId for () {
375402
impl GetDeviceId for DstackVerifiedReport {
376403
fn get_devide_id(&self) -> Vec<u8> {
377404
match self {
378-
DstackVerifiedReport::DstackTdx(tdx_report) => tdx_report.ppid.to_vec(),
405+
DstackVerifiedReport::DstackTdx(qvr) => {
406+
qvr.clone().into_report_unchecked().ppid
407+
}
379408
DstackVerifiedReport::DstackGcpTdx => Vec::new(),
380409
DstackVerifiedReport::DstackNitroEnclave => Vec::new(),
381410
}
@@ -638,12 +667,12 @@ impl Attestation {
638667
pub async fn verify_with_time(
639668
self,
640669
pccs_url: Option<&str>,
641-
_now: Option<SystemTime>,
670+
now: Option<SystemTime>,
642671
) -> Result<VerifiedAttestation> {
643672
let report = match &self.quote {
644673
AttestationQuote::DstackTdx(q) => {
645-
let report = self.verify_tdx(pccs_url, &q.quote).await?;
646-
DstackVerifiedReport::DstackTdx(report)
674+
let qvr = self.verify_tdx(pccs_url, &q.quote, now).await?;
675+
DstackVerifiedReport::DstackTdx(qvr)
647676
}
648677
AttestationQuote::DstackGcpTdx | AttestationQuote::DstackNitroEnclave => {
649678
bail!("Unsupported attestation mode: {:?}", self.quote.mode());
@@ -682,7 +711,12 @@ impl Attestation {
682711
self.verify_with_time(pccs_url, None).await
683712
}
684713

685-
async fn verify_tdx(&self, pccs_url: Option<&str>, quote: &[u8]) -> Result<TdxVerifiedReport> {
714+
async fn verify_tdx(
715+
&self,
716+
pccs_url: Option<&str>,
717+
quote: &[u8],
718+
now: Option<SystemTime>,
719+
) -> Result<QuoteVerificationResult> {
686720
let mut pccs_url = Cow::Borrowed(pccs_url.unwrap_or_default());
687721
if pccs_url.is_empty() {
688722
// try to read from PCCS_URL env var
@@ -691,13 +725,26 @@ impl Attestation {
691725
Err(_) => Cow::Borrowed(""),
692726
};
693727
}
694-
let tdx_report =
728+
let now_secs = now
729+
.unwrap_or_else(SystemTime::now)
730+
.duration_since(SystemTime::UNIX_EPOCH)
731+
.context("system time before epoch")?
732+
.as_secs();
733+
let qvr =
695734
dcap_qvl::collateral::get_collateral_and_verify(quote, Some(pccs_url.as_ref()))
696735
.await
697736
.context("Failed to get collateral")?;
698-
validate_tcb(&tdx_report)?;
699737

700-
let td_report = tdx_report.report.as_td10().context("no td report")?;
738+
// Baseline policy validation (business layer can apply stricter policies on the returned QVR)
739+
let supplemental = qvr.supplemental().context("Failed to build supplemental data")?;
740+
default_policy(now_secs)
741+
.validate(&supplemental)
742+
.context("TCB policy validation failed")?;
743+
744+
// Validate TEE attributes (debug mode, signer, etc.)
745+
validate_tcb(&supplemental.report)?;
746+
747+
let td_report = supplemental.report.as_td10().context("no td report")?;
701748
let replayed_rtmr = self.replay_runtime_events::<Sha384>(None);
702749
if replayed_rtmr != td_report.rt_mr3 {
703750
bail!(
@@ -710,12 +757,12 @@ impl Attestation {
710757
if td_report.report_data != self.report_data[..] {
711758
bail!("tdx report_data mismatch");
712759
}
713-
Ok(tdx_report)
760+
Ok(qvr)
714761
}
715762
}
716763

717-
/// Validate the TCB attributes
718-
pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> {
764+
/// Validate the TEE report attributes (debug mode, signer, etc.)
765+
pub fn validate_tcb(report: &Report) -> Result<()> {
719766
fn validate_td10(report: &TDReport10) -> Result<()> {
720767
let is_debug = report.td_attributes[0] & 0x01 != 0;
721768
if is_debug {
@@ -739,13 +786,37 @@ pub fn validate_tcb(report: &TdxVerifiedReport) -> Result<()> {
739786
}
740787
Ok(())
741788
}
742-
match &report.report {
789+
match report {
743790
Report::TD15(report) => validate_td15(report),
744791
Report::TD10(report) => validate_td10(report),
745792
Report::SgxEnclave(report) => validate_sgx(report),
746793
}
747794
}
748795

796+
/// Default TCB policy for dstack attestation.
797+
///
798+
/// Accepts common non-critical TCB statuses (`SWHardeningNeeded`, `ConfigurationNeeded`,
799+
/// `ConfigurationAndSWHardeningNeeded`, `OutOfDate`, `OutOfDateConfigurationNeeded`)
800+
/// with a 90-day collateral grace period, and allows SMT (hyperthreading).
801+
///
802+
/// Rejects quotes carrying high-severity advisories that directly compromise TEE guarantees:
803+
/// - INTEL-SA-01397: TDX migration → debuggable TD (CVSS 8.4, full TDX compromise)
804+
/// - INTEL-SA-01367: OOB write in SGX/TDX memory subsystem (CVSS 7.2)
805+
/// - INTEL-SA-01314: OOB write in TDX module
806+
/// - INTEL-SA-00837: Unauthorized error injection in SGX/TDX (CVSS 7.2)
807+
pub fn default_policy(now_secs: u64) -> SimplePolicy {
808+
use core::time::Duration;
809+
SimplePolicy::strict(now_secs)
810+
.allow_status(TcbStatus::OutOfDate)
811+
.platform_grace_period(Duration::from_secs(30 * 24 * 3600))
812+
.qe_grace_period(Duration::from_secs(14 * 24 * 3600))
813+
.allow_smt(true)
814+
.reject_advisory("INTEL-SA-01397")
815+
.reject_advisory("INTEL-SA-01367")
816+
.reject_advisory("INTEL-SA-01314")
817+
.reject_advisory("INTEL-SA-00837")
818+
}
819+
749820
/// Information about the app extracted from event log
750821
#[derive(Debug, Clone, Serialize, Deserialize)]
751822
pub struct AppInfo {

0 commit comments

Comments
 (0)