Skip to content

feat(tee-verifier-interface): Borsh DTOs for the verifier contract boundary - #3235

Merged
pbeza merged 12 commits into
mainfrom
feat/tee-verifier-interface
Jun 9, 2026
Merged

feat(tee-verifier-interface): Borsh DTOs for the verifier contract boundary#3235
pbeza merged 12 commits into
mainfrom
feat/tee-verifier-interface

Conversation

@pbeza

@pbeza pbeza commented May 14, 2026

Copy link
Copy Markdown
Contributor

Closes #3265

Overview

Adds tee-verifier-interface: the Borsh DTOs spoken at the tee-verifier contract boundary.

They are field-for-field mirrors of the dcap-qvl input and output types, owned here so the Borsh wire layout is independent of upstream.

This crate is the only DTO crate a consumer (mpc-contract, future Proximity / Defuse contracts) needs in order to talk to the verifier — without re-linking the dcap-qvl / ring / webpki / x509-cert closure into its own WASM.

Prerequisite for the verifier contract in #3266 (PR #3237); part of the stack tracked by #3264.

@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from 5e6fac2 to b3acde8 Compare May 14, 2026 14:35
@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from b3acde8 to 820497e Compare May 15, 2026 09:18
pbeza added a commit that referenced this pull request May 15, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from 820497e to 2e70aa9 Compare May 15, 2026 10:20
pbeza added a commit that referenced this pull request May 15, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from 2e70aa9 to dde7dd2 Compare June 3, 2026 09:08
pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from b731b70 to c3f7c04 Compare June 3, 2026 10:42
pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from a288dbd to b2ff62d Compare June 3, 2026 12:05
@pbeza
pbeza marked this pull request as ready for review June 3, 2026 12:10
Copilot AI review requested due to automatic review settings June 3, 2026 12:10

This comment was marked as outdated.

pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
pbeza added a commit that referenced this pull request Jun 3, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
@pbeza
pbeza force-pushed the feat/tee-verifier-interface branch from d392d5c to 198b297 Compare June 3, 2026 13:01
@pbeza
pbeza requested a review from Copilot June 3, 2026 13:02
@near near deleted a comment from claude Bot Jun 3, 2026
@near near deleted a comment from claude Bot Jun 3, 2026
@pbeza

pbeza commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@pbeza
pbeza dismissed stale reviews from barakeinav1 and gilcu3 via b9dfed6 June 8, 2026 10:30
The crate boundary is a cross-contract call (Borsh ABI), the payload is
mostly binary (a multi-KB quote plus collateral), and avoiding serde keeps
the dependency set minimal. Document that rationale so the absence of
serde/JSON support is a recorded decision rather than an apparent omission
…rors land

The serde `Collateral` DTO is superseded by `tee_verifier_interface::Collateral`
once `mpc-contract` consumes the Borsh mirrors. Add a TODO(#3494) marker so the
deferred cleanup is discoverable from the type itself
@pbeza

pbeza commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

Would be good to mention why we use borsh only (where we were previously using also serde). (...)

Good point! Added a note about that here: bb226c3.

(...) Also as many of these structs will then be removed from the contract-interface crate, might be good to point that out in some issue or comment.

Created an issue: #3494 and referenced it in the comment here: 314a535.

A more general question, if we plan to keep this in sync with the dcap types, would it make sense to add some tests to enforce that?

Great catch! #3237 (the PR stacked on top of this one) does exactly that: it adds the From<dcap_qvl::…> conversions plus tests asserting borsh::to_vec(interface) == borsh::to_vec(dcap) per type, so an upstream field reorder/rename fails CI. Has to live there rather than here, since this crate stays dcap-qvl-free and only the verifier crate links both. See:

/// Pins the Borsh wire layout of each `tee-verifier-interface` mirror type
/// against its `dcap_qvl` counterpart.
///
/// The conversions above already make the compiler reject an upstream rename,
/// removal, type change, or added variant; the drift they miss is a same-name
/// *reordering* of fields or variants, which silently changes the Borsh layout.
/// Each test builds both sides by the same field/variant names and asserts
/// equal Borsh bytes, so a reorder diverges — even for fieldless enum variants.
/// This relies on every field having a distinct fill value (`[1; _]`, `[2; _]`,
/// ...): a swap of two same-typed fields is only observable when they differ.
#[cfg(test)]
#[expect(non_snake_case)]
mod tests {
use super::*;
use rstest::rstest;
/// Asserts the two values encode to identical Borsh bytes.
fn assert_same_borsh_bytes<I: borsh::BorshSerialize, D: borsh::BorshSerialize>(
interface: &I,
dcap: &D,
) {
let interface_bytes = borsh::to_vec(interface).expect("interface should serialize");
let dcap_bytes = borsh::to_vec(dcap).expect("dcap should serialize");
assert_eq!(interface_bytes, dcap_bytes);
}
fn sample_collateral() -> Collateral {
Collateral {
pck_crl_issuer_chain: "issuer-chain".into(),
root_ca_crl: vec![1, 2, 3],
pck_crl: vec![4, 5, 6],
tcb_info_issuer_chain: "tcb-issuer".into(),
tcb_info: "tcb-info-json".into(),
tcb_info_signature: vec![7, 8],
qe_identity_issuer_chain: "qe-issuer".into(),
qe_identity: "qe-identity-json".into(),
qe_identity_signature: vec![9, 10],
pck_certificate_chain: Some("pck-chain".into()),
}
}
fn dcap_td10() -> dq_quote::TDReport10 {
dq_quote::TDReport10 {
tee_tcb_svn: [1; 16],
mr_seam: [2; 48],
mr_signer_seam: [3; 48],
seam_attributes: [4; 8],
td_attributes: [5; 8],
xfam: [6; 8],
mr_td: [7; 48],
mr_config_id: [8; 48],
mr_owner: [9; 48],
mr_owner_config: [10; 48],
rt_mr0: [11; 48],
rt_mr1: [12; 48],
rt_mr2: [13; 48],
rt_mr3: [14; 48],
report_data: [15; 64],
}
}
fn dcap_td15() -> dq_quote::TDReport15 {
dq_quote::TDReport15 {
base: dcap_td10(),
tee_tcb_svn2: [16; 16],
mr_service_td: [17; 48],
}
}
fn dcap_sgx() -> dq_quote::EnclaveReport {
dq_quote::EnclaveReport {
cpu_svn: [1; 16],
misc_select: 42,
reserved1: [2; 28],
attributes: [3; 16],
mr_enclave: [4; 32],
reserved2: [5; 32],
mr_signer: [6; 32],
reserved3: [7; 96],
isv_prod_id: 8,
isv_svn: 9,
reserved4: [10; 60],
report_data: [11; 64],
}
}
fn dcap_verified_report(report: dq_quote::Report) -> dq_verify::VerifiedReport {
dq_verify::VerifiedReport {
status: "UpToDate".into(),
advisory_ids: vec!["INTEL-SA-00001".into()],
report,
ppid: vec![0xAB; 16],
qe_status: dq_tcb::TcbStatusWithAdvisory {
status: dq_tcb::TcbStatus::UpToDate,
advisory_ids: vec![],
},
platform_status: dq_tcb::TcbStatusWithAdvisory {
status: dq_tcb::TcbStatus::ConfigurationNeeded,
advisory_ids: vec!["INTEL-SA-00002".into()],
},
}
}
/// Name-equal `dcap_qvl` counterpart of an interface [`TcbStatus`]. The
/// exhaustive `match` makes the compiler flag an upstream variant
/// rename/removal; the byte comparison in the test catches a reorder.
fn dcap_tcb_status(status: &TcbStatus) -> dq_tcb::TcbStatus {
match status {
TcbStatus::UpToDate => dq_tcb::TcbStatus::UpToDate,
TcbStatus::OutOfDateConfigurationNeeded => {
dq_tcb::TcbStatus::OutOfDateConfigurationNeeded
}
TcbStatus::OutOfDate => dq_tcb::TcbStatus::OutOfDate,
TcbStatus::ConfigurationAndSWHardeningNeeded => {
dq_tcb::TcbStatus::ConfigurationAndSWHardeningNeeded
}
TcbStatus::ConfigurationNeeded => dq_tcb::TcbStatus::ConfigurationNeeded,
TcbStatus::SWHardeningNeeded => dq_tcb::TcbStatus::SWHardeningNeeded,
TcbStatus::Revoked => dq_tcb::TcbStatus::Revoked,
}
}
#[test]
fn collateral__should_match_dcap_borsh_layout() {
let interface = sample_collateral();
let dcap = interface.clone().into_dcap_type();
assert_same_borsh_bytes(&interface, &dcap);
}
#[test]
fn td_report_10__should_match_dcap_borsh_layout() {
let dcap = dcap_td10();
let interface: TDReport10 = dcap.into_interface_type();
assert_same_borsh_bytes(&interface, &dcap);
}
#[test]
fn td_report_15__should_match_dcap_borsh_layout() {
let dcap = dcap_td15();
let interface: TDReport15 = dcap.into_interface_type();
assert_same_borsh_bytes(&interface, &dcap);
}
#[test]
fn enclave_report__should_match_dcap_borsh_layout() {
let dcap = dcap_sgx();
let interface: EnclaveReport = dcap.into_interface_type();
assert_same_borsh_bytes(&interface, &dcap);
}
#[rstest]
#[case::sgx(dq_quote::Report::SgxEnclave(dcap_sgx()))]
#[case::td10(dq_quote::Report::TD10(dcap_td10()))]
#[case::td15(dq_quote::Report::TD15(dcap_td15()))]
fn report__should_match_dcap_borsh_layout(#[case] dcap: dq_quote::Report) {
let interface: Report = dcap.clone().into_interface_type();
assert_same_borsh_bytes(&interface, &dcap);
}
#[rstest]
#[case(TcbStatus::UpToDate)]
#[case(TcbStatus::OutOfDateConfigurationNeeded)]
#[case(TcbStatus::OutOfDate)]
#[case(TcbStatus::ConfigurationAndSWHardeningNeeded)]
#[case(TcbStatus::ConfigurationNeeded)]
#[case(TcbStatus::SWHardeningNeeded)]
#[case(TcbStatus::Revoked)]
fn tcb_status__should_match_dcap_borsh_layout(#[case] status: TcbStatus) {
let dcap = dcap_tcb_status(&status);
assert_same_borsh_bytes(&status, &dcap);
}
#[test]
fn tcb_status_with_advisory__should_match_dcap_borsh_layout() {
let dcap = dq_tcb::TcbStatusWithAdvisory {
status: dq_tcb::TcbStatus::ConfigurationNeeded,
advisory_ids: vec!["INTEL-SA-00003".into()],
};
let interface: TcbStatusWithAdvisory = dcap.clone().into_interface_type();
assert_same_borsh_bytes(&interface, &dcap);
}
#[rstest]
#[case::sgx(dq_quote::Report::SgxEnclave(dcap_sgx()))]
#[case::td10(dq_quote::Report::TD10(dcap_td10()))]
#[case::td15(dq_quote::Report::TD15(dcap_td15()))]
fn verified_report__should_match_dcap_borsh_layout(#[case] report: dq_quote::Report) {
let dcap = dcap_verified_report(report);
let interface: VerifiedReport = dcap.clone().into_interface_type();
assert_same_borsh_bytes(&interface, &dcap);
}
}

@pbeza
pbeza requested review from barakeinav1, gilcu3 and netrome June 8, 2026 11:52
pbeza added 2 commits June 8, 2026 14:11
…rors land

The serde `Collateral` DTO is superseded by `tee_verifier_interface::Collateral`
once `mpc-contract` consumes the Borsh mirrors. Add a TODO(#3494) doc comment so
the deferred cleanup is discoverable from the type, and refresh the contract ABI
snapshot, which captures the new doc comment as the type's schema description
The TODO(#3494) note on `Collateral` was a `///` doc comment, which
`schemars` captures into the contract ABI as the type's schema description.
Demote it to a plain `//` comment so it stays visible to maintainers without
entering the ABI, and revert the snapshot accordingly
gilcu3
gilcu3 previously approved these changes Jun 8, 2026
@pbeza

pbeza commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

@barakeinav1 @netrome I need one more approval. Please re-review when you get a chance. Thanks! 🙏🏼

@pbeza
pbeza enabled auto-merge June 8, 2026 12:25
pbeza added a commit that referenced this pull request Jun 8, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
These are the verifier's outcome DTOs: VerificationResult { Verified, Rejected }
carries either a parsed report or a typed VerifierError, returned as the value
of a successful receipt rather than via a failed receipt. An on-chain caller's
#[callback_result] can then tell a rejected quote apart from PromiseError::Failed
(verifier unreachable/crashed), instead of both collapsing to one opaque failure.

Also aligns the design doc's failure-handling control flow with this model.
pbeza added a commit that referenced this pull request Jun 8, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
@pbeza
pbeza requested a review from gilcu3 June 8, 2026 17:07
pbeza added a commit that referenced this pull request Jun 8, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.

@barakeinav1 barakeinav1 left a comment

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.

Re-reviewed fa44ba9b. The VerificationResult { Verified, Rejected } change resolves the regression cleanly — a rejected quote now resumes immediately with its reason, and only a genuinely unreachable verifier falls through to the timeout, which is the correct behavior. Doc is fully in sync now. Thanks for the quick turnaround. 👍

@pbeza
pbeza added this pull request to the merge queue Jun 9, 2026
Merged via the queue into main with commit 1c5a462 Jun 9, 2026
15 checks passed
@pbeza
pbeza deleted the feat/tee-verifier-interface branch June 9, 2026 10:19
pbeza added a commit that referenced this pull request Jun 9, 2026
Adds the `tee-verifier` contract — a stateless NEAR contract exposing
one method:

    verify_quote(quote: QuoteBytes, collateral: Collateral) -> VerifiedReport

The method reads the current block timestamp inside the contract and
calls `dcap_qvl::verify::verify(quote, collateral, now)`. On success,
the parsed report is converted to the Borsh mirror types from
`tee-verifier-interface` and returned via Borsh. On verification
failure, the method panics with the upstream error rendered as a
string; callers handle this as `PromiseResult::Failed` in their
callback.

The contract has no state and no admin. All policy (allowlists,
report-data binding, RTMR3 replay, app-compose validation, etc.) is
the caller's responsibility — only the cryptographic dcap-qvl part
lives here.

Includes:
- `tee_verifier::TeeVerifier` (empty state struct, one method).
- `tee_verifier::conversions` (free functions converting between
  `dcap_qvl` types and the mirror types in `tee-verifier-interface`;
  free functions rather than `From` impls because of the orphan rule).
- An integration test (`tests/verify_quote.rs`) that calls
  `verify_quote` directly against the real Dstack quote+collateral
  fixture from `test-utils`, asserting the returned `VerifiedReport`
  has status `UpToDate`, no advisory IDs, and a TD10 report.

WASM size (non-reproducible, default release): ~518 KiB.

This is the v1 verifier from
`docs/design/attestation-verifier-contract.md` (#3160). A follow-up
PR will wire `mpc-contract`'s `submit_participant_info` into it via
Promise + callback.

Stacked on #3235.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce tee-verifier-interface crate (Borsh wire DTOs)

5 participants