From d351b8743c523d88b1be1274c5c0b881d96b6ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Thu, 14 May 2026 16:38:43 +0200 Subject: [PATCH 1/5] feat(tee-verifier): stateless dcap_qvl::verify wrapper contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Cargo.lock | 13 +++ Cargo.toml | 2 + crates/tee-verifier/Cargo.toml | 48 +++++++++ crates/tee-verifier/src/conversions.rs | 122 ++++++++++++++++++++++ crates/tee-verifier/src/lib.rs | 59 +++++++++++ crates/tee-verifier/tests/verify_quote.rs | 73 +++++++++++++ 6 files changed, 317 insertions(+) create mode 100644 crates/tee-verifier/Cargo.toml create mode 100644 crates/tee-verifier/src/conversions.rs create mode 100644 crates/tee-verifier/src/lib.rs create mode 100644 crates/tee-verifier/tests/verify_quote.rs diff --git a/Cargo.lock b/Cargo.lock index 5908984210..9383ed5606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11644,6 +11644,19 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tee-verifier" +version = "3.9.0" +dependencies = [ + "borsh", + "dcap-qvl", + "getrandom 0.2.17", + "hex", + "near-sdk", + "tee-verifier-interface", + "test-utils", +] + [[package]] name = "tee-verifier-interface" version = "3.11.0" diff --git a/Cargo.toml b/Cargo.toml index dfcbc0c67d..0452019990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "crates/tee-authority", "crates/tee-context", "crates/tee-launcher", + "crates/tee-verifier", "crates/tee-verifier-interface", "crates/test-migration-contract", "crates/test-parallel-contract", @@ -67,6 +68,7 @@ near-mpc-sdk = { path = "crates/near-mpc-sdk", version = "0.0.1" } near-mpc-signature-verifier = { path = "crates/near-mpc-signature-verifier", version = "0.0.1" } node-types = { path = "crates/node-types" } tee-authority = { path = "crates/tee-authority" } +tee-verifier-interface = { path = "crates/tee-verifier-interface" } test-port-allocator = { path = "crates/test-port-allocator" } test-utils = { path = "crates/test-utils" } threshold-signatures = { path = "crates/threshold-signatures" } diff --git a/crates/tee-verifier/Cargo.toml b/crates/tee-verifier/Cargo.toml new file mode 100644 index 0000000000..d24867aa27 --- /dev/null +++ b/crates/tee-verifier/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "tee-verifier" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = "https://github.com/near/mpc" + +[package.metadata.cargo-shear] +ignored = ["borsh"] + +[package.metadata.near.reproducible_build] +image = "sourcescan/cargo-near:0.17.0-rust-1.86.0" +image_digest = "sha256:1784ca6310f3496f0048356ce420921c8f5fdf71ee8124d43a2e1ceb1f70db8a" +passed_env = [] +container_build_command = [ + "cargo", + "near", + "build", + "non-reproducible-wasm", + "--locked", + "--features", + "abi", +] + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +# Enabled by `cargo near build` (via `--features near-sdk/__abi-generate`) +# and required for ABI / Borsh schema generation. Pulls in +# `borsh/unstable__schema` and `BorshSchema` derives on the wire types. +abi = ["borsh/unstable__schema", "tee-verifier-interface/borsh-schema"] + +[dependencies] +borsh = { workspace = true } +dcap-qvl = { workspace = true } +near-sdk = { workspace = true } +tee-verifier-interface = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { workspace = true, features = ["custom"] } + +[dev-dependencies] +hex = { workspace = true } +test-utils = { workspace = true } + +[lints] +workspace = true diff --git a/crates/tee-verifier/src/conversions.rs b/crates/tee-verifier/src/conversions.rs new file mode 100644 index 0000000000..2a0f9bf749 --- /dev/null +++ b/crates/tee-verifier/src/conversions.rs @@ -0,0 +1,122 @@ +//! Conversions between `dcap_qvl`'s types and the Borsh-mirrored types in +//! `tee-verifier-interface`. +//! +//! These conversions live here (in the contract crate that already +//! depends on `dcap-qvl`) rather than in `tee-verifier-interface`, so +//! that the interface crate stays free of `dcap-qvl` and can be linked +//! into consumer contracts without dragging in `ring`/`webpki`/X.509 +//! parsing. +//! +//! Free functions are used rather than `From`/`Into` impls because the +//! orphan rule forbids implementing a foreign trait between two foreign +//! types from a third crate. + +use dcap_qvl::{quote as dq_quote, tcb_info as dq_tcb, verify as dq_verify}; +use tee_verifier_interface::{ + Collateral, EnclaveReport, QuoteBytes, Report, TDReport10, TDReport15, TcbStatus, + TcbStatusWithAdvisory, VerifiedReport, +}; + +pub fn collateral_to_dcap(value: Collateral) -> dcap_qvl::QuoteCollateralV3 { + dcap_qvl::QuoteCollateralV3 { + pck_crl_issuer_chain: value.pck_crl_issuer_chain, + root_ca_crl: value.root_ca_crl, + pck_crl: value.pck_crl, + tcb_info_issuer_chain: value.tcb_info_issuer_chain, + tcb_info: value.tcb_info, + tcb_info_signature: value.tcb_info_signature, + qe_identity_issuer_chain: value.qe_identity_issuer_chain, + qe_identity: value.qe_identity, + qe_identity_signature: value.qe_identity_signature, + pck_certificate_chain: value.pck_certificate_chain, + } +} + +pub fn quote_bytes_to_vec(value: QuoteBytes) -> Vec { + value.0 +} + +pub fn verified_report(value: dq_verify::VerifiedReport) -> VerifiedReport { + VerifiedReport { + status: value.status, + advisory_ids: value.advisory_ids, + report: report(value.report), + ppid: value.ppid, + qe_status: tcb_status_with_advisory(value.qe_status), + platform_status: tcb_status_with_advisory(value.platform_status), + } +} + +fn report(value: dq_quote::Report) -> Report { + match value { + dq_quote::Report::SgxEnclave(r) => Report::SgxEnclave(enclave_report(r)), + dq_quote::Report::TD10(r) => Report::TD10(td_report_10(r)), + dq_quote::Report::TD15(r) => Report::TD15(td_report_15(r)), + } +} + +fn td_report_10(value: dq_quote::TDReport10) -> TDReport10 { + TDReport10 { + tee_tcb_svn: value.tee_tcb_svn, + mr_seam: value.mr_seam, + mr_signer_seam: value.mr_signer_seam, + seam_attributes: value.seam_attributes, + td_attributes: value.td_attributes, + xfam: value.xfam, + mr_td: value.mr_td, + mr_config_id: value.mr_config_id, + mr_owner: value.mr_owner, + mr_owner_config: value.mr_owner_config, + rt_mr0: value.rt_mr0, + rt_mr1: value.rt_mr1, + rt_mr2: value.rt_mr2, + rt_mr3: value.rt_mr3, + report_data: value.report_data, + } +} + +fn td_report_15(value: dq_quote::TDReport15) -> TDReport15 { + TDReport15 { + base: td_report_10(value.base), + tee_tcb_svn2: value.tee_tcb_svn2, + mr_service_td: value.mr_service_td, + } +} + +fn enclave_report(value: dq_quote::EnclaveReport) -> EnclaveReport { + EnclaveReport { + cpu_svn: value.cpu_svn, + misc_select: value.misc_select, + reserved1: value.reserved1, + attributes: value.attributes, + mr_enclave: value.mr_enclave, + reserved2: value.reserved2, + mr_signer: value.mr_signer, + reserved3: value.reserved3, + isv_prod_id: value.isv_prod_id, + isv_svn: value.isv_svn, + reserved4: value.reserved4, + report_data: value.report_data, + } +} + +fn tcb_status(value: dq_tcb::TcbStatus) -> TcbStatus { + match value { + dq_tcb::TcbStatus::UpToDate => TcbStatus::UpToDate, + dq_tcb::TcbStatus::OutOfDateConfigurationNeeded => TcbStatus::OutOfDateConfigurationNeeded, + dq_tcb::TcbStatus::OutOfDate => TcbStatus::OutOfDate, + dq_tcb::TcbStatus::ConfigurationAndSWHardeningNeeded => { + TcbStatus::ConfigurationAndSWHardeningNeeded + } + dq_tcb::TcbStatus::ConfigurationNeeded => TcbStatus::ConfigurationNeeded, + dq_tcb::TcbStatus::SWHardeningNeeded => TcbStatus::SWHardeningNeeded, + dq_tcb::TcbStatus::Revoked => TcbStatus::Revoked, + } +} + +fn tcb_status_with_advisory(value: dq_tcb::TcbStatusWithAdvisory) -> TcbStatusWithAdvisory { + TcbStatusWithAdvisory { + status: tcb_status(value.status), + advisory_ids: value.advisory_ids, + } +} diff --git a/crates/tee-verifier/src/lib.rs b/crates/tee-verifier/src/lib.rs new file mode 100644 index 0000000000..9e93cb81f7 --- /dev/null +++ b/crates/tee-verifier/src/lib.rs @@ -0,0 +1,59 @@ +//! Stateless TEE attestation verifier contract. +//! +//! 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. +//! +//! See `docs/design/attestation-verifier-contract.md` for the design. + +use near_sdk::{env, near}; +use tee_verifier_interface::{Collateral, QuoteBytes, VerifiedReport}; + +mod conversions; + +// `dcap-qvl`'s `contract` feature pulls in `getrandom` but doesn't enable +// any backend. On `wasm32-unknown-unknown` we register a custom impl that +// returns `UNSUPPORTED`. Quote verification should not draw any randomness; +// if it ever does, the call fails loudly rather than silently with zeros. +#[cfg(target_arch = "wasm32")] +fn randomness_unsupported(_buf: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) +} +#[cfg(target_arch = "wasm32")] +getrandom::register_custom_getrandom!(randomness_unsupported); + +#[derive(Debug, Default)] +#[near(contract_state)] +pub struct TeeVerifier {} + +#[near] +impl TeeVerifier { + /// Verify a TDX / SGX quote against Intel collateral. + /// + /// Calls `dcap_qvl::verify::verify` with the current block timestamp + /// and returns the parsed `VerifiedReport` on success. The caller is + /// responsible for any post-DCAP policy (RTMR3 replay, report-data + /// binding, measurement allowlist matching, etc.). + /// + /// On verification failure, panics with the upstream error rendered as + /// a string. Callers should treat this as a `PromiseResult::Failed` in + /// their callback. + /// + /// Borsh I/O on both arguments and return value. + #[result_serializer(borsh)] + pub fn verify_quote( + &self, + #[serializer(borsh)] quote: QuoteBytes, + #[serializer(borsh)] collateral: Collateral, + ) -> VerifiedReport { + let now_seconds = env::block_timestamp_ms() / 1000; + let quote_bytes = conversions::quote_bytes_to_vec(quote); + let collateral = conversions::collateral_to_dcap(collateral); + match dcap_qvl::verify::verify("e_bytes, &collateral, now_seconds) { + Ok(report) => conversions::verified_report(report), + Err(err) => env::panic_str(&format!("dcap verification failed: {err:?}")), + } + } +} diff --git a/crates/tee-verifier/tests/verify_quote.rs b/crates/tee-verifier/tests/verify_quote.rs new file mode 100644 index 0000000000..1ac23271ad --- /dev/null +++ b/crates/tee-verifier/tests/verify_quote.rs @@ -0,0 +1,73 @@ +//! Integration test for the stateless `tee-verifier` contract. +//! +//! Calls `TeeVerifier::verify_quote` directly (no Promise round-trip) +//! with a real Dstack quote+collateral fixture taken from `test-utils`, +//! and asserts that the returned `VerifiedReport` carries the +//! `UpToDate` TCB status and a TD10 report. + +#![allow(non_snake_case)] + +use near_sdk::test_utils::VMContextBuilder; +use near_sdk::testing_env; +use std::time::Duration; +use tee_verifier::TeeVerifier; +use tee_verifier_interface::{Collateral, QuoteBytes}; +use test_utils::attestation::{VALID_ATTESTATION_TIMESTAMP, collateral as collateral_json, quote}; + +fn make_collateral() -> Collateral { + // `test_utils::attestation::collateral()` returns a `serde_json::Value` + // matching `attestation::Collateral`'s JSON shape. We re-parse it + // into the interface crate's mirror type by extracting the same + // field names that `dcap_qvl::QuoteCollateralV3` uses. + let v = collateral_json(); + Collateral { + pck_crl_issuer_chain: v["pck_crl_issuer_chain"].as_str().unwrap().to_string(), + root_ca_crl: hex::decode(v["root_ca_crl"].as_str().unwrap()).unwrap(), + pck_crl: hex::decode(v["pck_crl"].as_str().unwrap()).unwrap(), + tcb_info_issuer_chain: v["tcb_info_issuer_chain"].as_str().unwrap().to_string(), + tcb_info: v["tcb_info"].as_str().unwrap().to_string(), + tcb_info_signature: hex::decode(v["tcb_info_signature"].as_str().unwrap()).unwrap(), + qe_identity_issuer_chain: v["qe_identity_issuer_chain"].as_str().unwrap().to_string(), + qe_identity: v["qe_identity"].as_str().unwrap().to_string(), + qe_identity_signature: hex::decode(v["qe_identity_signature"].as_str().unwrap()).unwrap(), + pck_certificate_chain: v + .get("pck_certificate_chain") + .and_then(|s| s.as_str()) + .map(str::to_string), + } +} + +fn make_quote_bytes() -> QuoteBytes { + QuoteBytes(Vec::from(quote())) +} + +#[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; + testing_env!( + VMContextBuilder::new() + .block_timestamp(block_timestamp_ns) + .build() + ); + let contract = TeeVerifier::default(); + let quote = make_quote_bytes(); + let collateral = make_collateral(); + + // When + let report = contract.verify_quote(quote, collateral); + + // Then + assert_eq!(report.status, "UpToDate"); + assert!(report.advisory_ids.is_empty()); + let td10 = report + .report + .as_td10() + .expect("fixture is a TD10 attestation"); + // The fixture's report_data is 64 bytes; we only check shape, not exact contents, + // because that's bound to the keys baked into the fixture. + assert_eq!(td10.report_data.len(), 64); + assert_eq!(td10.mr_td.len(), 48); + assert_eq!(td10.rt_mr0.len(), 48); + assert_eq!(td10.rt_mr3.len(), 48); +} From 5196a326956e8195fc5db7be744eb238957b832b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Wed, 3 Jun 2026 11:08:09 +0200 Subject: [PATCH 2/5] build(tee-verifier): enable near-sdk/unit-testing for tests via test-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. --- Cargo.lock | 3 ++- crates/tee-verifier/Cargo.toml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9383ed5606..843351e867 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11646,13 +11646,14 @@ dependencies = [ [[package]] name = "tee-verifier" -version = "3.9.0" +version = "3.11.0" dependencies = [ "borsh", "dcap-qvl", "getrandom 0.2.17", "hex", "near-sdk", + "tee-verifier", "tee-verifier-interface", "test-utils", ] diff --git a/crates/tee-verifier/Cargo.toml b/crates/tee-verifier/Cargo.toml index d24867aa27..6e0f14bffe 100644 --- a/crates/tee-verifier/Cargo.toml +++ b/crates/tee-verifier/Cargo.toml @@ -30,6 +30,9 @@ crate-type = ["cdylib", "lib"] # and required for ABI / Borsh schema generation. Pulls in # `borsh/unstable__schema` and `BorshSchema` derives on the wire types. abi = ["borsh/unstable__schema", "tee-verifier-interface/borsh-schema"] +# Enables `near_sdk::testing_env!` for tests; the workspace `near-sdk` +# dependency no longer turns on `unit-testing` by default. +test-utils = ["near-sdk/unit-testing"] [dependencies] borsh = { workspace = true } @@ -42,6 +45,7 @@ getrandom = { workspace = true, features = ["custom"] } [dev-dependencies] hex = { workspace = true } +tee-verifier = { path = ".", features = ["test-utils"] } test-utils = { workspace = true } [lints] From 18aca60c2d24be9b5015f2cd044a275fcb6529b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Wed, 3 Jun 2026 12:21:56 +0200 Subject: [PATCH 3/5] refactor(tee-verifier): return Result via #[handle_result], move error out of the wire crate verify_quote now returns Result 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. --- Cargo.lock | 1 + crates/tee-verifier/Cargo.toml | 1 + crates/tee-verifier/src/lib.rs | 34 ++++++++++++++--------- crates/tee-verifier/tests/verify_quote.rs | 4 ++- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 843351e867..f69ce5ffc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11656,6 +11656,7 @@ dependencies = [ "tee-verifier", "tee-verifier-interface", "test-utils", + "thiserror 2.0.18", ] [[package]] diff --git a/crates/tee-verifier/Cargo.toml b/crates/tee-verifier/Cargo.toml index 6e0f14bffe..e120e882e7 100644 --- a/crates/tee-verifier/Cargo.toml +++ b/crates/tee-verifier/Cargo.toml @@ -39,6 +39,7 @@ borsh = { workspace = true } dcap-qvl = { workspace = true } near-sdk = { workspace = true } tee-verifier-interface = { workspace = true } +thiserror = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { workspace = true, features = ["custom"] } diff --git a/crates/tee-verifier/src/lib.rs b/crates/tee-verifier/src/lib.rs index 9e93cb81f7..7f73ecde69 100644 --- a/crates/tee-verifier/src/lib.rs +++ b/crates/tee-verifier/src/lib.rs @@ -8,11 +8,25 @@ //! //! See `docs/design/attestation-verifier-contract.md` for the design. -use near_sdk::{env, near}; +use near_sdk::{FunctionError, env, near}; use tee_verifier_interface::{Collateral, QuoteBytes, VerifiedReport}; mod conversions; +/// Failure returned by [`TeeVerifier::verify_quote`]. +#[derive(Debug, Clone, thiserror::Error)] +pub enum VerifierError { + /// `dcap_qvl::verify::verify` rejected the quote / collateral. + #[error("dcap verification failed: {0}")] + DcapVerification(String), +} + +impl FunctionError for VerifierError { + fn panic(&self) -> ! { + env::panic_str(&self.to_string()) + } +} + // `dcap-qvl`'s `contract` feature pulls in `getrandom` but doesn't enable // any backend. On `wasm32-unknown-unknown` we register a custom impl that // returns `UNSUPPORTED`. Quote verification should not draw any randomness; @@ -30,30 +44,24 @@ pub struct TeeVerifier {} #[near] impl TeeVerifier { - /// Verify a TDX / SGX quote against Intel collateral. + /// Verify a TDX quote against Intel collateral. /// /// Calls `dcap_qvl::verify::verify` with the current block timestamp /// and returns the parsed `VerifiedReport` on success. The caller is /// responsible for any post-DCAP policy (RTMR3 replay, report-data /// binding, measurement allowlist matching, etc.). - /// - /// On verification failure, panics with the upstream error rendered as - /// a string. Callers should treat this as a `PromiseResult::Failed` in - /// their callback. - /// - /// Borsh I/O on both arguments and return value. + #[handle_result] #[result_serializer(borsh)] pub fn verify_quote( &self, #[serializer(borsh)] quote: QuoteBytes, #[serializer(borsh)] collateral: Collateral, - ) -> VerifiedReport { + ) -> Result { let now_seconds = env::block_timestamp_ms() / 1000; let quote_bytes = conversions::quote_bytes_to_vec(quote); let collateral = conversions::collateral_to_dcap(collateral); - match dcap_qvl::verify::verify("e_bytes, &collateral, now_seconds) { - Ok(report) => conversions::verified_report(report), - Err(err) => env::panic_str(&format!("dcap verification failed: {err:?}")), - } + dcap_qvl::verify::verify("e_bytes, &collateral, now_seconds) + .map(conversions::verified_report) + .map_err(|err| VerifierError::DcapVerification(format!("{err:?}"))) } } diff --git a/crates/tee-verifier/tests/verify_quote.rs b/crates/tee-verifier/tests/verify_quote.rs index 1ac23271ad..46b418705b 100644 --- a/crates/tee-verifier/tests/verify_quote.rs +++ b/crates/tee-verifier/tests/verify_quote.rs @@ -55,7 +55,9 @@ fn verify_quote__should_return_up_to_date_td10_report_for_valid_fixture() { let collateral = make_collateral(); // When - let report = contract.verify_quote(quote, collateral); + let report = contract + .verify_quote(quote, collateral) + .expect("valid fixture should verify"); // Then assert_eq!(report.status, "UpToDate"); From d63012fe6f969a02ca92183eafcb20c5357bfd9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Wed, 3 Jun 2026 13:16:23 +0200 Subject: [PATCH 4/5] chore(tee-verifier): drop unused repository manifest field Most internal workspace crates omit `repository`; only the published/SDK-style crates set it. Drop it here to match the common convention. --- crates/tee-verifier/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/tee-verifier/Cargo.toml b/crates/tee-verifier/Cargo.toml index e120e882e7..1f97bf7380 100644 --- a/crates/tee-verifier/Cargo.toml +++ b/crates/tee-verifier/Cargo.toml @@ -3,7 +3,6 @@ name = "tee-verifier" version = { workspace = true } license = { workspace = true } edition = { workspace = true } -repository = "https://github.com/near/mpc" [package.metadata.cargo-shear] ignored = ["borsh"] From cf72a5ab610bf540343e5f0a88a9d01a101ded7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Wed, 3 Jun 2026 16:29:53 +0200 Subject: [PATCH 5/5] test(tee-verifier): pin Borsh wire layout against dcap-qvl; convert via 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. --- Cargo.lock | 1 + crates/tee-verifier/Cargo.toml | 1 + crates/tee-verifier/src/conversions.rs | 391 +++++++++++++++++----- crates/tee-verifier/src/lib.rs | 11 +- crates/tee-verifier/tests/verify_quote.rs | 83 ++++- 5 files changed, 378 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f69ce5ffc6..35b3934105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11653,6 +11653,7 @@ dependencies = [ "getrandom 0.2.17", "hex", "near-sdk", + "rstest", "tee-verifier", "tee-verifier-interface", "test-utils", diff --git a/crates/tee-verifier/Cargo.toml b/crates/tee-verifier/Cargo.toml index 1f97bf7380..95dc15c8ea 100644 --- a/crates/tee-verifier/Cargo.toml +++ b/crates/tee-verifier/Cargo.toml @@ -45,6 +45,7 @@ getrandom = { workspace = true, features = ["custom"] } [dev-dependencies] hex = { workspace = true } +rstest = { workspace = true } tee-verifier = { path = ".", features = ["test-utils"] } test-utils = { workspace = true } diff --git a/crates/tee-verifier/src/conversions.rs b/crates/tee-verifier/src/conversions.rs index 2a0f9bf749..ef1377567f 100644 --- a/crates/tee-verifier/src/conversions.rs +++ b/crates/tee-verifier/src/conversions.rs @@ -1,15 +1,9 @@ //! Conversions between `dcap_qvl`'s types and the Borsh-mirrored types in -//! `tee-verifier-interface`. +//! `tee-verifier-interface`. They live here, not in the interface crate, so +//! that crate stays `no_std` and free of `dcap-qvl`. //! -//! These conversions live here (in the contract crate that already -//! depends on `dcap-qvl`) rather than in `tee-verifier-interface`, so -//! that the interface crate stays free of `dcap-qvl` and can be linked -//! into consumer contracts without dragging in `ring`/`webpki`/X.509 -//! parsing. -//! -//! Free functions are used rather than `From`/`Into` impls because the -//! orphan rule forbids implementing a foreign trait between two foreign -//! types from a third crate. +//! Mapped with the local [`IntoDcapType`] / [`IntoInterfaceType`] traits. We +//! can not use [`From`] and [`Into`] due to the [*orphan rule*](https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules). use dcap_qvl::{quote as dq_quote, tcb_info as dq_tcb, verify as dq_verify}; use tee_verifier_interface::{ @@ -17,106 +11,329 @@ use tee_verifier_interface::{ TcbStatusWithAdvisory, VerifiedReport, }; -pub fn collateral_to_dcap(value: Collateral) -> dcap_qvl::QuoteCollateralV3 { - dcap_qvl::QuoteCollateralV3 { - pck_crl_issuer_chain: value.pck_crl_issuer_chain, - root_ca_crl: value.root_ca_crl, - pck_crl: value.pck_crl, - tcb_info_issuer_chain: value.tcb_info_issuer_chain, - tcb_info: value.tcb_info, - tcb_info_signature: value.tcb_info_signature, - qe_identity_issuer_chain: value.qe_identity_issuer_chain, - qe_identity: value.qe_identity, - qe_identity_signature: value.qe_identity_signature, - pck_certificate_chain: value.pck_certificate_chain, +/// Converts an interface type into its `dcap_qvl` counterpart `T`. +pub(crate) trait IntoDcapType { + fn into_dcap_type(self) -> T; +} + +/// Converts a `dcap_qvl` type into its `tee-verifier-interface` counterpart `T`. +pub(crate) trait IntoInterfaceType { + fn into_interface_type(self) -> T; +} + +impl IntoDcapType for Collateral { + fn into_dcap_type(self) -> dcap_qvl::QuoteCollateralV3 { + dcap_qvl::QuoteCollateralV3 { + pck_crl_issuer_chain: self.pck_crl_issuer_chain, + root_ca_crl: self.root_ca_crl, + pck_crl: self.pck_crl, + tcb_info_issuer_chain: self.tcb_info_issuer_chain, + tcb_info: self.tcb_info, + tcb_info_signature: self.tcb_info_signature, + qe_identity_issuer_chain: self.qe_identity_issuer_chain, + qe_identity: self.qe_identity, + qe_identity_signature: self.qe_identity_signature, + pck_certificate_chain: self.pck_certificate_chain, + } } } -pub fn quote_bytes_to_vec(value: QuoteBytes) -> Vec { - value.0 +impl IntoDcapType> for QuoteBytes { + fn into_dcap_type(self) -> Vec { + self.0 + } } -pub fn verified_report(value: dq_verify::VerifiedReport) -> VerifiedReport { - VerifiedReport { - status: value.status, - advisory_ids: value.advisory_ids, - report: report(value.report), - ppid: value.ppid, - qe_status: tcb_status_with_advisory(value.qe_status), - platform_status: tcb_status_with_advisory(value.platform_status), +impl IntoInterfaceType for dq_verify::VerifiedReport { + fn into_interface_type(self) -> VerifiedReport { + VerifiedReport { + status: self.status, + advisory_ids: self.advisory_ids, + report: self.report.into_interface_type(), + ppid: self.ppid, + qe_status: self.qe_status.into_interface_type(), + platform_status: self.platform_status.into_interface_type(), + } + } +} + +impl IntoInterfaceType for dq_quote::Report { + fn into_interface_type(self) -> Report { + match self { + dq_quote::Report::SgxEnclave(r) => Report::SgxEnclave(r.into_interface_type()), + dq_quote::Report::TD10(r) => Report::TD10(r.into_interface_type()), + dq_quote::Report::TD15(r) => Report::TD15(r.into_interface_type()), + } } } -fn report(value: dq_quote::Report) -> Report { - match value { - dq_quote::Report::SgxEnclave(r) => Report::SgxEnclave(enclave_report(r)), - dq_quote::Report::TD10(r) => Report::TD10(td_report_10(r)), - dq_quote::Report::TD15(r) => Report::TD15(td_report_15(r)), +impl IntoInterfaceType for dq_quote::TDReport10 { + fn into_interface_type(self) -> TDReport10 { + TDReport10 { + tee_tcb_svn: self.tee_tcb_svn, + mr_seam: self.mr_seam, + mr_signer_seam: self.mr_signer_seam, + seam_attributes: self.seam_attributes, + td_attributes: self.td_attributes, + xfam: self.xfam, + mr_td: self.mr_td, + mr_config_id: self.mr_config_id, + mr_owner: self.mr_owner, + mr_owner_config: self.mr_owner_config, + rt_mr0: self.rt_mr0, + rt_mr1: self.rt_mr1, + rt_mr2: self.rt_mr2, + rt_mr3: self.rt_mr3, + report_data: self.report_data, + } } } -fn td_report_10(value: dq_quote::TDReport10) -> TDReport10 { - TDReport10 { - tee_tcb_svn: value.tee_tcb_svn, - mr_seam: value.mr_seam, - mr_signer_seam: value.mr_signer_seam, - seam_attributes: value.seam_attributes, - td_attributes: value.td_attributes, - xfam: value.xfam, - mr_td: value.mr_td, - mr_config_id: value.mr_config_id, - mr_owner: value.mr_owner, - mr_owner_config: value.mr_owner_config, - rt_mr0: value.rt_mr0, - rt_mr1: value.rt_mr1, - rt_mr2: value.rt_mr2, - rt_mr3: value.rt_mr3, - report_data: value.report_data, +impl IntoInterfaceType for dq_quote::TDReport15 { + fn into_interface_type(self) -> TDReport15 { + TDReport15 { + base: self.base.into_interface_type(), + tee_tcb_svn2: self.tee_tcb_svn2, + mr_service_td: self.mr_service_td, + } } } -fn td_report_15(value: dq_quote::TDReport15) -> TDReport15 { - TDReport15 { - base: td_report_10(value.base), - tee_tcb_svn2: value.tee_tcb_svn2, - mr_service_td: value.mr_service_td, +impl IntoInterfaceType for dq_quote::EnclaveReport { + fn into_interface_type(self) -> EnclaveReport { + EnclaveReport { + cpu_svn: self.cpu_svn, + misc_select: self.misc_select, + reserved1: self.reserved1, + attributes: self.attributes, + mr_enclave: self.mr_enclave, + reserved2: self.reserved2, + mr_signer: self.mr_signer, + reserved3: self.reserved3, + isv_prod_id: self.isv_prod_id, + isv_svn: self.isv_svn, + reserved4: self.reserved4, + report_data: self.report_data, + } } } -fn enclave_report(value: dq_quote::EnclaveReport) -> EnclaveReport { - EnclaveReport { - cpu_svn: value.cpu_svn, - misc_select: value.misc_select, - reserved1: value.reserved1, - attributes: value.attributes, - mr_enclave: value.mr_enclave, - reserved2: value.reserved2, - mr_signer: value.mr_signer, - reserved3: value.reserved3, - isv_prod_id: value.isv_prod_id, - isv_svn: value.isv_svn, - reserved4: value.reserved4, - report_data: value.report_data, +impl IntoInterfaceType for dq_tcb::TcbStatus { + fn into_interface_type(self) -> TcbStatus { + match self { + dq_tcb::TcbStatus::UpToDate => TcbStatus::UpToDate, + dq_tcb::TcbStatus::OutOfDateConfigurationNeeded => { + TcbStatus::OutOfDateConfigurationNeeded + } + dq_tcb::TcbStatus::OutOfDate => TcbStatus::OutOfDate, + dq_tcb::TcbStatus::ConfigurationAndSWHardeningNeeded => { + TcbStatus::ConfigurationAndSWHardeningNeeded + } + dq_tcb::TcbStatus::ConfigurationNeeded => TcbStatus::ConfigurationNeeded, + dq_tcb::TcbStatus::SWHardeningNeeded => TcbStatus::SWHardeningNeeded, + dq_tcb::TcbStatus::Revoked => TcbStatus::Revoked, + } } } -fn tcb_status(value: dq_tcb::TcbStatus) -> TcbStatus { - match value { - dq_tcb::TcbStatus::UpToDate => TcbStatus::UpToDate, - dq_tcb::TcbStatus::OutOfDateConfigurationNeeded => TcbStatus::OutOfDateConfigurationNeeded, - dq_tcb::TcbStatus::OutOfDate => TcbStatus::OutOfDate, - dq_tcb::TcbStatus::ConfigurationAndSWHardeningNeeded => { - TcbStatus::ConfigurationAndSWHardeningNeeded +impl IntoInterfaceType for dq_tcb::TcbStatusWithAdvisory { + fn into_interface_type(self) -> TcbStatusWithAdvisory { + TcbStatusWithAdvisory { + status: self.status.into_interface_type(), + advisory_ids: self.advisory_ids, } - dq_tcb::TcbStatus::ConfigurationNeeded => TcbStatus::ConfigurationNeeded, - dq_tcb::TcbStatus::SWHardeningNeeded => TcbStatus::SWHardeningNeeded, - dq_tcb::TcbStatus::Revoked => TcbStatus::Revoked, } } -fn tcb_status_with_advisory(value: dq_tcb::TcbStatusWithAdvisory) -> TcbStatusWithAdvisory { - TcbStatusWithAdvisory { - status: tcb_status(value.status), - advisory_ids: value.advisory_ids, +/// 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( + 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); } } diff --git a/crates/tee-verifier/src/lib.rs b/crates/tee-verifier/src/lib.rs index 7f73ecde69..ddd7f6ccd9 100644 --- a/crates/tee-verifier/src/lib.rs +++ b/crates/tee-verifier/src/lib.rs @@ -12,9 +12,10 @@ use near_sdk::{FunctionError, env, near}; use tee_verifier_interface::{Collateral, QuoteBytes, VerifiedReport}; mod conversions; +use conversions::{IntoDcapType as _, IntoInterfaceType as _}; /// Failure returned by [`TeeVerifier::verify_quote`]. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum VerifierError { /// `dcap_qvl::verify::verify` rejected the quote / collateral. #[error("dcap verification failed: {0}")] @@ -58,10 +59,10 @@ impl TeeVerifier { #[serializer(borsh)] collateral: Collateral, ) -> Result { let now_seconds = env::block_timestamp_ms() / 1000; - let quote_bytes = conversions::quote_bytes_to_vec(quote); - let collateral = conversions::collateral_to_dcap(collateral); + let quote_bytes: Vec = quote.into_dcap_type(); + let collateral = collateral.into_dcap_type(); dcap_qvl::verify::verify("e_bytes, &collateral, now_seconds) - .map(conversions::verified_report) - .map_err(|err| VerifierError::DcapVerification(format!("{err:?}"))) + .map(|report| report.into_interface_type()) + .map_err(|err| VerifierError::DcapVerification(format!("{err}"))) } } diff --git a/crates/tee-verifier/tests/verify_quote.rs b/crates/tee-verifier/tests/verify_quote.rs index 46b418705b..1ac9c561e7 100644 --- a/crates/tee-verifier/tests/verify_quote.rs +++ b/crates/tee-verifier/tests/verify_quote.rs @@ -2,16 +2,17 @@ //! //! Calls `TeeVerifier::verify_quote` directly (no Promise round-trip) //! with a real Dstack quote+collateral fixture taken from `test-utils`, -//! and asserts that the returned `VerifiedReport` carries the -//! `UpToDate` TCB status and a TD10 report. +//! and asserts the returned `VerifiedReport` matches the fixture's known +//! value in full. #![allow(non_snake_case)] -use near_sdk::test_utils::VMContextBuilder; -use near_sdk::testing_env; +use near_sdk::{test_utils::VMContextBuilder, testing_env}; use std::time::Duration; use tee_verifier::TeeVerifier; -use tee_verifier_interface::{Collateral, QuoteBytes}; +use tee_verifier_interface::{ + Collateral, QuoteBytes, Report, TDReport10, TcbStatus, TcbStatusWithAdvisory, VerifiedReport, +}; use test_utils::attestation::{VALID_ATTESTATION_TIMESTAMP, collateral as collateral_json, quote}; fn make_collateral() -> Collateral { @@ -60,16 +61,64 @@ fn verify_quote__should_return_up_to_date_td10_report_for_valid_fixture() { .expect("valid fixture should verify"); // Then - assert_eq!(report.status, "UpToDate"); - assert!(report.advisory_ids.is_empty()); - let td10 = report - .report - .as_td10() - .expect("fixture is a TD10 attestation"); - // The fixture's report_data is 64 bytes; we only check shape, not exact contents, - // because that's bound to the keys baked into the fixture. - assert_eq!(td10.report_data.len(), 64); - assert_eq!(td10.mr_td.len(), 48); - assert_eq!(td10.rt_mr0.len(), 48); - assert_eq!(td10.rt_mr3.len(), 48); + let expected = VerifiedReport { + status: "UpToDate".to_string(), + advisory_ids: vec![], + report: Report::TD10(TDReport10 { + tee_tcb_svn: hex_arr("0b010400000000000000000000000000"), + mr_seam: hex_arr( + "7bf063280e94fb051f5dd7b1fc59ce9aac42bb961df8d44b709c9b0ff87a7b4df648657ba6d1189589feab1d5a3c9a9d", + ), + mr_signer_seam: hex_arr( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + seam_attributes: hex_arr("0000000000000000"), + td_attributes: hex_arr("0000001000000000"), + xfam: hex_arr("e702060000000000"), + mr_td: hex_arr( + "f06dfda6dce1cf904d4e2bab1dc370634cf95cefa2ceb2de2eee127c9382698090d7a4a13e14c536ec6c9c3c8fa87077", + ), + mr_config_id: hex_arr( + "01cb9b2d6204f5e44238b75f69e3a3069550734c0d99ebdd3be507c238a261d8fa000000000000000000000000000000", + ), + mr_owner: hex_arr( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + mr_owner_config: hex_arr( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), + rt_mr0: hex_arr( + "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46", + ), + rt_mr1: hex_arr( + "b598fde9491427341bc4683b75d10d3e36770af3a36a6954d8b6b7b22aa66358f13e1f172e51b7d6e6710d99a8d8532f", + ), + rt_mr2: hex_arr( + "c812d42bfff1c75382e91a37c867ab117b97eb5e8d6797488928ea38e5fd38b5ed2f87d9613d392507f1c3af94657c93", + ), + rt_mr3: hex_arr( + "b7662ac19c27af648a939be042684bbdb43bb3dddf4cd17bb21f4d455ab1926c6ee57038152fc46ddea392c47eb2af27", + ), + report_data: hex_arr( + "00014ee5e70e861db29a95224e48a47c016ab03c61238333319af7614593cd155ba531073edd69921742beb1c510ff4339480000000000000000000000000000", + ), + }), + ppid: hex::decode("d208dfb1002346ae1bb4ef2a3c055292").unwrap(), + qe_status: TcbStatusWithAdvisory { + status: TcbStatus::UpToDate, + advisory_ids: vec![], + }, + platform_status: TcbStatusWithAdvisory { + status: TcbStatus::UpToDate, + advisory_ids: vec![], + }, + }; + assert_eq!(report, expected); +} + +fn hex_arr(s: &str) -> [u8; N] { + hex::decode(s) + .expect("valid hex") + .try_into() + .expect("correct length") }