Skip to content

feat(tee-verifier): stateless dcap_qvl::verify wrapper contract#3237

Open
pbeza wants to merge 5 commits into
feat/tee-verifier-interfacefrom
feat/tee-verifier-contract
Open

feat(tee-verifier): stateless dcap_qvl::verify wrapper contract#3237
pbeza wants to merge 5 commits into
feat/tee-verifier-interfacefrom
feat/tee-verifier-contract

Conversation

@pbeza
Copy link
Copy Markdown
Contributor

@pbeza pbeza commented May 14, 2026

Closes #3266

Overview

Adds tee-verifier: a stateless TEE attestation verifier contract.

It wraps dcap_qvl::verify::verify in a single verify_quote method. The contract holds no state and has no admin — verifier-internal policy (the dcap-qvl version, Intel root certs, etc.) is bound to the deployed code hash. Per-team allowlists, report-data binding, and other post-DCAP checks live in the caller, not here.

This moves the heavy dcap-qvl / ring / webpki / x509-cert closure out of any contract that needs quote verification and into this one contract, reached over a cross-contract call using the DTOs from #3235.

Builds on #3235 (PR #3235); part of the stack tracked by #3264. See docs/design/attestation-verifier-contract.md for the design.

@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from b3acde8 to 820497e Compare May 15, 2026 09:18
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch from eb25314 to 7766451 Compare May 15, 2026 09:29
@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from 820497e to 2e70aa9 Compare May 15, 2026 10:20
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch from 7766451 to 43413b5 Compare May 15, 2026 10:23
pbeza added a commit that referenced this pull request May 15, 2026
…-qvl

Pure crate split, no behaviour change. Sets up the WASM-size win that
the follow-up PR (mpc-contract Promise refactor) will land.

What moves:
- `app_compose`, `measurements`, `report_data`, `tcb_info` modules and
  their assets — none of these touched `dcap-qvl` to begin with.
- The post-DCAP verification helpers (`verify_tcb_status`,
  `verify_report_data`, `verify_rtmr3`, `verify_app_compose`,
  `verify_any_measurements`, `verify_static_rtmrs`,
  `verify_key_provider_digest`, `verify_event_log_rtmr3`,
  `validate_app_compose_config`, `compare_hashes`, `compare_hex_hashes`,
  plus the `OrErr` and `GetSingleEvent` traits and `VerificationError`)
  — refactored from private methods on `DstackAttestation` to free
  functions in `attestation_types::verify_post_dcap`, taking the
  `tee_verifier_interface::VerifiedReport` mirror instead of the
  `dcap_qvl` type.

What stays in `attestation`:
- `DstackAttestation::verify` (the one and only `dcap_qvl::verify::verify`
  call site). It now converts the dcap-qvl-returned report to the
  Borsh mirror once and calls the free functions on it.
- The `Collateral` newtype and `QuoteBytes` newtype. Their dedup with
  the `tee-verifier-interface` mirrors is deferred to a later PR.

What this enables (next PR):
- `mpc-contract` can depend on `attestation-types` alone (no `dcap-qvl`)
  and call the post-DCAP helpers from a Promise callback that receives
  the verifier-returned `VerifiedReport`. That is the actual WASM-size
  win for NEP-509.

Drops the dead `TryFrom<dcap_qvl::verify::VerifiedReport> for Measurements`
impl (no callers anywhere in the codebase).

Re-exports the moved modules from `attestation::lib.rs` so existing
consumers (`mpc-attestation`, `tee-authority`, `attestation-cli`, etc.)
keep their import paths unchanged.

Part of the verifier breakout from
docs/design/attestation-verifier-contract.md (#3160). Stacked on #3237.
@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from 2e70aa9 to dde7dd2 Compare June 3, 2026 09:08
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch from 43413b5 to 5fae5d5 Compare June 3, 2026 09:08
@pbeza pbeza changed the title feat(tee-verifier): stateless dcap_qvl::verify wrapper contract feat(tee-verifier): stateless dcap_qvl::verify wrapper contract Jun 3, 2026
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch from d1e513b to 54fc11c Compare June 3, 2026 10:37
@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from b731b70 to c3f7c04 Compare June 3, 2026 10:42
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch 3 times, most recently from 56f6430 to d69203b Compare June 3, 2026 12:05
@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from a288dbd to b2ff62d Compare June 3, 2026 12:05
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch 2 times, most recently from 5edeb86 to 7f576f3 Compare June 3, 2026 13:01
@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from d392d5c to 198b297 Compare June 3, 2026 13:01
pbeza added 4 commits June 3, 2026 16:33
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.
…utils feature

The workspace near-sdk dependency no longer enables `unit-testing` by default,
so `tests/verify_quote.rs`'s use of `testing_env!` fails to compile after
rebasing onto main. Add a `test-utils` feature that turns on
`near-sdk/unit-testing` and enable it through a path self dev-dependency,
mirroring how `crates/contract` does it.
…r out of the wire crate

verify_quote now returns Result<VerifiedReport, VerifierError> with
directly. Behaviour is unchanged (an Err still panics, surfacing to a
cross-contract caller as PromiseError::Failed), but the fallible-method shape
matches the rest of the contract surface and the error message now comes from
the error's Display.

VerifierError moves from tee-verifier-interface into tee-verifier: it never
crosses the wire (failures panic rather than serialize a returned error), so it
does not belong in the DTO crate. tee-verifier-interface drops its thiserror
dependency and keeps only the genuine wire types; tee-verifier picks thiserror
up and implements FunctionError on the local error.
Most internal workspace crates omit `repository`; only the published/SDK-style
crates set it. Drop it here to match the common convention.
@pbeza pbeza force-pushed the feat/tee-verifier-interface branch from 198b297 to 77b14bb Compare June 3, 2026 14:35
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch from 7f576f3 to 6d043fe Compare June 3, 2026 14:35
@pbeza pbeza marked this pull request as ready for review June 3, 2026 14:36
Copilot AI review requested due to automatic review settings June 3, 2026 14:36
@pbeza
Copy link
Copy Markdown
Contributor Author

pbeza commented Jun 3, 2026

@claude review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces a new stateless NEAR contract crate (tee-verifier) that centralizes Intel TDX quote verification by wrapping dcap_qvl::verify::verify behind a single verify_quote entry point, so other contracts can delegate DCAP verification over a cross-contract call without linking the heavy dcap-qvl dependency set themselves.

Changes:

  • Added the tee-verifier contract crate with a single Borsh-serialized verify_quote method returning VerifiedReport.
  • Added conversion glue (and layout-pinning tests) between dcap_qvl types and tee-verifier-interface DTOs.
  • Wired the new crate into the workspace and added an integration test using real fixtures from test-utils.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/tee-verifier/src/lib.rs Implements the stateless TeeVerifier contract and the verify_quote wrapper around dcap_qvl::verify::verify.
crates/tee-verifier/src/conversions.rs Adds dcap_qvl ↔ interface DTO conversions plus tests that pin Borsh wire layout to upstream types.
crates/tee-verifier/tests/verify_quote.rs Integration test exercising verify_quote with real quote+collateral fixtures and asserting expected report shape/status.
crates/tee-verifier/Cargo.toml Defines the new contract crate, features (ABI + unit-testing), and reproducible build metadata.
Cargo.toml Adds crates/tee-verifier to workspace members and wires tee-verifier-interface in workspace deps.
Cargo.lock Locks dependencies for the newly added tee-verifier crate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

#[test]
fn verify_quote__should_return_up_to_date_td10_report_for_valid_fixture() {
// Given
let block_timestamp_ns = Duration::from_secs(VALID_ATTESTATION_TIMESTAMP).as_nanos() as u64;
@claude
Copy link
Copy Markdown

claude Bot commented Jun 3, 2026

Pull request overview

Adds a new tee-verifier NEAR contract: a stateless dcap_qvl::verify::verify wrapper exposing a single Borsh-encoded verify_quote(quote, collateral) -> VerifiedReport entry point. The crate also provides field-for-field conversions between tee-verifier-interface DTOs (from #3235) and the upstream dcap_qvl types, with Borsh-layout pinning tests so an upstream field/variant reorder is caught at CI time. Builds on PR #3235.

Changes:

  • New crates/tee-verifier crate (lib + cdylib) wrapping dcap_qvl::verify.
  • IntoDcapType / IntoInterfaceType traits with exhaustive conversions for Collateral, VerifiedReport, Report, TDReport10/15, EnclaveReport, TcbStatus, TcbStatusWithAdvisory.
  • Borsh wire-layout regression tests comparing interface and dcap_qvl encodings for every mirrored type.
  • Custom getrandom::register_custom_getrandom! shim returning UNSUPPORTED on wasm32 (because dcap-qvl's contract feature pulls in getrandom without a backend).
  • Integration test exercising verify_quote against the shared test-utils fixture and VALID_ATTESTATION_TIMESTAMP.
  • Workspace wiring (Cargo.toml, Cargo.lock).

Reviewed changes

Per-file summary
File Description
Cargo.toml Adds crates/tee-verifier to members; declares tee-verifier-interface as a workspace dep.
Cargo.lock Generated entries for the new crate (note the dev-dependency self-pointer below).
crates/tee-verifier/Cargo.toml New manifest: cdylib+lib, abi/test-utils features, reproducible-build metadata, wasm-only getrandom.
crates/tee-verifier/src/lib.rs Stateless TeeVerifier contract with verify_quote, VerifierError, and the wasm getrandom shim.
crates/tee-verifier/src/conversions.rs IntoDcapType / IntoInterfaceType impls + Borsh-layout pinning tests for every mirrored type.
crates/tee-verifier/tests/verify_quote.rs Happy-path integration test against the Dstack fixture.

Findings

No blocking issues found. A few non-blocking observations:

Non-blocking (suggestions/nits):

  • crates/tee-verifier/src/lib.rs:67VerifierError::DcapVerification(format!(\"{err}\")) propagates the upstream dcap_qvl::verify::Error Display string. This is fine for diagnostics but the wrapped string is opaque to callers and the variant is the only one in the enum — consider either splitting into a richer enum that mirrors dcap_qvl::verify::Error (so callers can branch on OutOfDate vs BadSignature etc.) or documenting that the string is intentionally opaque. Not a blocker for this PR; can be revisited when the first real consumer lands.
  • crates/tee-verifier/tests/verify_quote.rs:27 — only the happy path is covered. Consider adding negative cases (e.g. truncated quote bytes, mismatched collateral, block_timestamp before/after the collateral validity window) so that a regression in error propagation through #[handle_result] is caught. Existing dcap-qvl upstream tests already cover the cryptographic surface, so this is about the wrapper, not the verifier.
  • crates/tee-verifier/src/conversions.rs:158 — the Borsh-layout tests rely on every fill byte being unique to detect a same-type field swap. The fills are unique today, but a future field with a fill value that collides with an existing one would silently weaken the test. A one-line invariant comment at each dcap_*() constructor reminding contributors of this would help; alternatively, derive fills from a counter so collisions are impossible by construction.
  • Cargo.lock:11647 — the lockfile lists tee-verifier as a dependency of tee-verifier. That is the expected result of the tee-verifier = { path = \".\", features = [\"test-utils\"] } dev-dependency trick used to gate near-sdk/unit-testing to tests only. Worth a one-line comment in Cargo.toml explaining the pattern so it isn't "cleaned up" later.
  • crates/tee-verifier/src/lib.rs:36 — the wasm-only getrandom_unsupported shim has no test (unavoidable in unit tests since it only registers on wasm32-unknown-unknown). If dcap_qvl::verify::verify ever starts pulling randomness, the failure path is silent until contract integration. Consider an E2E or near-workspaces test that loads the compiled WASM and exercises a verification, to lock in the assumption that no RNG is drawn.

✅ Approved

@claude
Copy link
Copy Markdown

claude Bot commented Jun 3, 2026

test review body

@claude
Copy link
Copy Markdown

claude Bot commented Jun 3, 2026

Pull request overview

Adds a stateless tee-verifier smart-contract crate that wraps dcap_qvl::verify::verify in a Borsh-serialized verify_quote method. A conversions module maps dcap_qvl types to/from the tee-verifier-interface mirrors. A no-op getrandom is registered for wasm32. An integration test calls verify_quote directly with the existing test-utils Dstack fixture and asserts UpToDate/TD10.

Changes:

  • New crate crates/tee-verifier (lib.rs, conversions.rs with Borsh-layout pinning tests, tests/verify_quote.rs).
  • Workspace plumbing: adds the new member, a tee-verifier-interface workspace-dep entry, Cargo.lock updated.

Reviewed changes

Cargo.toml - Adds the new tee-verifier member and a tee-verifier-interface workspace-dep entry.
Cargo.lock - New tee-verifier package plus the self-referential dev-dep edge produced by the path-equals-dot pattern.
crates/tee-verifier/Cargo.toml - New manifest with cdylib+lib, abi/test-utils features, getrandom/custom on wasm32.
crates/tee-verifier/src/lib.rs - Stateless TeeVerifier with verify_quote; defines VerifierError; registers wasm32 getrandom.
crates/tee-verifier/src/conversions.rs - IntoDcapType / IntoInterfaceType traits plus Borsh-equal-bytes pinning tests.
crates/tee-verifier/tests/verify_quote.rs - Integration test against the real Dstack fixture under testing_env! with VALID_ATTESTATION_TIMESTAMP.

Findings

BLOCKING (must fix or explicitly resolve before merge):

  • crates/tee-verifier/src/lib.rs:55 -- The handle_result attribute on verify_quote is documented to call FunctionError::panic() on the Err branch, which here is env::panic_str(self.to_string()). That means an unhappy verifier emits a runtime panic, not a borsh-serialized Err(VerifierError), so a .then callback in mpc-contract will observe Err(PromiseError::Failed) -- indistinguishable from verifier-account-missing, OOG, or verifier-silent. This directly contradicts the design (docs/design/attestation-verifier-contract.md:462), whose resolve_verification expects Result<Result<VerifiedReport, VerifierError>, PromiseError> and branches on Ok(Err(verifier_err)) separately from Err(promise_err). Either (a) drop handle_result so near-sdk borsh-serializes the Result, or (b) update the design doc to acknowledge that DCAP failures and infra failures are deliberately merged into PromiseError::Failed.

  • crates/tee-verifier/src/lib.rs:18 -- VerifierError lives in the contract crate, not in tee-verifier-interface. The design doc (attestation-verifier-contract.md:268) lists VerifierError among the wire DTOs in the interface crate, with one variant per dcap_qvl::verify::verify failure category (quote-malformed, collateral-expired, tcb-revoked, signature-mismatch, etc.) plus a fallback Other(String). The PR ships a single opaque DcapVerification(String). Combined with the handle_result behavior above, callers get no structured failure information. If DCAP outcomes are meant to be surfaced to callers (Defuse/Proximity audit logging, refund-vs-retry policy in mpc-contract), the variant set should be promoted into the interface crate and given Borsh derives before this contract is locked to an account.

NON-BLOCKING (nits, follow-ups, suggestions):

  • crates/tee-verifier/src/lib.rs:64 -- format!(err) is identical to err.to_string(); minor.
  • crates/tee-verifier/Cargo.toml:49 -- The dev-dep tee-verifier with path-equals-dot and features test-utils works but is the kind of self-referential pattern that confuses tools (and shows up as a self-edge in Cargo.lock). A cleaner alternative is to add near-sdk to dev-dependencies with workspace=true and the unit-testing feature, avoiding the self-import.
  • crates/tee-verifier/tests/verify_quote.rs:14 -- Re-parsing the serde_json::Value field-by-field with .as_str().unwrap() is fragile to typos in JSON keys; consider deriving serde::Deserialize on interface Collateral (gated behind a serde feature) so the fixture round-trips through serde_json::from_value directly. Test-only.
  • crates/tee-verifier/src/lib.rs:30 -- The getrandom UNSUPPORTED shim is good defense-in-depth; consider asserting in the integration or a tiny WASM-build test that no DCAP path pulls randomness today, so a future dcap-qvl bump that does fails loudly rather than only on chain.

Issues found.

…ia local traits

Add a conformance test module to conversions.rs that asserts each
tee-verifier-interface mirror type encodes to the same Borsh bytes as its
upstream dcap_qvl counterpart, paired by field/variant name. This catches a
silent same-name reorder upstream (which the field-by-field conversions and
self-consistent round-trip tests cannot), failing CI on the next dcap-qvl bump
rather than at runtime on the wire.

Replace the free conversion functions with local IntoDcapType /
IntoInterfaceType traits, since tee-verifier owns neither type family and
cannot host From/Into impls; the dcap-qvl -> interface direction would be
orphan-legal only in the no_std, dcap-qvl-free interface crate.

Also fold in self-review fixes: render the dcap failure via Display rather
than Debug, drop the unused Clone on VerifierError, and remove tautological
length assertions on fixed-size arrays in the integration test.
@pbeza pbeza force-pushed the feat/tee-verifier-contract branch from 6d043fe to cf72a5a Compare June 3, 2026 17:03
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.

2 participants