Skip to content
Closed
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3dc354
refactor(attestation): split DCAP verification from post-DCAP checks
pbeza Jun 11, 2026
2bdfcf1
feat(attestation): lower attestation expiry window from 7 days to 1 day
pbeza Jun 11, 2026
1c31292
feat(contract): add trusted-verifier state and participant voting (do…
pbeza Jun 11, 2026
097f177
feat(contract): verify Dstack attestations via the tee-verifier contract
pbeza Jun 11, 2026
326f38e
test(contract): stub tee-verifier + sandbox tests for async attestati…
pbeza Jun 11, 2026
28de2f6
test: fix attestation-cli expiry assertion + silence stub enum lint
pbeza Jun 11, 2026
332cb46
docs(attestation-verifier): add implemented-status banner with remain…
pbeza Jun 11, 2026
f4f29e5
fix(contract): use plain borsh for PendingAttestation/FinalOutcome
pbeza Jun 11, 2026
3fa3535
fix(contract): satisfy --all-features ABI build for verifier stub + F…
pbeza Jun 11, 2026
d1d1486
test(contract): update borsh-schema snapshot for new verifier state
pbeza Jun 11, 2026
9bcd696
fix(contract): shadow old Config layout in v3_11_2 migration state
pbeza Jun 11, 2026
a6e5ad3
fix: conform two TODO(#NNNN) prose references to the TODO-format check
pbeza Jun 12, 2026
21033ef
fix: hoist use statements out of function bodies (no-use-in-fn lint)
pbeza Jun 12, 2026
f898a48
test(contract): update ABI snapshot for the verifier-integration methods
pbeza Jun 12, 2026
be7ced6
style: apply nightly rustfmt formatting
pbeza Jun 12, 2026
377eba8
docs: fix intra-doc links broken by the verify split + conversions move
pbeza Jun 12, 2026
e773072
test(contract): sync ABI snapshot with the resolve_verification doc fix
pbeza Jun 12, 2026
1722caf
test(contract): assert observable state, not yield-resume tx flags
pbeza Jun 12, 2026
2cdd6fc
Merge remote-tracking branch 'origin/main' into feat/integrate-tee-ve…
pbeza Jun 12, 2026
186eb40
fix(contract): correct async attestation callback edge cases
pbeza Jun 12, 2026
0d16721
test(contract): sync ABI snapshot with async-callback review fixes
pbeza Jun 12, 2026
bdd2eb4
Merge remote-tracking branch 'origin/main' into feat/integrate-tee-ve…
pbeza Jun 12, 2026
cc18ebb
fix(contract): guard verifier vote against placeholder; pin FinalOutc…
pbeza Jun 12, 2026
61690fb
Merge remote-tracking branch 'origin/main' into feat/integrate-tee-ve…
pbeza Jun 15, 2026
1567afe
Merge remote-tracking branch 'origin/main' into feat/integrate-tee-ve…
pbeza Jun 16, 2026
e01f2c3
Merge remote-tracking branch 'origin/main' into feat/integrate-tee-ve…
pbeza Jun 16, 2026
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
17 changes: 17 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ members = [
"crates/test-migration-contract",
"crates/test-parallel-contract",
"crates/test-port-allocator",
"crates/test-tee-verifier",
"crates/test-utils",
"crates/threshold-signatures",
"crates/tls",
Expand Down
2 changes: 1 addition & 1 deletion crates/attestation-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ anyhow = { workspace = true }
attestation = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true }
mpc-attestation = { workspace = true }
mpc-attestation = { workspace = true, features = ["local-verify"] }
mpc-primitives = { workspace = true }
node-types = { workspace = true }
reqwest = { workspace = true }
Expand Down
5 changes: 3 additions & 2 deletions crates/attestation-cli/src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ pub fn verify_at_timestamp(
VerificationError::Custom(format!("failed to load expected measurements: {e}"))
})?;

// Single verify call — same verification logic as the contract and node
// Full local verification (DCAP + post-DCAP) — the CLI verifies end-to-end
// locally, the same post-DCAP logic the contract runs on the verifier's report.
let AcceptedAttestation {
attestation: verified_attestation,
advisory_ids,
} = attestation.verify(
} = attestation.verify_locally(
report_data.into(),
timestamp_seconds,
&cli.allowed_image_hashes,
Expand Down
4 changes: 2 additions & 2 deletions crates/attestation-cli/tests/test_verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::path::PathBuf;

use attestation_cli::cli::Cli;
use attestation_cli::verify;
use mpc_attestation::attestation::Attestation;
use mpc_attestation::attestation::{Attestation, DEFAULT_EXPIRATION_DURATION_SECONDS};
use near_mpc_crypto_types::Ed25519PublicKey;
use node_types::http_server::StaticWebData;
use test_utils::attestation::{
Expand Down Expand Up @@ -56,7 +56,7 @@ fn full_verification_succeeds_with_valid_attestation() {
assert_eq!(result.mpc_image_hash.as_hex(), TEST_MPC_IMAGE_DIGEST_HEX);
assert_eq!(
result.expiry_timestamp_seconds,
VALID_ATTESTATION_TIMESTAMP + 60 * 60 * 24 * 7
VALID_ATTESTATION_TIMESTAMP + DEFAULT_EXPIRATION_DURATION_SECONDS
);
}

Expand Down
17 changes: 15 additions & 2 deletions crates/attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,37 @@ license = { workspace = true }
edition = { workspace = true }

[features]
borsh-schema = ["borsh/unstable__schema"]
borsh-schema = ["borsh/unstable__schema", "tee-verifier-interface/borsh-schema"]
dstack-conversions = ["dep:dstack-sdk-types"]
test-utils = []
# Off-chain only: pulls in `dcap-qvl` for `DstackAttestation::verify_locally`
# (full local DCAP + post-DCAP verification). On-chain callers (the contract)
# do not enable this; they get the `VerifiedReport` from the verifier contract
# and call `verify_with_report` directly.
local-verify = ["dep:dcap-qvl"]

[dependencies]
borsh = { workspace = true }
dcap-qvl = { workspace = true }
dcap-qvl = { workspace = true, optional = true }
derive_more = { workspace = true }
dstack-sdk-types = { workspace = true, optional = true }
hex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { workspace = true }
sha2 = { workspace = true }
# `serde` feature: `DstackAttestation` re-exports and embeds the interface
# `Collateral`/`QuoteBytes` and derives serde on them (needed for the node's
# `/public_data` payload). This is the only place that turns the interface
# crate's off-by-default `serde` feature on.
tee-verifier-interface = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }

[dev-dependencies]
assert_matches = { workspace = true }
# Self-dependency enabling the off-chain features so unit tests can exercise
# the `dcap_conversions` Borsh-layout pin and `verify_locally`.
attestation = { path = ".", features = ["local-verify", "test-utils", "dstack-conversions"] }
dstack-sdk-types = { workspace = true }
rstest = { workspace = true }
test-utils = { workspace = true }
Expand Down
114 changes: 79 additions & 35 deletions crates/attestation/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ use core::fmt;
use derive_more::Constructor;
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256, Sha384};
use tee_verifier_interface::{TDReport10, VerifiedReport};

#[cfg(feature = "local-verify")]
use crate::dcap_conversions::{IntoDcapType as _, IntoInterfaceType as _};

/// Expected TCB status for a successfully verified TEE quote.
const EXPECTED_QUOTE_STATUS: &str = "UpToDate";
Expand All @@ -30,14 +34,19 @@ pub(crate) const KEY_PROVIDER_EVENT: &str = "key-provider";

const RTMR3_INDEX: u32 = 3;

// `quote` and `collateral` are the `tee-verifier-interface` mirrors; their
// serde impls come from that crate's off-by-default `serde` feature, which
// `attestation` enables. Serde is needed because `DstackAttestation` is
// embedded (via `mpc_attestation::Attestation`) in the node's serde-serialized
// `/public_data` HTTP payload.
#[derive(Clone, Constructor, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
pub struct DstackAttestation {
pub quote: QuoteBytes,
pub collateral: Collateral,
pub tcb_info: TcbInfo,
}

/// Result of a successful [`DstackAttestation::verify`] call.
/// Result of a successful [`DstackAttestation::verify_with_report`] call.
#[derive(Clone, Debug)]
pub struct AcceptedDstackAttestation {
pub measurements: ExpectedMeasurements,
Expand Down Expand Up @@ -123,33 +132,30 @@ impl fmt::Debug for DstackAttestation {
}

impl DstackAttestation {
/// Checks whether this attestation is valid
/// with respect to expected values of:
/// - 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.
/// Runs the post-DCAP checks against an already-verified report.
///
/// Pure: no `dcap-qvl`, no host calls. The DCAP cryptographic verification
/// (`dcap_qvl::verify::verify`) is done elsewhere — by the `tee-verifier`
/// contract on-chain, or by [`verify_locally`](Self::verify_locally)
/// off-chain — and its `VerifiedReport` is passed in here. This is the
/// function both the contract and the off-chain helper share.
///
/// On success, returns the matched measurements along with any informational
/// advisory IDs surfaced alongside an `UpToDate` TCB status.
pub fn verify(
/// `report` must be the verifier's output for *this* attestation's quote
/// and collateral; the checks below bind it to the expected report data,
/// the embedded TCB info, and the accepted measurement sets.
pub fn verify_with_report(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there is a better name, I had to read the description to understand what "with_report" means.
but I don't have any suggestions :-(

&self,
report: &VerifiedReport,
expected_report_data: ReportData,
timestamp_seconds: u64,
accepted_measurements: &[ExpectedMeasurements],
) -> Result<AcceptedDstackAttestation, VerificationError> {
let verification_result =
dcap_qvl::verify::verify(&self.quote, &self.collateral, timestamp_seconds)
.map_err(|e| VerificationError::DcapVerification(e.to_string()))?;

let report_data = verification_result
let report_data = report
.report
.as_td10()
.ok_or(VerificationError::ReportNotTd10)?;

// Verify all attestation components
let advisory_ids = Self::verify_tcb_status(&verification_result)?;
let advisory_ids = Self::verify_tcb_status(report)?;
self.verify_report_data(&expected_report_data, report_data)?;

self.verify_rtmr3(report_data, &self.tcb_info)?;
Expand All @@ -163,6 +169,43 @@ impl DstackAttestation {
})
}

/// Full local verification: runs `dcap_qvl::verify::verify` and then the
/// post-DCAP checks via [`verify_with_report`](Self::verify_with_report).
///
/// Off-chain only (the `local-verify` feature pulls in `dcap-qvl`). Used by
/// the node, `tee-authority`, and `attestation-cli` to verify an
/// attestation end-to-end without the verifier contract. On-chain,
/// `mpc-contract` instead calls the verifier contract for the DCAP step and
/// then `verify_with_report` directly.
#[cfg(feature = "local-verify")]
pub fn verify_locally(
&self,
expected_report_data: ReportData,
timestamp_seconds: u64,
accepted_measurements: &[ExpectedMeasurements],
) -> Result<AcceptedDstackAttestation, VerificationError> {
let report = self.dcap_report(timestamp_seconds)?;
self.verify_with_report(&report, expected_report_data, accepted_measurements)
}

/// Runs only the DCAP step (`dcap_qvl::verify::verify`) and returns the
/// resulting report as the `tee-verifier-interface` mirror — the same value
/// the `tee-verifier` contract returns on-chain. Off-chain only.
///
/// This is the boundary between the DCAP verification (which the contract
/// offloads to the verifier) and the post-DCAP checks
/// ([`verify_with_report`](Self::verify_with_report)).
#[cfg(feature = "local-verify")]
pub fn dcap_report(&self, timestamp_seconds: u64) -> Result<VerifiedReport, VerificationError> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe call it dcap_verify_report or dcap_verify_quote?

let quote: Vec<u8> = self.quote.clone().into_dcap_type();
let collateral = self.collateral.clone().into_dcap_type();
Ok(
dcap_qvl::verify::verify(&quote, &collateral, timestamp_seconds)
.map_err(|e| VerificationError::DcapVerification(e.to_string()))?
.into_interface_type(),
)
}

/// Replays RTMR3 from the event log by hashing all relevant events together and verifies all
/// digests are correct
fn verify_event_log_rtmr3(
Expand Down Expand Up @@ -241,21 +284,18 @@ impl DstackAttestation {
/// after a product's Extended Servicing Updates date). These may appear with
/// `UpToDate` and do not indicate a vulnerability; they are returned so the
/// caller can log/expose them.
fn verify_tcb_status(
verification_result: &dcap_qvl::verify::VerifiedReport,
) -> Result<Vec<String>, VerificationError> {
(verification_result.status == EXPECTED_QUOTE_STATUS).or_err(|| {
VerificationError::TcbStatusNotUpToDate(verification_result.status.clone())
})?;
fn verify_tcb_status(report: &VerifiedReport) -> Result<Vec<String>, VerificationError> {
(report.status == EXPECTED_QUOTE_STATUS)
.or_err(|| VerificationError::TcbStatusNotUpToDate(report.status.clone()))?;

Ok(verification_result.advisory_ids.clone())
Ok(report.advisory_ids.clone())
}

/// Verifies report data matches expected values.
fn verify_report_data(
&self,
expected: &ReportData,
actual: &dcap_qvl::quote::TDReport10,
actual: &TDReport10,
) -> Result<(), VerificationError> {
// Check if sha384(tls_public_key) matches the hash in report_data. This check effectively
// proves that tls_public_key was included in the quote's report_data by an app running
Expand All @@ -267,7 +307,7 @@ impl DstackAttestation {
/// On success, returns the matched measurements.
fn verify_any_measurements(
&self,
report_data: &dcap_qvl::quote::TDReport10,
report_data: &TDReport10,
tcb_info: &TcbInfo,
accepted_measurements: &[ExpectedMeasurements],
) -> Result<ExpectedMeasurements, VerificationError> {
Expand All @@ -292,7 +332,7 @@ impl DstackAttestation {
/// Verifies static RTMRs match expected values.
fn verify_static_rtmrs(
&self,
report_data: &dcap_qvl::quote::TDReport10,
report_data: &TDReport10,
tcb_info: &TcbInfo,
expected_measurements: &ExpectedMeasurements,
) -> Result<(), VerificationError> {
Expand Down Expand Up @@ -346,7 +386,7 @@ impl DstackAttestation {
/// Verifies RTMR3 by replaying event log.
fn verify_rtmr3(
&self,
report_data: &dcap_qvl::quote::TDReport10,
report_data: &TDReport10,
tcb_info: &TcbInfo,
) -> Result<(), VerificationError> {
compare_hashes("rtmr3", tcb_info.rtmr3.as_slice(), &report_data.rt_mr3)?;
Expand Down Expand Up @@ -489,10 +529,8 @@ mod tests {
use super::*;

use alloc::{string::ToString, vec, vec::Vec};
use dcap_qvl::{
quote::{EnclaveReport, Report},
tcb_info::{TcbStatus, TcbStatusWithAdvisory},
verify::VerifiedReport,
use tee_verifier_interface::{
EnclaveReport, Report, TcbStatus, TcbStatusWithAdvisory, VerifiedReport,
};

fn verified_report(status: &str, advisory_ids: Vec<String>) -> VerifiedReport {
Expand All @@ -516,8 +554,14 @@ mod tests {
report_data: [0u8; 64],
}),
ppid: Vec::new(),
qe_status: TcbStatusWithAdvisory::new(TcbStatus::UpToDate, Vec::new()),
platform_status: TcbStatusWithAdvisory::new(TcbStatus::UpToDate, Vec::new()),
qe_status: TcbStatusWithAdvisory {
status: TcbStatus::UpToDate,
advisory_ids: Vec::new(),
},
platform_status: TcbStatusWithAdvisory {
status: TcbStatus::UpToDate,
advisory_ids: Vec::new(),
},
}
}

Expand Down
Loading
Loading