diff --git a/Cargo.lock b/Cargo.lock index 69b52d95d2..a28350b22d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -967,16 +967,15 @@ name = "attestation" version = "3.9.0" dependencies = [ "assert_matches", + "attestation-types", "borsh", "dcap-qvl", "derive_more 2.1.1", "dstack-sdk-types", "hex", - "rstest", "serde", "serde_json", - "serde_with", - "sha2 0.10.9", + "tee-verifier-interface", "test-utils", "thiserror 2.0.18", ] @@ -1003,6 +1002,23 @@ dependencies = [ "url", ] +[[package]] +name = "attestation-types" +version = "3.9.0" +dependencies = [ + "borsh", + "derive_more 2.1.1", + "dstack-sdk-types", + "hex", + "rstest", + "serde", + "serde_json", + "serde_with", + "sha2 0.10.9", + "tee-verifier-interface", + "thiserror 2.0.18", +] + [[package]] name = "attohttpc" version = "0.19.1" @@ -11399,6 +11415,28 @@ 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.9.0" +dependencies = [ + "borsh", + "rstest", + "thiserror 2.0.18", +] + [[package]] name = "tempfile" version = "3.27.0" diff --git a/Cargo.toml b/Cargo.toml index b48085e50a..81904cf320 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "3" members = [ "crates/attestation", "crates/attestation-cli", + "crates/attestation-types", "crates/backup-cli", "crates/chain-gateway", "crates/chain-gateway-test-contract", @@ -28,6 +29,8 @@ 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", "crates/test-port-allocator", @@ -46,6 +49,7 @@ repository = "https://github.com/near/mpc" [workspace.dependencies] #workspace members attestation = { path = "crates/attestation" } +attestation-types = { path = "crates/attestation-types" } chain-gateway = { path = "crates/chain-gateway" } chain-gateway-test-contract = { path = "crates/chain-gateway-test-contract" } contract-history = { path = "crates/contract-history" } @@ -66,6 +70,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/attestation-types/Cargo.toml b/crates/attestation-types/Cargo.toml new file mode 100644 index 0000000000..4ec9599e27 --- /dev/null +++ b/crates/attestation-types/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "attestation-types" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = "https://github.com/near/mpc" + +[features] +borsh-schema = ["borsh/unstable__schema", "tee-verifier-interface/borsh-schema"] +dstack-conversions = ["dep:dstack-sdk-types"] +test-utils = [] + +[dependencies] +borsh = { workspace = true } +derive_more = { workspace = true } +dstack-sdk-types = { workspace = true, optional = true } +hex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_with = { workspace = true } +sha2 = { workspace = true } +tee-verifier-interface = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +dstack-sdk-types = { workspace = true } +rstest = { workspace = true } + +[lints] +workspace = true diff --git a/crates/attestation-types/assets/event_log.json b/crates/attestation-types/assets/event_log.json new file mode 100644 index 0000000000..8790b37a86 --- /dev/null +++ b/crates/attestation-types/assets/event_log.json @@ -0,0 +1,7 @@ +{ + "imr": 0, + "event_type": 2147483659, + "digest": "8ae1e425351df7992c444586eff99d35af3b779aa2b0e981cb4b73bc5b279f2ade19b6a62a203fc3c3bbdaae80af596d", + "event": "", + "event_payload": "095464785461626c65000100000000000000af96bb93f2b9b84e9462e0ba745642360090800000000000" +} diff --git a/crates/attestation-types/assets/tcb_info.json b/crates/attestation-types/assets/tcb_info.json new file mode 100644 index 0000000000..f1c777e178 --- /dev/null +++ b/crates/attestation-types/assets/tcb_info.json @@ -0,0 +1,216 @@ +{ + "mrtd": "f06dfda6dce1cf904d4e2bab1dc370634cf95cefa2ceb2de2eee127c9382698090d7a4a13e14c536ec6c9c3c8fa87077", + "rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46", + "rtmr1": "b598fde9491427341bc4683b75d10d3e36770af3a36a6954d8b6b7b22aa66358f13e1f172e51b7d6e6710d99a8d8532f", + "rtmr2": "c812d42bfff1c75382e91a37c867ab117b97eb5e8d6797488928ea38e5fd38b5ed2f87d9613d392507f1c3af94657c93", + "rtmr3": "b7662ac19c27af648a939be042684bbdb43bb3dddf4cd17bb21f4d455ab1926c6ee57038152fc46ddea392c47eb2af27", + "os_image_hash": "7d47512fda31dc5a7318f72ae1869a3c76323981eea21fc30cafd0f79668642c", + "compose_hash": "cb9b2d6204f5e44238b75f69e3a3069550734c0d99ebdd3be507c238a261d8fa", + "device_id": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1774945636\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:1f54b55bad22c45067228a9262bc6377e393ca1a07edb64e691e80704f49b74e\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:rw\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}", + "event_log": [ + { + "imr": 0, + "event_type": 2147483659, + "digest": "8ae1e425351df7992c444586eff99d35af3b779aa2b0e981cb4b73bc5b279f2ade19b6a62a203fc3c3bbdaae80af596d", + "event": "", + "event_payload": "095464785461626c65000100000000000000af96bb93f2b9b84e9462e0ba745642360090800000000000" + }, + { + "imr": 0, + "event_type": 2147483658, + "digest": "344bc51c980ba621aaa00da3ed7436f7d6e549197dfe699515dfa2c6583d95e6412af21c097d473155875ffd561d6790", + "event": "", + "event_payload": "2946762858585858585858582d585858582d585858582d585858582d58585858585858585858585829000000c0ff000000000040080000000000" + }, + { + "imr": 0, + "event_type": 2147483649, + "digest": "9dc3a1f80bcec915391dcda5ffbb15e7419f77eab462bbf72b42166fb70d50325e37b36f93537a863769bcf9bedae6fb", + "event": "", + "event_payload": "61dfe48bca93d211aa0d00e098032b8c0a00000000000000000000000000000053006500630075007200650042006f006f007400" + }, + { + "imr": 0, + "event_type": 2147483649, + "digest": "6f2e3cbc14f9def86980f5f66fd85e99d63e69a73014ed8a5633ce56eca5b64b692108c56110e22acadcef58c3250f1b", + "event": "", + "event_payload": "61dfe48bca93d211aa0d00e098032b8c0200000000000000000000000000000050004b00" + }, + { + "imr": 0, + "event_type": 2147483649, + "digest": "d607c0efb41c0d757d69bca0615c3a9ac0b1db06c557d992e906c6b7dee40e0e031640c7bfd7bcd35844ef9edeadc6f9", + "event": "", + "event_payload": "61dfe48bca93d211aa0d00e098032b8c030000000000000000000000000000004b0045004b00" + }, + { + "imr": 0, + "event_type": 2147483649, + "digest": "08a74f8963b337acb6c93682f934496373679dd26af1089cb4eaf0c30cf260a12e814856385ab8843e56a9acea19e127", + "event": "", + "event_payload": "cbb219d73a3d9645a3bcdad00e67656f0200000000000000000000000000000064006200" + }, + { + "imr": 0, + "event_type": 2147483649, + "digest": "18cc6e01f0c6ea99aa23f8a280423e94ad81d96d0aeb5180504fc0f7a40cb3619dd39bd6a95ec1680a86ed6ab0f9828d", + "event": "", + "event_payload": "cbb219d73a3d9645a3bcdad00e67656f03000000000000000000000000000000640062007800" + }, + { + "imr": 0, + "event_type": 4, + "digest": "394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0", + "event": "", + "event_payload": "00000000" + }, + { + "imr": 0, + "event_type": 10, + "digest": "6dae15170c9fea6455681e3f838941a642ff9001a02a333e9ca8549af1db4ba47f01403e31dabe6e8a0b41ddd38b6d99", + "event": "", + "event_payload": "414350492044415441" + }, + { + "imr": 0, + "event_type": 10, + "digest": "b3a62232ef6be064cce25a8b92cf55d4a6c099ee7a9c0852ce0c7d572393dae84895c0f59a9db5000f0b34a90c1b1bec", + "event": "", + "event_payload": "414350492044415441" + }, + { + "imr": 0, + "event_type": 10, + "digest": "b6ed8ff3fca3c308f3f1ec7889054cc900b1c6dad9b14aedd0144d046626c81a5dbae47937f4949bb2d674a0bd699a7b", + "event": "", + "event_payload": "414350492044415441" + }, + { + "imr": 1, + "event_type": 2147483651, + "digest": "69e0bbb3861d993ddd4b79b40bb31cdc58ed540c754574e3c8af2432c5ac88e751a3b37bcf55a2352bad236b6ff4dc5c", + "event": "", + "event_payload": "18e0fd7a00000000008485000000000000000000000000002a000000000000000403140072f728144ab61e44b8c39ebdd7f893c7040412006b00650072006e0065006c0000007fff0400" + }, + { + "imr": 0, + "event_type": 2147483650, + "digest": "1dd6f7b457ad880d840d41c961283bab688e94e4b59359ea45686581e90feccea3c624b1226113f824f315eb60ae0a7c", + "event": "", + "event_payload": "61dfe48bca93d211aa0d00e098032b8c0900000000000000020000000000000042006f006f0074004f0072006400650072000000" + }, + { + "imr": 0, + "event_type": 2147483650, + "digest": "23ada07f5261f12f34a0bd8e46760962d6b4d576a416f1fea1c64bc656b1d28eacf7047ae6e967c58fd2a98bfa74c298", + "event": "", + "event_payload": "61dfe48bca93d211aa0d00e098032b8c08000000000000003e0000000000000042006f006f0074003000300030003000090100002c0055006900410070007000000004071400c9bdb87cebf8344faaea3ee4af6516a10406140021aa2c4614760345836e8ab6f46623317fff0400" + }, + { + "imr": 1, + "event_type": 2147483655, + "digest": "77a0dab2312b4e1e57a84d865a21e5b2ee8d677a21012ada819d0a98988078d3d740f6346bfe0abaa938ca20439a8d71", + "event": "", + "event_payload": "43616c6c696e6720454649204170706c69636174696f6e2066726f6d20426f6f74204f7074696f6e" + }, + { + "imr": 1, + "event_type": 4, + "digest": "394341b7182cd227c5c6b07ef8000cdfd86136c4292b8e576573ad7ed9ae41019f5818b4b971c9effc60e1ad9f1289f0", + "event": "", + "event_payload": "00000000" + }, + { + "imr": 2, + "event_type": 6, + "digest": "67f31a01871425f296100b04393a310fbf18e0a1d2ac19270b5a4609227e0e5fbe6ab838ef5822699a9e7216003378f6", + "event": "", + "event_payload": "ed223b8f1a0000004c4f414445445f494d4147453a3a4c6f61644f7074696f6e7300" + }, + { + "imr": 2, + "event_type": 6, + "digest": "dbfb59810302b3564182a02ae2fc5dcda5f8207380a7b18a9e6dd6fb648c33b1612eb10fb95ec620fb70b9c240ffda1c", + "event": "", + "event_payload": "ec223b8f0d0000004c696e757820696e6974726400" + }, + { + "imr": 1, + "event_type": 2147483655, + "digest": "214b0bef1379756011344877743fdc2a5382bac6e70362d624ccf3f654407c1b4badf7d8f9295dd3dabdef65b27677e0", + "event": "", + "event_payload": "4578697420426f6f7420536572766963657320496e766f636174696f6e" + }, + { + "imr": 1, + "event_type": 2147483655, + "digest": "0a2e01c85deae718a530ad8c6d20a84009babe6c8989269e950d8cf440c6e997695e64d455c4174a652cd080f6230b74", + "event": "", + "event_payload": "4578697420426f6f742053657276696365732052657475726e656420776974682053756363657373" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "f9974020ef507068183313d0ca808e0d1ca9b2d1ad0c61f5784e7157c362c06536f5ddacdad4451693f48fcc72fff624", + "event": "system-preparing", + "event_payload": "" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "8f4219433b1fbb548e8e3c9c0a308a09888871da6b0472f8b2057d14846b33f66d674ebaa5829f26e9ffb3d2745eb5ce", + "event": "app-id", + "event_payload": "cb9b2d6204f5e44238b75f69e3a3069550734c0d" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "f53085ad730605df556bbe4617e942a89186705897eac8974656be41ddcc516a08c4d2b9135fbf8a0f25836c26e1c0d3", + "event": "compose-hash", + "event_payload": "cb9b2d6204f5e44238b75f69e3a3069550734c0d99ebdd3be507c238a261d8fa" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "305a62e30e8f4ca791946c3ede6755cfacebe02be9101f0bccf2591509a0c8e8095bc83b3d53bfc5d70d6c7cf7813fc5", + "event": "instance-id", + "event_payload": "" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "98bd7e6bd3952720b65027fd494834045d06b4a714bf737a06b874638b3ea00ff402f7f583e3e3b05e921c8570433ac6", + "event": "boot-mr-done", + "event_payload": "" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "61ce56b6be756a9e45af7715b13c15040a4e6090cc740be24e2cc02e33b4fb53ae4e3c945c9af83e2a26c6d5efa414a8", + "event": "key-provider", + "event_payload": "7b226e616d65223a226c6f63616c2d736778222c226964223a2236623565643032653534396131633330616161386533313731613034356631663434396230303137333533656635393565373865333963333438633938643031227d" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "ba51104636900268b0e059fa3d266419d079d1e94aea26fb9fcbb8d764bf4c89a67ac271b8a0d1a3989945132a111fc7", + "event": "storage-fs", + "event_payload": "7a6673" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "1a76b2a80a0be71eae59f80945d876351a7a3fb8e9fd1ff1cede5734aa84ea11fd72b4edfbb6f04e5a85edd114c751bd", + "event": "system-ready", + "event_payload": "" + }, + { + "imr": 3, + "event_type": 134217729, + "digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678", + "event": "mpc-image-digest", + "event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980" + } + ] +} diff --git a/crates/attestation/src/app_compose.rs b/crates/attestation-types/src/app_compose.rs similarity index 100% rename from crates/attestation/src/app_compose.rs rename to crates/attestation-types/src/app_compose.rs diff --git a/crates/attestation-types/src/lib.rs b/crates/attestation-types/src/lib.rs new file mode 100644 index 0000000000..fe3e17269b --- /dev/null +++ b/crates/attestation-types/src/lib.rs @@ -0,0 +1,29 @@ +//! TEE attestation types and post-DCAP verification helpers, decoupled from +//! `dcap-qvl`. +//! +//! Crate contents: +//! - DTOs that `mpc-contract` and other consumers exchange and store: +//! [`tcb_info::TcbInfo`], [`app_compose::AppCompose`], +//! [`measurements::Measurements`] / [`measurements::ExpectedMeasurements`], +//! [`report_data::ReportData`]. +//! - The post-DCAP verification helpers ([`verify_post_dcap`]): RTMR3 replay, +//! app-compose validation, TCB-status / advisory-id checks, measurement +//! matching. These operate on the +//! [`tee_verifier_interface::VerifiedReport`] mirror — *not* on the +//! `dcap_qvl` type — so this crate has no `dcap-qvl` dependency and can +//! be linked into consumer contracts without dragging in +//! `ring`/`webpki`/X.509. +//! +//! The `dcap_qvl::verify::verify` call itself lives elsewhere — in the +//! `attestation` crate for off-chain local verify, and in the +//! `tee-verifier` contract for cross-contract verify. + +#![no_std] + +extern crate alloc; + +pub mod app_compose; +pub mod measurements; +pub mod report_data; +pub mod tcb_info; +pub mod verify_post_dcap; diff --git a/crates/attestation/src/measurements.rs b/crates/attestation-types/src/measurements.rs similarity index 93% rename from crates/attestation/src/measurements.rs rename to crates/attestation-types/src/measurements.rs index 7608b22b5b..1bb476fc4a 100644 --- a/crates/attestation/src/measurements.rs +++ b/crates/attestation-types/src/measurements.rs @@ -70,10 +70,12 @@ impl From<&crate::tcb_info::TcbInfo> for Measurements { } } -impl TryFrom for Measurements { +impl TryFrom for Measurements { type Error = MeasurementsError; - fn try_from(verified_report: dcap_qvl::verify::VerifiedReport) -> Result { + fn try_from( + verified_report: tee_verifier_interface::VerifiedReport, + ) -> Result { let td10 = verified_report .report .as_td10() diff --git a/crates/attestation/src/report_data.rs b/crates/attestation-types/src/report_data.rs similarity index 100% rename from crates/attestation/src/report_data.rs rename to crates/attestation-types/src/report_data.rs diff --git a/crates/attestation/src/tcb_info.rs b/crates/attestation-types/src/tcb_info.rs similarity index 100% rename from crates/attestation/src/tcb_info.rs rename to crates/attestation-types/src/tcb_info.rs diff --git a/crates/attestation-types/src/verify_post_dcap.rs b/crates/attestation-types/src/verify_post_dcap.rs new file mode 100644 index 0000000000..f81cbad5d9 --- /dev/null +++ b/crates/attestation-types/src/verify_post_dcap.rs @@ -0,0 +1,432 @@ +//! Post-DCAP verification helpers. +//! +//! These functions run after `dcap_qvl::verify::verify` has already validated +//! the quote's cryptographic chain and produced a `VerifiedReport`. They +//! take the [`tee_verifier_interface::VerifiedReport`] mirror (not the +//! upstream `dcap_qvl` type), so this module compiles without any +//! `dcap-qvl` dependency and can be linked into consumer contracts. +//! +//! The actual `dcap_qvl::verify::verify` call lives elsewhere — for +//! local-verify in the `attestation` crate, and for cross-contract verify +//! in the `tee-verifier` contract. + +use alloc::string::{String, ToString}; + +use sha2::{Digest as _, Sha256, Sha384}; +use tee_verifier_interface::{TDReport10, VerifiedReport}; + +use crate::{ + app_compose::AppCompose, + measurements::ExpectedMeasurements, + report_data::ReportData, + tcb_info::{EventLog, TcbInfo}, +}; + +/// Expected TCB status for a successfully verified TEE quote. +pub const EXPECTED_QUOTE_STATUS: &str = "UpToDate"; + +// DSTACK_EVENT_TYPE is defined in https://github.com/Dstack-TEE/dstack/blob/cfa4cc4e8a4f525d537883b1a0ba5d9fbfd87f1e/tdx-attest/src/lib.rs#L28 +// It is the same for all events +pub const DSTACK_EVENT_TYPE: u32 = 134217729; + +pub const COMPOSE_HASH_EVENT: &str = "compose-hash"; +pub const KEY_PROVIDER_EVENT: &str = "key-provider"; + +pub const RTMR3_INDEX: u32 = 3; + +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] +pub enum VerificationError { + #[error("could not parse embedded measurements: {0}")] + EmbeddedMeasurementsParsing(crate::measurements::MeasurementsError), + #[error("dcap verification failed: {0}")] + DcapVerification(String), + #[error("verification report is not TD10")] + ReportNotTd10, + #[error("TCB status `{0}` is not up to date")] + TcbStatusNotUpToDate(String), + #[error("ouststanding advisories reported: {0}")] + NonEmptyAdvisoryIds(String), + #[error("wrong {name} hash (found {found} expected {expected})")] + WrongHash { + name: &'static str, + found: String, + expected: String, + }, + #[error("invalid event type {0}")] + InvalidEventType(u32), + #[error("failed to decode event digest `{0}`")] + EventDecoding(String), + #[error("failed to parse app compose JSON: {0}")] + AppComposeParsing(String), + #[error("no {0} event in event log")] + MissingEvent(&'static str), + #[error("duplicate {0} events in event log")] + DuplicateEvent(&'static str), + #[error("invalid app compose config: `{0}`")] + InvalidAppComposeConfig(String), + #[error("app-compose event payload had an unexpected size of {0}")] + AppComposeEventPayloadWrongSize(usize), + #[error("app-compose event payload `{0}` is not a hex string")] + AppComposeEventPayloadNotHex(String), + #[error( + "the attestation certificate with timestap {attestation_time} has expired since {expiry_time}" + )] + ExpiredCertificate { + attestation_time: u64, + expiry_time: u64, + }, + #[error("the mock attestation is invalid per definition")] + InvalidMockAttestation, + #[error("the allowed measurements list is empty")] + EmptyMeasurementsList, + #[error("the attestation's measurements are not in the allowed set")] + MeasurementsNotAllowed, + #[error("custom error: `{0}`")] + Custom(String), +} + +/// Verifies TCB status and security advisories. +pub fn verify_tcb_status(verified_report: &VerifiedReport) -> Result<(), VerificationError> { + // The "UpToDate" TCB status indicates that the measured platform components (CPU + // microcode, firmware, etc.) match the latest known good values published by Intel + // and do not require any updates or mitigations. + let status_is_up_to_date = verified_report.status == EXPECTED_QUOTE_STATUS; + + // Advisory IDs indicate known security vulnerabilities or issues with the TEE. + // For a quote to be considered secure, there should be no outstanding advisories. + let no_security_advisories = verified_report.advisory_ids.is_empty(); + + status_is_up_to_date + .or_err(|| VerificationError::TcbStatusNotUpToDate(verified_report.status.clone()))?; + + no_security_advisories.or_err(|| { + VerificationError::NonEmptyAdvisoryIds(verified_report.advisory_ids.join(", ")) + })?; + + Ok(()) +} + +/// Verifies report data matches expected values. +pub fn verify_report_data( + expected: &ReportData, + actual: &TDReport10, +) -> Result<(), VerificationError> { + // Check if sha384(tls_public_key) matches the hash in report_data. This check effectively + // proves that tls_public_key was included in the quote's report_data by an app running + // inside a TDX enclave. + compare_hashes("report_data", &actual.report_data, &expected.to_bytes()) +} + +/// Verifies RTMR3 by replaying the event log. +pub fn verify_rtmr3(report_data: &TDReport10, tcb_info: &TcbInfo) -> Result<(), VerificationError> { + compare_hashes("rtmr3", tcb_info.rtmr3.as_slice(), &report_data.rt_mr3)?; + verify_event_log_rtmr3(&tcb_info.event_log, report_data.rt_mr3) +} + +/// Verifies app compose configuration and hash. The compose-hash is measured into RTMR3, +/// and since it's (roughly) a hash of the unmeasured docker_compose_file, this is +/// sufficient to prove its validity. +pub fn verify_app_compose(tcb_info: &TcbInfo) -> Result<(), VerificationError> { + let app_compose: AppCompose = serde_json::from_str(&tcb_info.app_compose) + .map_err(|e| VerificationError::AppComposeParsing(e.to_string()))?; + + validate_app_compose_config(&app_compose) + .or_err(|| VerificationError::InvalidAppComposeConfig(tcb_info.app_compose.to_string()))?; + + let app_compose_event = tcb_info.get_single_event(COMPOSE_HASH_EVENT)?; + + compare_hex_hashes( + "app_compose_event_hash", + &app_compose_event.event_payload, + &hex::encode(*tcb_info.compose_hash), + )?; + + validate_app_compose_payload(&app_compose_event.event_payload, &tcb_info.app_compose) +} + +/// Try to verify static RTMRs and key_provider_digest against multiple expected +/// measurement sets. On success, returns the matched measurements. +pub fn verify_any_measurements( + report_data: &TDReport10, + tcb_info: &TcbInfo, + accepted_measurements: &[ExpectedMeasurements], +) -> Result { + for expected in accepted_measurements { + if verify_static_rtmrs(report_data, tcb_info, expected).is_ok() + && verify_key_provider_digest(tcb_info, &expected.key_provider_event_digest).is_ok() + { + return Ok(*expected); // found a valid match + } + } + + Err(VerificationError::WrongHash { + name: "expected_measurements", + expected: "one of the embedded TCB info sets (prod or dev)".into(), + found: "none matched".into(), + }) +} + +/// Verifies static RTMRs match expected values. +pub fn verify_static_rtmrs( + report_data: &TDReport10, + tcb_info: &TcbInfo, + expected_measurements: &ExpectedMeasurements, +) -> Result<(), VerificationError> { + // Check if the RTMRs match the expected values. To learn more about RTMRs and + // their significance, refer to the TDX documentation: + // - https://phala.network/posts/understanding-tdx-attestation-reports-a-developers-guide + // - https://www.kernel.org/doc/Documentation/x86/tdx.rst + compare_hashes( + "rtmr0_report_data", + &report_data.rt_mr0, + &expected_measurements.rtmrs.rtmr0, + )?; + compare_hashes( + "rtmr1_report_data", + &report_data.rt_mr1, + &expected_measurements.rtmrs.rtmr1, + )?; + compare_hashes( + "rtmr2_report_data", + &report_data.rt_mr2, + &expected_measurements.rtmrs.rtmr2, + )?; + compare_hashes( + "mrtd_report_data", + &report_data.mr_td, + &expected_measurements.rtmrs.mrtd, + )?; + + compare_hashes( + "rtmr0_tcb_info", + tcb_info.rtmr0.as_slice(), + &expected_measurements.rtmrs.rtmr0, + )?; + compare_hashes( + "rtmr1_tcb_info", + tcb_info.rtmr1.as_slice(), + &expected_measurements.rtmrs.rtmr1, + )?; + compare_hashes( + "rtmr2_tcb_info", + tcb_info.rtmr2.as_slice(), + &expected_measurements.rtmrs.rtmr2, + )?; + compare_hashes( + "mtrd_tcb_info", + tcb_info.mrtd.as_slice(), + &expected_measurements.rtmrs.mrtd, + ) +} + +/// Verifies local key-provider event digest matches the expected digest. +pub fn verify_key_provider_digest( + tcb_info: &TcbInfo, + expected_digest: &[u8; 48], +) -> Result<(), VerificationError> { + let key_provider_event = tcb_info.get_single_event(KEY_PROVIDER_EVENT)?; + + compare_hashes( + "key_provider", + key_provider_event.digest.as_slice(), + expected_digest, + ) +} + +/// Replays RTMR3 from the event log by hashing all relevant events together and +/// verifies all digests are correct. +fn verify_event_log_rtmr3( + event_log: &[EventLog], + expected_digest: [u8; 48], +) -> Result<(), VerificationError> { + let mut digest = [0u8; 48]; + + let filtered_events = event_log.iter().filter(|e| e.imr == RTMR3_INDEX); + + for event in filtered_events { + // In Dstack, all events measured in RTMR3 are of type DSTACK_EVENT_TYPE + if event.event_type != DSTACK_EVENT_TYPE { + return Err(VerificationError::InvalidEventType(event.event_type)); + } + let mut hasher = Sha384::new(); + hasher.update(digest); + let payload_bytes = match hex::decode(&event.event_payload) { + Ok(bytes) => bytes, + Err(_) => { + return Err(VerificationError::EventDecoding(hex::encode(*event.digest))); + } + }; + let expected_event_digest = event_digest(event.event_type, &event.event, &payload_bytes); + compare_hashes( + "event_digest", + event.digest.as_slice(), + &expected_event_digest, + )?; + + hasher.update(event.digest.as_slice()); + + digest = hasher.finalize().into(); + } + + compare_hashes("event_log", &digest, &expected_digest) +} + +fn validate_app_compose_payload( + expected_event_payload_hex: &str, + app_compose: &str, +) -> Result<(), VerificationError> { + let expected_payload = match hex::decode(expected_event_payload_hex) { + Ok(bytes) => match <[u8; 32]>::try_from(bytes.as_slice()) { + Ok(expected_bytes) => expected_bytes, + Err(_) => { + return Err(VerificationError::AppComposeEventPayloadWrongSize( + bytes.len(), + )); + } + }, + Err(_) => { + return Err(VerificationError::AppComposeEventPayloadNotHex( + expected_event_payload_hex.to_string(), + )); + } + }; + + let app_compose_hash: [u8; 32] = Sha256::digest(app_compose.as_bytes()).into(); + + compare_hashes("app_compose_payload", &app_compose_hash, &expected_payload) +} + +/// Validates app compose configuration against expected security requirements. +pub fn validate_app_compose_config(app_compose: &AppCompose) -> bool { + app_compose.manifest_version == 2 + && app_compose.runner == "docker-compose" + && !app_compose.kms_enabled + && app_compose.gateway_enabled == Some(false) + && app_compose.public_logs + && app_compose.public_sysinfo + && app_compose.local_key_provider_enabled + && app_compose.allowed_envs.is_empty() + && app_compose.no_instance_id + && app_compose.pre_launch_script.is_none() +} + +// Implementation matches Dstack's: +// https://github.com/Dstack-TEE/dstack/blob/cfa4cc4e8a4f525d537883b1a0ba5d9fbfd87f1e/cc-eventlog/src/lib.rs#L54 +fn event_digest(event_type: u32, event: &str, payload: &[u8]) -> [u8; 48] { + let mut hasher = Sha384::new(); + hasher.update(event_type.to_ne_bytes()); + hasher.update(b":"); + hasher.update(event.as_bytes()); + hasher.update(b":"); + hasher.update(payload); + hasher.finalize().into() +} + +pub fn compare_hashes( + name: &'static str, + found: &[u8], + expected: &[u8], +) -> Result<(), VerificationError> { + (found == expected).or_err(|| VerificationError::WrongHash { + name, + found: hex::encode(found), + expected: hex::encode(expected), + }) +} + +pub fn compare_hex_hashes( + name: &'static str, + found: S, + expected: S, +) -> Result<(), VerificationError> { + (found == expected).or_err(|| VerificationError::WrongHash { + name, + found: found.to_string(), + expected: expected.to_string(), + }) +} + +pub trait OrErr { + fn or_err(self, err: impl FnOnce() -> Error) -> Result<(), Error>; +} + +impl OrErr for bool { + fn or_err(self, err: impl FnOnce() -> Error) -> Result<(), Error> { + self.then_some(()).ok_or_else(err) + } +} + +pub trait GetSingleEvent { + fn get_single_event(&self, event_name: &'static str) -> Result<&EventLog, VerificationError>; +} + +impl GetSingleEvent for TcbInfo { + fn get_single_event(&self, event_name: &'static str) -> Result<&EventLog, VerificationError> { + let mut events = self + .event_log + .iter() + .filter(|event| event.event == event_name && event.imr == RTMR3_INDEX); + + let Some(event) = events.next() else { + return Err(VerificationError::MissingEvent(event_name)); + }; + + if events.next().is_some() { + Err(VerificationError::DuplicateEvent(event_name)) + } else { + Ok(event) + } + } +} + +#[cfg(test)] +#[expect(non_snake_case)] +mod tests { + use super::*; + use alloc::{string::ToString, vec::Vec}; + + #[test] + fn validate_app_compose_config__succeeds_on_valid_app_compose() { + // Given + let app_compose = valid_app_compose(); + // When + let result = validate_app_compose_config(&app_compose); + + // Then + assert!(result) + } + + #[test] + fn validate_app_compose_config__allows_insecure_time() { + // Given + let app_compose = AppCompose { + secure_time: Some(false), + ..valid_app_compose() + }; + // When + let result = validate_app_compose_config(&app_compose); + + // Then + assert!(result) + } + + fn valid_app_compose() -> AppCompose { + AppCompose { + manifest_version: 2, + name: "".to_string(), + runner: "docker-compose".to_string(), + docker_compose_file: "".to_string().into(), + kms_enabled: false, + tproxy_enabled: None, + gateway_enabled: Some(false), + public_logs: true, + public_sysinfo: true, + local_key_provider_enabled: true, + key_provider_id: None, + allowed_envs: Vec::new(), + no_instance_id: true, + secure_time: None, + pre_launch_script: None, + } + } +} diff --git a/crates/attestation/Cargo.toml b/crates/attestation/Cargo.toml index 5fc292a911..7af0a4e191 100644 --- a/crates/attestation/Cargo.toml +++ b/crates/attestation/Cargo.toml @@ -5,32 +5,26 @@ license = { workspace = true } edition = { workspace = true } [features] -borsh-schema = ["borsh/unstable__schema"] -dstack-conversions = ["dep:dstack-sdk-types"] +borsh-schema = ["borsh/unstable__schema", "attestation-types/borsh-schema"] +dstack-conversions = ["attestation-types/dstack-conversions"] test-utils = [] [dependencies] +attestation-types = { workspace = true } borsh = { workspace = true } dcap-qvl = { workspace = true } derive_more = { workspace = true } -dstack-sdk-types = { workspace = true, optional = true } hex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -serde_with = { workspace = true } -sha2 = { workspace = true } +tee-verifier-interface = { workspace = true } thiserror = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } dstack-sdk-types = { workspace = true } -rstest = { workspace = true } test-utils = { workspace = true } -[[test]] -name = "app_compose" -required-features = ["dstack-conversions"] - [[test]] name = "collateral" required-features = ["test-utils"] diff --git a/crates/attestation/src/attestation.rs b/crates/attestation/src/attestation.rs index adc2f36180..1667a26613 100644 --- a/crates/attestation/src/attestation.rs +++ b/crates/attestation/src/attestation.rs @@ -1,11 +1,15 @@ -use crate::{ - app_compose::AppCompose, - collateral::Collateral, - measurements::{ExpectedMeasurements, MeasurementsError}, - quote::QuoteBytes, - report_data::ReportData, - tcb_info::{EventLog, TcbInfo}, -}; +//! Local off-chain TEE attestation verification. +//! +//! Carries the `DstackAttestation` struct and its [`DstackAttestation::verify`] +//! method, which is the *only* place the heavy `dcap_qvl::verify::verify` +//! cryptographic call is made. After that call, the parsed report is +//! converted to the [`tee_verifier_interface::VerifiedReport`] mirror and +//! the post-DCAP checks are run via the free functions in +//! [`attestation_types::verify_post_dcap`] — same code path the +//! `tee-verifier` contract uses on its callback side. +//! +//! Consumers that don't need to run `dcap-qvl` locally should depend on +//! `attestation-types` directly, not this crate. use alloc::{ format, @@ -13,22 +17,25 @@ use alloc::{ }; use borsh::{BorshDeserialize, BorshSerialize}; use core::fmt; -use dcap_qvl::verify::VerifiedReport; use derive_more::Constructor; use serde::{Deserialize, Serialize}; -use sha2::{Digest as _, Sha256, Sha384}; - -/// Expected TCB status for a successfully verified TEE quote. -const EXPECTED_QUOTE_STATUS: &str = "UpToDate"; -// DSTACK_EVENT_TYPE is defined in https://github.com/Dstack-TEE/dstack/blob/cfa4cc4e8a4f525d537883b1a0ba5d9fbfd87f1e/tdx-attest/src/lib.rs#L28 -// It is the same for all events -const DSTACK_EVENT_TYPE: u32 = 134217729; +use attestation_types::{ + measurements::ExpectedMeasurements, + report_data::ReportData, + tcb_info::TcbInfo, + verify_post_dcap::{ + verify_any_measurements, verify_app_compose, verify_report_data, verify_rtmr3, + verify_tcb_status, + }, +}; -const COMPOSE_HASH_EVENT: &str = "compose-hash"; -pub(crate) const KEY_PROVIDER_EVENT: &str = "key-provider"; +// Re-export the post-DCAP helper traits and the error type at the historical +// `attestation::attestation::*` paths so existing consumers (e.g. +// `mpc-attestation`) keep working without import-path churn. +pub use attestation_types::verify_post_dcap::{GetSingleEvent, OrErr, VerificationError}; -const RTMR3_INDEX: u32 = 3; +use crate::{collateral::Collateral, quote::QuoteBytes}; #[derive(Clone, Constructor, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] pub struct DstackAttestation { @@ -37,57 +44,6 @@ pub struct DstackAttestation { pub tcb_info: TcbInfo, } -#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] -pub enum VerificationError { - #[error("could not parse embedded measurements: {0}")] - EmbeddedMeasurementsParsing(MeasurementsError), - #[error("dcap verification failed: {0}")] - DcapVerification(String), - #[error("verification report is not TD10")] - ReportNotTd10, - #[error("TCB status `{0}` is not up to date")] - TcbStatusNotUpToDate(String), - #[error("ouststanding advisories reported: {0}")] - NonEmptyAdvisoryIds(String), - #[error("wrong {name} hash (found {found} expected {expected})")] - WrongHash { - name: &'static str, - found: String, - expected: String, - }, - #[error("invalid event type {0}")] - InvalidEventType(u32), - #[error("failed to decode event digest `{0}`")] - EventDecoding(String), - #[error("failed to parse app compose JSON: {0}")] - AppComposeParsing(String), - #[error("no {0} event in event log")] - MissingEvent(&'static str), - #[error("duplicate {0} events in event log")] - DuplicateEvent(&'static str), - #[error("invalid app compose config: `{0}`")] - InvalidAppComposeConfig(String), - #[error("app-compose event payload had an unexpected size of {0}")] - AppComposeEventPayloadWrongSize(usize), - #[error("app-compose event payload `{0}` is not a hex string")] - AppComposeEventPayloadNotHex(String), - #[error( - "the attestation certificate with timestap {attestation_time} has expired since {expiry_time}" - )] - ExpiredCertificate { - attestation_time: u64, - expiry_time: u64, - }, - #[error("the mock attestation is invalid per definition")] - InvalidMockAttestation, - #[error("the allowed measurements list is empty")] - EmptyMeasurementsList, - #[error("the attestation's measurements are not in the allowed set")] - MeasurementsNotAllowed, - #[error("custom error: `{0}`")] - Custom(String), -} - impl fmt::Debug for DstackAttestation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { const MAX_BYTES: usize = 2048; @@ -114,13 +70,11 @@ impl fmt::Debug for DstackAttestation { } impl DstackAttestation { - /// Checks whether this attestation is valid - /// with respect to expected values of: - /// - report_data: must be measured correctly in RTMR3 - /// - timestamp_seconds: current UNIX time in seconds - /// - accepted_measurements: set of accepted RTMRs and key-provider event digest. - /// If any element in the set is valid, the function accepts the attestation as - /// valid. + /// Checks whether this attestation is valid with respect to expected values of: + /// - `expected_report_data`: must be measured correctly in RTMR3 + /// - `timestamp_seconds`: current UNIX time in seconds + /// - `accepted_measurements`: set of accepted RTMRs and key-provider event digest. + /// If any element in the set is valid, the function accepts the attestation as valid. /// /// On success, returns the matched measurements. pub fn verify( @@ -133,382 +87,124 @@ impl DstackAttestation { dcap_qvl::verify::verify(&self.quote, &self.collateral, timestamp_seconds) .map_err(|e| VerificationError::DcapVerification(e.to_string()))?; - let report_data = verification_result + let verified_report = to_mirror_verified_report(verification_result); + + let report_data = verified_report .report .as_td10() .ok_or(VerificationError::ReportNotTd10)?; - // Verify all attestation components - self.verify_tcb_status(&verification_result)?; - self.verify_report_data(&expected_report_data, report_data)?; - - self.verify_rtmr3(report_data, &self.tcb_info)?; - self.verify_app_compose(&self.tcb_info)?; - - self.verify_any_measurements(report_data, &self.tcb_info, accepted_measurements) - } - - /// Replays RTMR3 from the event log by hashing all relevant events together and verifies all - /// digests are correct - fn verify_event_log_rtmr3( - event_log: &[EventLog], - expected_digest: [u8; 48], - ) -> Result<(), VerificationError> { - let mut digest = [0u8; 48]; - - let filtered_events = event_log.iter().filter(|e| e.imr == RTMR3_INDEX); - - for event in filtered_events { - // In Dstack, all events measured in RTMR3 are of type DSTACK_EVENT_TYPE - if event.event_type != DSTACK_EVENT_TYPE { - return Err(VerificationError::InvalidEventType(event.event_type)); - } - let mut hasher = Sha384::new(); - hasher.update(digest); - let payload_bytes = match hex::decode(&event.event_payload) { - Ok(bytes) => bytes, - Err(_) => { - return Err(VerificationError::EventDecoding(hex::encode(*event.digest))); - } - }; - let expected_event_digest = - Self::event_digest(event.event_type, &event.event, &payload_bytes); - compare_hashes( - "event_digest", - event.digest.as_slice(), - &expected_event_digest, - )?; - - hasher.update(event.digest.as_slice()); - - digest = hasher.finalize().into(); - } - - compare_hashes("event_log", &digest, &expected_digest) - } - - fn validate_app_compose_payload( - expected_event_payload_hex: &str, - app_compose: &str, - ) -> Result<(), VerificationError> { - let expected_payload = match hex::decode(expected_event_payload_hex) { - Ok(bytes) => match <[u8; 32]>::try_from(bytes.as_slice()) { - Ok(expected_bytes) => expected_bytes, - Err(_) => { - return Err(VerificationError::AppComposeEventPayloadWrongSize( - bytes.len(), - )); - } - }, - Err(_) => { - return Err(VerificationError::AppComposeEventPayloadNotHex( - expected_event_payload_hex.to_string(), - )); - } - }; - - let app_compose_hash: [u8; 32] = Sha256::digest(app_compose.as_bytes()).into(); + verify_tcb_status(&verified_report)?; + verify_report_data(&expected_report_data, report_data)?; + verify_rtmr3(report_data, &self.tcb_info)?; + verify_app_compose(&self.tcb_info)?; - compare_hashes("app_compose_payload", &app_compose_hash, &expected_payload) - } - - /// Verifies TCB status and security advisories. - fn verify_tcb_status( - &self, - verification_result: &VerifiedReport, - ) -> Result<(), VerificationError> { - // The "UpToDate" TCB status indicates that the measured platform components (CPU - // microcode, firmware, etc.) match the latest known good values published by Intel - // and do not require any updates or mitigations. - let status_is_up_to_date = verification_result.status == EXPECTED_QUOTE_STATUS; - - // Advisory IDs indicate known security vulnerabilities or issues with the TEE. - // For a quote to be considered secure, there should be no outstanding advisories. - let no_security_advisories = verification_result.advisory_ids.is_empty(); - - status_is_up_to_date.or_err(|| { - VerificationError::TcbStatusNotUpToDate(verification_result.status.clone()) - })?; - - no_security_advisories.or_err(|| { - VerificationError::NonEmptyAdvisoryIds(verification_result.advisory_ids.join(", ")) - })?; - - Ok(()) + verify_any_measurements(report_data, &self.tcb_info, accepted_measurements) } +} - /// Verifies report data matches expected values. - fn verify_report_data( - &self, - expected: &ReportData, - actual: &dcap_qvl::quote::TDReport10, - ) -> Result<(), VerificationError> { - // Check if sha384(tls_public_key) matches the hash in report_data. This check effectively - // proves that tls_public_key was included in the quote's report_data by an app running - // inside a TDX enclave. - compare_hashes("report_data", &actual.report_data, &expected.to_bytes()) +/// Converts `dcap_qvl::verify::VerifiedReport` (serde-only upstream type) into +/// the Borsh-stable [`tee_verifier_interface::VerifiedReport`] mirror that +/// the post-DCAP helpers operate on. +/// +/// Duplicated in `crates/tee-verifier/src/conversions.rs` for the on-chain +/// verifier; kept here as well to avoid pulling the full `attestation` +/// crate into the verifier-contract dep graph. +fn to_mirror_verified_report( + value: dcap_qvl::verify::VerifiedReport, +) -> tee_verifier_interface::VerifiedReport { + tee_verifier_interface::VerifiedReport { + status: value.status, + advisory_ids: value.advisory_ids, + report: to_mirror_report(value.report), + ppid: value.ppid, + qe_status: to_mirror_tcb_status_with_advisory(value.qe_status), + platform_status: to_mirror_tcb_status_with_advisory(value.platform_status), } +} - /// Try to verify static RTMRs and key_provider_digest against multiple expected measurement sets. - /// On success, returns the matched measurements. - fn verify_any_measurements( - &self, - report_data: &dcap_qvl::quote::TDReport10, - tcb_info: &TcbInfo, - accepted_measurements: &[ExpectedMeasurements], - ) -> Result { - for expected in accepted_measurements { - if self - .verify_static_rtmrs(report_data, tcb_info, expected) - .is_ok() - && self - .verify_key_provider_digest(tcb_info, &expected.key_provider_event_digest) - .is_ok() - { - return Ok(*expected); // found a valid match - } +fn to_mirror_report(value: dcap_qvl::quote::Report) -> tee_verifier_interface::Report { + match value { + dcap_qvl::quote::Report::SgxEnclave(r) => { + tee_verifier_interface::Report::SgxEnclave(to_mirror_enclave_report(r)) } - - Err(VerificationError::WrongHash { - name: "expected_measurements", - expected: "one of the embedded TCB info sets (prod or dev)".into(), - found: "none matched".into(), - }) - } - /// Verifies static RTMRs match expected values. - fn verify_static_rtmrs( - &self, - report_data: &dcap_qvl::quote::TDReport10, - tcb_info: &TcbInfo, - expected_measurements: &ExpectedMeasurements, - ) -> Result<(), VerificationError> { - // Check if the RTMRs match the expected values. To learn more about RTMRs and - // their significance, refer to the TDX documentation: - // - https://phala.network/posts/understanding-tdx-attestation-reports-a-developers-guide - // - https://www.kernel.org/doc/Documentation/x86/tdx.rst - compare_hashes( - "rtmr0_report_data", - &report_data.rt_mr0, - &expected_measurements.rtmrs.rtmr0, - )?; - compare_hashes( - "rtmr1_report_data", - &report_data.rt_mr1, - &expected_measurements.rtmrs.rtmr1, - )?; - compare_hashes( - "rtmr2_report_data", - &report_data.rt_mr2, - &expected_measurements.rtmrs.rtmr2, - )?; - compare_hashes( - "mrtd_report_data", - &report_data.mr_td, - &expected_measurements.rtmrs.mrtd, - )?; - - compare_hashes( - "rtmr0_tcb_info", - tcb_info.rtmr0.as_slice(), - &expected_measurements.rtmrs.rtmr0, - )?; - compare_hashes( - "rtmr1_tcb_info", - tcb_info.rtmr1.as_slice(), - &expected_measurements.rtmrs.rtmr1, - )?; - compare_hashes( - "rtmr2_tcb_info", - tcb_info.rtmr2.as_slice(), - &expected_measurements.rtmrs.rtmr2, - )?; - compare_hashes( - "mtrd_tcb_info", - tcb_info.mrtd.as_slice(), - &expected_measurements.rtmrs.mrtd, - ) - } - - /// Verifies RTMR3 by replaying event log. - fn verify_rtmr3( - &self, - report_data: &dcap_qvl::quote::TDReport10, - tcb_info: &TcbInfo, - ) -> Result<(), VerificationError> { - compare_hashes("rtmr3", tcb_info.rtmr3.as_slice(), &report_data.rt_mr3)?; - - Self::verify_event_log_rtmr3(&tcb_info.event_log, report_data.rt_mr3) - } - - /// Verifies app compose configuration and hash. The compose-hash is measured into RTMR3, and - /// since it's (roughly) a hash of the unmeasured docker_compose_file, this is sufficient to - /// prove its validity. - fn verify_app_compose(&self, tcb_info: &TcbInfo) -> Result<(), VerificationError> { - let app_compose: AppCompose = serde_json::from_str(&tcb_info.app_compose) - .map_err(|e| VerificationError::AppComposeParsing(e.to_string()))?; - - Self::validate_app_compose_config(&app_compose).or_err(|| { - VerificationError::InvalidAppComposeConfig(tcb_info.app_compose.to_string()) - })?; - - let app_compose_event = tcb_info.get_single_event(COMPOSE_HASH_EVENT)?; - - compare_hex_hashes( - "app_compose_event_hash", - &app_compose_event.event_payload, - &hex::encode(*tcb_info.compose_hash), - )?; - - Self::validate_app_compose_payload(&app_compose_event.event_payload, &tcb_info.app_compose) - } - - /// Validates app compose configuration against expected security requirements. - fn validate_app_compose_config(app_compose: &AppCompose) -> bool { - app_compose.manifest_version == 2 - && app_compose.runner == "docker-compose" - && !app_compose.kms_enabled - && app_compose.gateway_enabled == Some(false) - && app_compose.public_logs - && app_compose.public_sysinfo - && app_compose.local_key_provider_enabled - && app_compose.allowed_envs.is_empty() - && app_compose.no_instance_id - && app_compose.pre_launch_script.is_none() - } - - /// Verifies local key-provider event digest matches the expected digest. - fn verify_key_provider_digest( - &self, - tcb_info: &TcbInfo, - expected_digest: &[u8; 48], - ) -> Result<(), VerificationError> { - let key_provider_event = tcb_info.get_single_event(KEY_PROVIDER_EVENT)?; - - compare_hashes( - "key_provider", - key_provider_event.digest.as_slice(), - expected_digest, - ) - } - - // Implementation taken to match Dstack's https://github.com/Dstack-TEE/dstack/blob/cfa4cc4e8a4f525d537883b1a0ba5d9fbfd87f1e/cc-eventlog/src/lib.rs#L54 - fn event_digest(event_type: u32, event: &str, payload: &[u8]) -> [u8; 48] { - let mut hasher = Sha384::new(); - hasher.update(event_type.to_ne_bytes()); - hasher.update(b":"); - hasher.update(event.as_bytes()); - hasher.update(b":"); - hasher.update(payload); - hasher.finalize().into() + dcap_qvl::quote::Report::TD10(r) => tee_verifier_interface::Report::TD10(to_mirror_td10(r)), + dcap_qvl::quote::Report::TD15(r) => tee_verifier_interface::Report::TD15(to_mirror_td15(r)), } } -fn compare_hashes( - name: &'static str, - found: &[u8], - expected: &[u8], -) -> Result<(), VerificationError> { - (found == expected).or_err(|| VerificationError::WrongHash { - name, - found: hex::encode(found), - expected: hex::encode(expected), - }) -} - -fn compare_hex_hashes( - name: &'static str, - found: S, - expected: S, -) -> Result<(), VerificationError> { - (found == expected).or_err(|| VerificationError::WrongHash { - name, - found: found.to_string(), - expected: expected.to_string(), - }) -} - -pub trait OrErr { - fn or_err(self, err: impl FnOnce() -> Error) -> Result<(), Error>; +fn to_mirror_td10(value: dcap_qvl::quote::TDReport10) -> tee_verifier_interface::TDReport10 { + tee_verifier_interface::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 OrErr for bool { - fn or_err(self, err: impl FnOnce() -> Error) -> Result<(), Error> { - self.then_some(()).ok_or_else(err) +fn to_mirror_td15(value: dcap_qvl::quote::TDReport15) -> tee_verifier_interface::TDReport15 { + tee_verifier_interface::TDReport15 { + base: to_mirror_td10(value.base), + tee_tcb_svn2: value.tee_tcb_svn2, + mr_service_td: value.mr_service_td, } } -pub trait GetSingleEvent { - fn get_single_event(&self, event_name: &'static str) -> Result<&EventLog, VerificationError>; +fn to_mirror_enclave_report( + value: dcap_qvl::quote::EnclaveReport, +) -> tee_verifier_interface::EnclaveReport { + tee_verifier_interface::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 GetSingleEvent for TcbInfo { - fn get_single_event(&self, event_name: &'static str) -> Result<&EventLog, VerificationError> { - let mut events = self - .event_log - .iter() - .filter(|event| event.event == event_name && event.imr == RTMR3_INDEX); - - let Some(event) = events.next() else { - return Err(VerificationError::MissingEvent(event_name)); - }; - - if events.next().is_some() { - Err(VerificationError::DuplicateEvent(event_name)) - } else { - Ok(event) +fn to_mirror_tcb_status(value: dcap_qvl::tcb_info::TcbStatus) -> tee_verifier_interface::TcbStatus { + match value { + dcap_qvl::tcb_info::TcbStatus::UpToDate => tee_verifier_interface::TcbStatus::UpToDate, + dcap_qvl::tcb_info::TcbStatus::OutOfDateConfigurationNeeded => { + tee_verifier_interface::TcbStatus::OutOfDateConfigurationNeeded + } + dcap_qvl::tcb_info::TcbStatus::OutOfDate => tee_verifier_interface::TcbStatus::OutOfDate, + dcap_qvl::tcb_info::TcbStatus::ConfigurationAndSWHardeningNeeded => { + tee_verifier_interface::TcbStatus::ConfigurationAndSWHardeningNeeded + } + dcap_qvl::tcb_info::TcbStatus::ConfigurationNeeded => { + tee_verifier_interface::TcbStatus::ConfigurationNeeded } + dcap_qvl::tcb_info::TcbStatus::SWHardeningNeeded => { + tee_verifier_interface::TcbStatus::SWHardeningNeeded + } + dcap_qvl::tcb_info::TcbStatus::Revoked => tee_verifier_interface::TcbStatus::Revoked, } } -#[cfg(test)] -#[expect(non_snake_case)] -mod tests { - use super::*; - - use alloc::{string::ToString, vec::Vec}; - - #[test] - fn validate_app_compose_config__succeeds_on_valid_app_compose() { - // Given - let app_compose = valid_app_compose(); - // When - let result = DstackAttestation::validate_app_compose_config(&app_compose); - - // Then - assert!(result) - } - - #[test] - fn validate_app_compose_config__allows_insecure_time() { - // Given - let app_compose = AppCompose { - secure_time: Some(false), - ..valid_app_compose() - }; - // When - let result = DstackAttestation::validate_app_compose_config(&app_compose); - - // Then - assert!(result) - } - - fn valid_app_compose() -> AppCompose { - AppCompose { - manifest_version: 2, - name: "".to_string(), - runner: "docker-compose".to_string(), - docker_compose_file: "".to_string().into(), - kms_enabled: false, - tproxy_enabled: None, - gateway_enabled: Some(false), - public_logs: true, - public_sysinfo: true, - local_key_provider_enabled: true, - key_provider_id: None, - allowed_envs: Vec::new(), - no_instance_id: true, - secure_time: None, - pre_launch_script: None, - } +fn to_mirror_tcb_status_with_advisory( + value: dcap_qvl::tcb_info::TcbStatusWithAdvisory, +) -> tee_verifier_interface::TcbStatusWithAdvisory { + tee_verifier_interface::TcbStatusWithAdvisory { + status: to_mirror_tcb_status(value.status), + advisory_ids: value.advisory_ids, } } diff --git a/crates/attestation/src/lib.rs b/crates/attestation/src/lib.rs index 1bc9d889ba..0b0366ebba 100644 --- a/crates/attestation/src/lib.rs +++ b/crates/attestation/src/lib.rs @@ -2,10 +2,12 @@ extern crate alloc; -pub mod app_compose; pub mod attestation; pub mod collateral; -pub mod measurements; pub mod quote; -pub mod report_data; -pub mod tcb_info; + +// DTOs and post-DCAP helpers live in `attestation-types`. Re-exported here +// so existing consumers can keep using paths like `attestation::tcb_info` +// without churn. The `attestation` crate adds only the `dcap_qvl::verify` +// entry point (`DstackAttestation::verify`) on top. +pub use attestation_types::{app_compose, measurements, report_data, tcb_info, verify_post_dcap}; diff --git a/crates/tee-verifier-interface/Cargo.toml b/crates/tee-verifier-interface/Cargo.toml new file mode 100644 index 0000000000..27de655a76 --- /dev/null +++ b/crates/tee-verifier-interface/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tee-verifier-interface" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = "https://github.com/near/mpc" + +[features] +borsh-schema = ["borsh/unstable__schema"] + +[dependencies] +borsh = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +rstest = { workspace = true } + +[lints] +workspace = true diff --git a/crates/tee-verifier-interface/src/lib.rs b/crates/tee-verifier-interface/src/lib.rs new file mode 100644 index 0000000000..a8c08ca9d7 --- /dev/null +++ b/crates/tee-verifier-interface/src/lib.rs @@ -0,0 +1,331 @@ +//! Borsh DTOs spoken at the `tee-verifier` contract boundary. +//! +//! 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. The crate is `no_std` and has no +//! `dcap-qvl` dependency; the `From` conversions live in +//! the `tee-verifier` contract crate, the only crate that depends on +//! both. + +#![no_std] + +extern crate alloc; + +use alloc::{string::String, vec::Vec}; +use borsh::{BorshDeserialize, BorshSerialize}; +// `BorshSchema` derive expands to `T::declaration().to_string()`, which is +// only in scope under no_std when `alloc::string::ToString` is imported. +#[cfg(feature = "borsh-schema")] +use alloc::string::ToString as _; + +/// Raw bytes of an Intel TDX / SGX quote, as produced by the platform. +/// +/// Borsh-encoded as a length-prefixed byte vector. The verifier expects the +/// same byte layout that `dcap_qvl::verify::verify` expects as its first +/// argument. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct QuoteBytes(pub Vec); + +impl From> for QuoteBytes { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for Vec { + fn from(value: QuoteBytes) -> Self { + value.0 + } +} + +/// Quote collateral, mirroring `dcap_qvl::QuoteCollateralV3`. +/// +/// Field-for-field copy so the wire layout matches the upstream Borsh +/// encoding of `QuoteCollateralV3` when the upstream `borsh` feature is on. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct Collateral { + pub pck_crl_issuer_chain: String, + pub root_ca_crl: Vec, + pub pck_crl: Vec, + pub tcb_info_issuer_chain: String, + pub tcb_info: String, + pub tcb_info_signature: Vec, + pub qe_identity_issuer_chain: String, + pub qe_identity: String, + pub qe_identity_signature: Vec, + pub pck_certificate_chain: Option, +} + +/// Errors returned by `tee-verifier`'s `verify_quote` method. +/// +/// Borsh-stable. Mirrors what `dcap_qvl::verify::verify` would otherwise +/// surface to the caller, plus room for verifier-side failures. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, thiserror::Error)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub enum VerifierError { + /// `dcap_qvl::verify::verify` rejected the quote / collateral. The string + /// is the upstream error rendered with `Display`. + #[error("dcap verification failed: {0}")] + DcapVerification(String), +} + +/// Verified TDX / SGX quote, mirroring `dcap_qvl::verify::VerifiedReport`. +/// +/// All fields match the upstream type one-to-one. Borsh-stable. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct VerifiedReport { + pub status: String, + pub advisory_ids: Vec, + pub report: Report, + pub ppid: Vec, + pub qe_status: TcbStatusWithAdvisory, + pub platform_status: TcbStatusWithAdvisory, +} + +/// Parsed quote report, mirroring `dcap_qvl::quote::Report`. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub enum Report { + SgxEnclave(EnclaveReport), + TD10(TDReport10), + TD15(TDReport15), +} + +impl Report { + /// Mirrors `dcap_qvl::quote::Report::as_td10`: returns the TD10 view of + /// either a `TD10` or a `TD15` report (the latter via its `base` field). + pub fn as_td10(&self) -> Option<&TDReport10> { + match self { + Report::TD10(report) => Some(report), + Report::TD15(report) => Some(&report.base), + Report::SgxEnclave(_) => None, + } + } +} + +/// Mirror of `dcap_qvl::quote::TDReport10`. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct TDReport10 { + pub tee_tcb_svn: [u8; 16], + pub mr_seam: [u8; 48], + pub mr_signer_seam: [u8; 48], + pub seam_attributes: [u8; 8], + pub td_attributes: [u8; 8], + pub xfam: [u8; 8], + pub mr_td: [u8; 48], + pub mr_config_id: [u8; 48], + pub mr_owner: [u8; 48], + pub mr_owner_config: [u8; 48], + pub rt_mr0: [u8; 48], + pub rt_mr1: [u8; 48], + pub rt_mr2: [u8; 48], + pub rt_mr3: [u8; 48], + pub report_data: [u8; 64], +} + +/// Mirror of `dcap_qvl::quote::TDReport15`. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct TDReport15 { + pub base: TDReport10, + pub tee_tcb_svn2: [u8; 16], + pub mr_service_td: [u8; 48], +} + +/// Mirror of `dcap_qvl::quote::EnclaveReport`. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct EnclaveReport { + pub cpu_svn: [u8; 16], + pub misc_select: u32, + pub reserved1: [u8; 28], + pub attributes: [u8; 16], + pub mr_enclave: [u8; 32], + pub reserved2: [u8; 32], + pub mr_signer: [u8; 32], + pub reserved3: [u8; 96], + pub isv_prod_id: u16, + pub isv_svn: u16, + pub reserved4: [u8; 60], + pub report_data: [u8; 64], +} + +/// Mirror of `dcap_qvl::tcb_info::TcbStatus`. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub enum TcbStatus { + UpToDate, + OutOfDateConfigurationNeeded, + OutOfDate, + ConfigurationAndSWHardeningNeeded, + ConfigurationNeeded, + SWHardeningNeeded, + Revoked, +} + +/// Mirror of `dcap_qvl::tcb_info::TcbStatusWithAdvisory`. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[cfg_attr(feature = "borsh-schema", derive(borsh::BorshSchema))] +pub struct TcbStatusWithAdvisory { + pub status: TcbStatus, + pub advisory_ids: Vec, +} + +#[cfg(test)] +#[expect(non_snake_case)] +mod tests { + use super::*; + use alloc::vec; + use rstest::rstest; + + fn sample_td10() -> TDReport10 { + 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 sample_verified_report(report: Report) -> VerifiedReport { + VerifiedReport { + status: String::from("UpToDate"), + advisory_ids: vec![String::from("INTEL-SA-00001")], + report, + ppid: vec![0xAB; 16], + qe_status: TcbStatusWithAdvisory { + status: TcbStatus::UpToDate, + advisory_ids: vec![], + }, + platform_status: TcbStatusWithAdvisory { + status: TcbStatus::ConfigurationNeeded, + advisory_ids: vec![String::from("INTEL-SA-00002")], + }, + } + } + + #[rstest] + #[case::td10(Report::TD10(sample_td10()))] + #[case::td15(Report::TD15(TDReport15 { + base: sample_td10(), + tee_tcb_svn2: [16; 16], + mr_service_td: [17; 48], + }))] + #[case::sgx(Report::SgxEnclave(EnclaveReport { + cpu_svn: [1; 16], + misc_select: 42, + reserved1: [0; 28], + attributes: [2; 16], + mr_enclave: [3; 32], + reserved2: [0; 32], + mr_signer: [4; 32], + reserved3: [0; 96], + isv_prod_id: 1, + isv_svn: 2, + reserved4: [0; 60], + report_data: [5; 64], + }))] + fn verified_report__should_round_trip_borsh(#[case] report: Report) { + // Given + let original = sample_verified_report(report); + + // When + let bytes = borsh::to_vec(&original).expect("Borsh serialization should succeed"); + let decoded: VerifiedReport = + borsh::from_slice(&bytes).expect("Borsh deserialization should succeed"); + + // Then + assert_eq!(original, decoded); + } + + #[test] + fn verifier_error__should_round_trip_borsh() { + // Given + let original = VerifierError::DcapVerification(String::from("bad signature")); + + // When + let bytes = borsh::to_vec(&original).expect("Borsh serialization should succeed"); + let decoded: VerifierError = + borsh::from_slice(&bytes).expect("Borsh deserialization should succeed"); + + // Then + match decoded { + VerifierError::DcapVerification(msg) => assert_eq!(msg, "bad signature"), + } + } + + #[test] + fn report__as_td10__should_unwrap_td15_to_its_base() { + // Given + let base = sample_td10(); + let td15 = Report::TD15(TDReport15 { + base: base.clone(), + tee_tcb_svn2: [0; 16], + mr_service_td: [0; 48], + }); + + // When + let unwrapped = td15.as_td10(); + + // Then + assert_eq!(unwrapped, Some(&base)); + } + + #[test] + fn collateral__should_round_trip_borsh() { + // Given + let original = Collateral { + pck_crl_issuer_chain: String::from("issuer-chain"), + root_ca_crl: vec![1, 2, 3], + pck_crl: vec![4, 5, 6], + tcb_info_issuer_chain: String::from("tcb-issuer"), + tcb_info: String::from("tcb-info-json"), + tcb_info_signature: vec![7, 8], + qe_identity_issuer_chain: String::from("qe-issuer"), + qe_identity: String::from("qe-identity-json"), + qe_identity_signature: vec![9, 10], + pck_certificate_chain: Some(String::from("pck-chain")), + }; + + // When + let bytes = borsh::to_vec(&original).expect("Borsh serialization should succeed"); + let decoded: Collateral = + borsh::from_slice(&bytes).expect("Borsh deserialization should succeed"); + + // Then + assert_eq!(original, decoded); + } + + #[test] + fn quote_bytes__should_round_trip_borsh() { + // Given + let original = QuoteBytes(vec![0xDE, 0xAD, 0xBE, 0xEF]); + + // When + let bytes = borsh::to_vec(&original).expect("Borsh serialization should succeed"); + let decoded: QuoteBytes = + borsh::from_slice(&bytes).expect("Borsh deserialization should succeed"); + + // Then + assert_eq!(original, decoded); + } +} 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); +}