diff --git a/Cargo.lock b/Cargo.lock index 590898421..35b393410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11644,6 +11644,22 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tee-verifier" +version = "3.11.0" +dependencies = [ + "borsh", + "dcap-qvl", + "getrandom 0.2.17", + "hex", + "near-sdk", + "rstest", + "tee-verifier", + "tee-verifier-interface", + "test-utils", + "thiserror 2.0.18", +] + [[package]] name = "tee-verifier-interface" version = "3.11.0" diff --git a/Cargo.toml b/Cargo.toml index dfcbc0c67..045201999 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 000000000..95dc15c8e --- /dev/null +++ b/crates/tee-verifier/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "tee-verifier" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[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"] +# 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 } +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"] } + +[dev-dependencies] +hex = { workspace = true } +rstest = { workspace = true } +tee-verifier = { path = ".", features = ["test-utils"] } +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 000000000..ef1377567 --- /dev/null +++ b/crates/tee-verifier/src/conversions.rs @@ -0,0 +1,339 @@ +//! Conversions between `dcap_qvl`'s types and the Borsh-mirrored types in +//! `tee-verifier-interface`. They live here, not in the interface crate, so +//! that crate stays `no_std` and free of `dcap-qvl`. +//! +//! 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::{ + Collateral, EnclaveReport, QuoteBytes, Report, TDReport10, TDReport15, TcbStatus, + TcbStatusWithAdvisory, VerifiedReport, +}; + +/// 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, + } + } +} + +impl IntoDcapType> for QuoteBytes { + fn into_dcap_type(self) -> Vec { + self.0 + } +} + +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()), + } + } +} + +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, + } + } +} + +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, + } + } +} + +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, + } + } +} + +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, + } + } +} + +impl IntoInterfaceType for dq_tcb::TcbStatusWithAdvisory { + fn into_interface_type(self) -> TcbStatusWithAdvisory { + TcbStatusWithAdvisory { + status: self.status.into_interface_type(), + advisory_ids: self.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 new file mode 100644 index 000000000..ddd7f6ccd --- /dev/null +++ b/crates/tee-verifier/src/lib.rs @@ -0,0 +1,68 @@ +//! 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::{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, 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; +// 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 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.). + #[handle_result] + #[result_serializer(borsh)] + pub fn verify_quote( + &self, + #[serializer(borsh)] quote: QuoteBytes, + #[serializer(borsh)] collateral: Collateral, + ) -> Result { + let now_seconds = env::block_timestamp_ms() / 1000; + let quote_bytes: Vec = quote.into_dcap_type(); + let collateral = collateral.into_dcap_type(); + dcap_qvl::verify::verify("e_bytes, &collateral, now_seconds) + .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 new file mode 100644 index 000000000..1ac9c561e --- /dev/null +++ b/crates/tee-verifier/tests/verify_quote.rs @@ -0,0 +1,124 @@ +//! 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 the returned `VerifiedReport` matches the fixture's known +//! value in full. + +#![allow(non_snake_case)] + +use near_sdk::{test_utils::VMContextBuilder, testing_env}; +use std::time::Duration; +use tee_verifier::TeeVerifier; +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 { + // `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) + .expect("valid fixture should verify"); + + // Then + 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") +}