Skip to content
Draft
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
5 changes: 0 additions & 5 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
//! `Collateral` — Borsh-stable mirror of `dcap_qvl::QuoteCollateralV3`.
//!
//! Field-for-field copy. Borsh wire layout matches the upstream type when
//! `dcap-qvl` is built with its `borsh` feature, so on-chain state that
//! previously stored an `attestation::collateral::Collateral` (newtype
//! wrapping `dcap_qvl::QuoteCollateralV3`) decodes into this type with no
//! migration.
//!
//! The conversion to/from `dcap_qvl::QuoteCollateralV3` lives in the
//! `attestation` crate (it depends on `dcap-qvl`); this crate does not.

use alloc::{string::String, vec::Vec};
use borsh::{BorshDeserialize, BorshSerialize};
use derive_more::{Deref, From, Into};
use serde::{Deserialize, Serialize};

#[cfg(feature = "test-utils")]
use {
alloc::{string::String, vec::Vec},
core::str::FromStr,
hex::FromHexError,
serde_json::Value,
thiserror::Error,
};
// `BorshSchema` derive expands to `T::declaration().to_string()`, which is
// only in scope under no_std when `alloc::string::ToString` is imported.
#[cfg(feature = "borsh-schema")]
use alloc::string::ToString as _;

pub use dcap_qvl::QuoteCollateralV3;
#[cfg(feature = "test-utils")]
use {core::str::FromStr, hex::FromHexError, serde_json::Value, thiserror::Error};

/// Supplemental data for the TEE quote, including Intel certificates to verify it came from genuine
/// Intel hardware, along with details about the Trusted Computing Base (TCB) versioning, status,
/// and other relevant info.
#[derive(
Clone, From, Deref, Into, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
)]
#[derive(Clone, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))]
#[cfg_attr(feature = "test-utils", serde(try_from = "Value"))]
pub struct Collateral(QuoteCollateralV3);
pub struct Collateral {
pub pck_crl_issuer_chain: String,
pub root_ca_crl: Vec<u8>,
pub pck_crl: Vec<u8>,
pub tcb_info_issuer_chain: String,
pub tcb_info: String,
pub tcb_info_signature: Vec<u8>,
pub qe_identity_issuer_chain: String,
pub qe_identity: String,
pub qe_identity_signature: Vec<u8>,
pub pck_certificate_chain: Option<String>,
}

#[cfg(feature = "test-utils")]
impl Collateral {
Expand All @@ -47,7 +65,7 @@ impl Collateral {
})
}

let quote_collateral = QuoteCollateralV3 {
Ok(Self {
tcb_info_issuer_chain: get_str(&v, "tcb_info_issuer_chain")?,
tcb_info: get_str(&v, "tcb_info")?,
tcb_info_signature: get_hex(&v, "tcb_info_signature")?,
Expand All @@ -58,8 +76,7 @@ impl Collateral {
root_ca_crl: get_hex(&v, "root_ca_crl")?,
pck_crl: get_hex(&v, "pck_crl")?,
pck_certificate_chain: get_str(&v, "pck_certificate_chain").ok(),
};
Ok(Self(quote_collateral))
})
}
}

Expand All @@ -68,16 +85,6 @@ impl FromStr for Collateral {
type Err = CollateralError;

/// Attempts to parse a JSON string into a [`Collateral`].
///
/// This is a convenience method that first parses the string as JSON, then attempts to convert
/// it to a [`Collateral`].
///
/// # Errors
///
/// Returns a [`CollateralError`] if:
/// - The string is not valid JSON
/// - The JSON doesn't contain the required collateral fields
/// - Hex fields cannot be decoded
fn from_str(s: &str) -> Result<Self, Self::Err> {
let json_value: Value =
serde_json::from_str(s).map_err(|_| CollateralError::InvalidJson)?;
Expand Down
48 changes: 48 additions & 0 deletions crates/attestation-types/src/dstack_attestation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! `DstackAttestation` — the bundle a TDX node submits for verification.
//!
//! Holds the raw quote bytes, the Intel collateral required to verify
//! them, and the Dstack-supplied TCB info. This crate carries only the
//! data shape; the `dcap_qvl::verify::verify` call that consumes a
//! `(quote, collateral)` pair lives in the `attestation` crate (which is
//! the only crate that depends on `dcap-qvl`).

use borsh::{BorshDeserialize, BorshSerialize};
use core::fmt;
use derive_more::Constructor;
use serde::{Deserialize, Serialize};

use alloc::{format, string::String};

use crate::{collateral::Collateral, quote::QuoteBytes, tcb_info::TcbInfo};

#[derive(Clone, Constructor, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
pub struct DstackAttestation {
pub quote: QuoteBytes,
pub collateral: Collateral,
pub tcb_info: TcbInfo,
}

impl fmt::Debug for DstackAttestation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const MAX_BYTES: usize = 2048;

fn truncate_debug<T: fmt::Debug>(value: &T, max_bytes: usize) -> String {
let debug_str = format!("{:?}", value);
if debug_str.len() <= max_bytes {
debug_str
} else {
format!(
"{}... (truncated {} bytes)",
&debug_str[..max_bytes],
debug_str.len() - max_bytes
)
}
}

f.debug_struct("DstackAttestation")
.field("quote", &truncate_debug(&self.quote, MAX_BYTES))
.field("collateral", &truncate_debug(&self.collateral, MAX_BYTES))
.field("tcb_info", &truncate_debug(&self.tcb_info, MAX_BYTES))
.finish()
}
}
3 changes: 3 additions & 0 deletions crates/attestation-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
extern crate alloc;

pub mod app_compose;
pub mod collateral;
pub mod dstack_attestation;
pub mod measurements;
pub mod quote;
pub mod report_data;
pub mod tcb_info;
pub mod verify_post_dcap;
31 changes: 31 additions & 0 deletions crates/attestation-types/src/quote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use alloc::vec::Vec;
use borsh::{BorshDeserialize, BorshSerialize};
use derive_more::{Deref, From, Into};
use serde::{Deserialize, Serialize};

// `BorshSchema` derive expands to `T::declaration().to_string()`, which is
// only in scope under no_std when `alloc::string::ToString` is imported.
#[cfg(feature = "borsh-schema")]
use alloc::string::ToString as _;

/// Raw bytes of an Intel TDX / SGX quote, as produced by the platform.
///
/// Borsh-encoded as a length-prefixed byte vector. Identical wire layout to
/// `dcap_qvl::verify::verify`'s first argument.
#[derive(
Debug,
Clone,
From,
Into,
Deref,
Serialize,
Deserialize,
BorshDeserialize,
BorshSerialize,
PartialEq,
Eq,
PartialOrd,
Ord,
)]
#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))]
pub struct QuoteBytes(Vec<u8>);
11 changes: 3 additions & 8 deletions crates/attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,19 @@ license = { workspace = true }
edition = { workspace = true }

[features]
borsh-schema = ["borsh/unstable__schema", "attestation-types/borsh-schema"]
borsh-schema = ["attestation-types/borsh-schema"]
dstack-conversions = ["attestation-types/dstack-conversions"]
test-utils = []
test-utils = ["attestation-types/test-utils"]

[dependencies]
attestation-types = { workspace = true }
borsh = { workspace = true }
dcap-qvl = { workspace = true }
derive_more = { workspace = true }
hex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tee-verifier-interface = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
assert_matches = { workspace = true }
dstack-sdk-types = { workspace = true }
serde_json = { workspace = true }
test-utils = { workspace = true }

[[test]]
Expand Down
102 changes: 45 additions & 57 deletions crates/attestation/src/attestation.rs
Original file line number Diff line number Diff line change
@@ -1,90 +1,78 @@
//! Local off-chain TEE attestation verification.
//!
//! Carries the `DstackAttestation` struct and its [`DstackAttestation::verify`]
//! method, which is the *only* place the heavy `dcap_qvl::verify::verify`
//! cryptographic call is made. After that call, the parsed report is
//! converted to the [`tee_verifier_interface::VerifiedReport`] mirror and
//! the post-DCAP checks are run via the free functions in
//! Adds the [`DstackVerify::verify`] trait method to the wasm-friendly
//! `DstackAttestation` struct that lives in `attestation-types`. The
//! `verify` implementation is the *only* place the heavy
//! `dcap_qvl::verify::verify` cryptographic call is made. After that
//! call, the parsed report is converted to the
//! [`tee_verifier_interface::VerifiedReport`] mirror and the post-DCAP
//! checks are run via the free functions in
//! [`attestation_types::verify_post_dcap`] — same code path the
//! `tee-verifier` contract uses on its callback side.
//!
//! Consumers that don't need to run `dcap-qvl` locally should depend on
//! `attestation-types` directly, not this crate.

use alloc::{
format,
string::{String, ToString},
};
use borsh::{BorshDeserialize, BorshSerialize};
use core::fmt;
use derive_more::Constructor;
use serde::{Deserialize, Serialize};
use alloc::string::ToString;

use attestation_types::{
measurements::ExpectedMeasurements,
report_data::ReportData,
tcb_info::TcbInfo,
verify_post_dcap::{
verify_any_measurements, verify_app_compose, verify_report_data, verify_rtmr3,
verify_tcb_status,
},
};

// Re-export the post-DCAP helper traits and the error type at the historical
// `attestation::attestation::*` paths so existing consumers (e.g.
// `mpc-attestation`) keep working without import-path churn.
// Re-export the post-DCAP helper traits, the error type, and
// `DstackAttestation` itself at the historical
// `attestation::attestation::*` paths so existing consumers
// (e.g. `mpc-attestation`) keep working without import-path churn.
// `DstackAttestation` now lives in `attestation-types`; the
// `dcap_qvl::verify::verify` entry point is provided as an inherent
// extension via the [`DstackVerify`] trait below (Rust forbids inherent
// impls on foreign types, so a trait is the only way to add the method
// from this crate).
pub use attestation_types::dstack_attestation::DstackAttestation;
pub use attestation_types::verify_post_dcap::{GetSingleEvent, OrErr, VerificationError};

use crate::{collateral::Collateral, quote::QuoteBytes};

#[derive(Clone, Constructor, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
pub struct DstackAttestation {
pub quote: QuoteBytes,
pub collateral: Collateral,
pub tcb_info: TcbInfo,
}

impl fmt::Debug for DstackAttestation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const MAX_BYTES: usize = 2048;

fn truncate_debug<T: fmt::Debug>(value: &T, max_bytes: usize) -> String {
let debug_str = format!("{:?}", value);
if debug_str.len() <= max_bytes {
debug_str
} else {
format!(
"{}... (truncated {} bytes)",
&debug_str[..max_bytes],
debug_str.len() - max_bytes
)
}
}

f.debug_struct("DstackAttestation")
.field("quote", &truncate_debug(&self.quote, MAX_BYTES))
.field("collateral", &truncate_debug(&self.collateral, MAX_BYTES))
.field("tcb_info", &truncate_debug(&self.tcb_info, MAX_BYTES))
.finish()
}
}

impl DstackAttestation {
/// Checks whether this attestation is valid with respect to expected values of:
/// Local `dcap_qvl::verify::verify` entry point on `DstackAttestation`.
///
/// Implemented for `attestation_types::dstack_attestation::DstackAttestation`
/// in this crate, which is the only crate that depends on `dcap-qvl`.
/// Defined as a trait because inherent impls on foreign types are not
/// allowed in Rust.
pub trait DstackVerify {
/// Checks whether this attestation is valid with respect to expected
/// values of:
/// - `expected_report_data`: must be measured correctly in RTMR3
/// - `timestamp_seconds`: current UNIX time in seconds
/// - `accepted_measurements`: set of accepted RTMRs and key-provider event digest.
/// If any element in the set is valid, the function accepts the attestation as valid.
/// - `accepted_measurements`: set of accepted RTMRs and key-provider
/// event digest. If any element in the set is valid, the function
/// accepts the attestation as valid.
///
/// On success, returns the matched measurements.
pub fn verify(
fn verify(
&self,
expected_report_data: ReportData,
timestamp_seconds: u64,
accepted_measurements: &[ExpectedMeasurements],
) -> Result<ExpectedMeasurements, VerificationError>;
}

impl DstackVerify for DstackAttestation {
fn verify(
&self,
expected_report_data: ReportData,
timestamp_seconds: u64,
accepted_measurements: &[ExpectedMeasurements],
) -> Result<ExpectedMeasurements, VerificationError> {
// The local-verify path constructs a fresh `QuoteCollateralV3` from
// the wasm-friendly mirror by cloning. The cost is negligible
// (a handful of String / Vec<u8> clones) and only paid off-chain.
let collateral = crate::collateral_to_dcap(self.collateral.clone());
let verification_result =
dcap_qvl::verify::verify(&self.quote, &self.collateral, timestamp_seconds)
dcap_qvl::verify::verify(&self.quote, &collateral, timestamp_seconds)
.map_err(|e| VerificationError::DcapVerification(e.to_string()))?;

let verified_report = to_mirror_verified_report(verification_result);
Expand Down
Loading
Loading