diff --git a/Cargo.lock b/Cargo.lock index d809d6faeb..5d6baed0f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,7 +1049,6 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" name = "attestation" version = "3.14.0" dependencies = [ - "assert_matches", "attestation", "borsh", "dcap-qvl", diff --git a/crates/attestation/Cargo.toml b/crates/attestation/Cargo.toml index 1c10387762..6822fc2398 100644 --- a/crates/attestation/Cargo.toml +++ b/crates/attestation/Cargo.toml @@ -27,7 +27,6 @@ tee-verifier-interface = { workspace = true, features = ["serde"] } thiserror = { workspace = true } [dev-dependencies] -assert_matches = { workspace = true } attestation = { path = ".", features = [ "local-verify", "test-utils", @@ -41,9 +40,5 @@ test-utils = { workspace = true } name = "app_compose" required-features = ["dstack-conversions"] -[[test]] -name = "collateral" -required-features = ["test-utils"] - [lints] workspace = true diff --git a/crates/attestation/src/collateral.rs b/crates/attestation/src/collateral.rs index 7e2f5c3a25..502a6f2748 100644 --- a/crates/attestation/src/collateral.rs +++ b/crates/attestation/src/collateral.rs @@ -2,71 +2,4 @@ //! //! [`Collateral`] is re-exported from `tee-verifier-interface`, not redefined, //! so it has a single canonical definition. -//! -//! The `test-utils` JSON parser below lives here, not in the wire crate: -//! `tee-verifier-interface` is Borsh-only on the cross-contract call, so -//! adding `serde_json` + `hex` there would bloat every consumer's WASM. The -//! only place collateral exists as JSON is off-chain test fixtures. pub use tee_verifier_interface::Collateral; - -#[cfg(feature = "test-utils")] -pub use parse::{CollateralError, collateral_from_json, collateral_from_str}; - -#[cfg(feature = "test-utils")] -mod parse { - use super::Collateral; - use alloc::{string::String, vec::Vec}; - use hex::FromHexError; - use serde_json::Value; - use thiserror::Error; - - pub fn collateral_from_json(v: Value) -> Result { - fn get_str(v: &Value, key: &str) -> Result { - v.get(key) - .and_then(Value::as_str) - .map(String::from) - .ok_or_else(|| CollateralError::MissingField(String::from(key))) - } - - fn get_hex(v: &Value, key: &str) -> Result, CollateralError> { - let hex_str = get_str(v, key)?; - hex::decode(hex_str).map_err(|source| CollateralError::HexDecode { - field: String::from(key), - source, - }) - } - - Ok(Collateral { - pck_crl_issuer_chain: get_str(&v, "pck_crl_issuer_chain")?, - root_ca_crl: get_hex(&v, "root_ca_crl")?, - pck_crl: get_hex(&v, "pck_crl")?, - tcb_info_issuer_chain: get_str(&v, "tcb_info_issuer_chain")?, - tcb_info: get_str(&v, "tcb_info")?, - tcb_info_signature: get_hex(&v, "tcb_info_signature")?, - qe_identity_issuer_chain: get_str(&v, "qe_identity_issuer_chain")?, - qe_identity: get_str(&v, "qe_identity")?, - qe_identity_signature: get_hex(&v, "qe_identity_signature")?, - pck_certificate_chain: get_str(&v, "pck_certificate_chain").ok(), - }) - } - - pub fn collateral_from_str(s: &str) -> Result { - let json_value: Value = - serde_json::from_str(s).map_err(|_| CollateralError::InvalidJson)?; - collateral_from_json(json_value) - } - - #[derive(Debug, Error)] - pub enum CollateralError { - #[error("Missing or invalid field: {0}")] - MissingField(String), - #[error("Failed to decode hex field '{field}': {source}")] - HexDecode { - field: String, - #[source] - source: FromHexError, - }, - #[error("Invalid JSON format")] - InvalidJson, - } -} diff --git a/crates/attestation/tests/collateral.rs b/crates/attestation/tests/collateral.rs deleted file mode 100644 index 466ddd68f6..0000000000 --- a/crates/attestation/tests/collateral.rs +++ /dev/null @@ -1,91 +0,0 @@ -use assert_matches::assert_matches; -use attestation::collateral::{CollateralError, collateral_from_json, collateral_from_str}; -use serde_json::json; -use test_utils::attestation::collateral; - -#[test] -fn test_collateral_missing_field() { - let mut json_value = collateral(); - // Remove a required field - json_value.as_object_mut().unwrap().remove("tcb_info"); - - let result = collateral_from_json(json_value); - - assert_matches!(result, Err(CollateralError::MissingField(field)) => { - assert_eq!(field, "tcb_info"); - }); -} - -#[test] -fn test_collateral_invalid_hex() { - let mut json_value = collateral(); - // Set invalid hex value - json_value["tcb_info_signature"] = json!("not_valid_hex"); - - let result = collateral_from_json(json_value); - - assert_matches!(result, Err(CollateralError::HexDecode { field, ..}) => { - assert_eq!(field, "tcb_info_signature"); - }); -} - -#[test] -fn test_collateral_null_field() { - let mut json_value = collateral(); - // Set field to null - json_value["qe_identity"] = json!(null); - - let result = collateral_from_json(json_value); - - assert_matches!(result, Err(CollateralError::MissingField(field)) => { - assert_eq!(field, "qe_identity"); - }); -} - -#[test] -fn test_collateral_wrong_type_field() { - let mut json_value = collateral(); - // Set field to wrong type (number instead of string) - json_value["tcb_info_issuer_chain"] = json!(12345); - - let result = collateral_from_json(json_value); - - assert_matches!(result, Err(CollateralError::MissingField(field)) => { - assert_eq!(field, "tcb_info_issuer_chain"); - }); -} - -#[test] -fn test_hex_signature_lengths() { - let json_value = collateral(); - let collateral = collateral_from_json(json_value).unwrap(); - - // TCB info signature should be 64 hex chars (32 bytes) - assert_eq!(collateral.tcb_info_signature.len(), 64); - // QE identity signature should be 64 hex chars (32 bytes) - assert_eq!(collateral.qe_identity_signature.len(), 64); -} - -#[test] -fn test_collateral_parses_expected_fields() { - let json_value = collateral(); - let collateral = collateral_from_json(json_value).unwrap(); - - assert!(collateral.tcb_info.contains("\"id\":\"TDX\"")); -} - -#[test] -fn test_from_str_valid_json() { - let json_str = serde_json::to_string(&collateral()).unwrap(); - let collateral = collateral_from_str(&json_str).unwrap(); - - assert!(collateral.tcb_info.contains("\"id\":\"TDX\"")); -} - -#[test] -fn test_from_str_invalid_json() { - let invalid_json = "{ invalid json }"; - let result = collateral_from_str(invalid_json); - - assert_matches!(result, Err(CollateralError::InvalidJson)); -} diff --git a/crates/tee-authority/src/tee_authority.rs b/crates/tee-authority/src/tee_authority.rs index 4d609474df..8b6375d130 100644 --- a/crates/tee-authority/src/tee_authority.rs +++ b/crates/tee-authority/src/tee_authority.rs @@ -1117,12 +1117,7 @@ mod tests { /// fresh for the JSON-field tests; the dedicated CRL-staleness tests /// drive their own `now` past the window. fn fixture_pck_crl() -> Vec { - let collateral_json = test_utils::attestation::collateral(); - let hex_str = collateral_json - .get("pck_crl") - .and_then(|v| v.as_str()) - .expect("test fixture has pck_crl"); - hex::decode(hex_str).expect("test fixture pck_crl is valid hex") + test_utils::attestation::collateral().pck_crl } /// Build a [`Collateral`] with synthetic `tcb_info` / `qe_identity` JSON diff --git a/crates/tee-verifier/tests/verify_quote.rs b/crates/tee-verifier/tests/verify_quote.rs index c1ed9a4f4f..d869bb9ee7 100644 --- a/crates/tee-verifier/tests/verify_quote.rs +++ b/crates/tee-verifier/tests/verify_quote.rs @@ -15,33 +15,10 @@ 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, - VerificationResult, VerifiedReport, VerifierError, + QuoteBytes, Report, TDReport10, TcbStatus, TcbStatusWithAdvisory, VerificationResult, + VerifiedReport, VerifierError, }; -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), - } -} +use test_utils::attestation::{VALID_ATTESTATION_TIMESTAMP, collateral, quote}; fn make_quote_bytes() -> QuoteBytes { QuoteBytes(Vec::from(quote())) @@ -66,7 +43,7 @@ fn verify_quote__should_return_verified_td10_report_for_valid_fixture() { set_valid_timestamp_context(); let contract = TeeVerifier::default(); let quote = make_quote_bytes(); - let collateral = make_collateral(); + let collateral = collateral(); // When let result = contract.verify_quote(quote, collateral); @@ -133,7 +110,7 @@ fn verify_quote__should_reject_without_panicking_for_invalid_quote() { set_valid_timestamp_context(); let contract = TeeVerifier::default(); let invalid_quote = QuoteBytes(vec![0u8; 16]); - let collateral = make_collateral(); + let collateral = collateral(); // When let result = contract.verify_quote(invalid_quote, collateral); @@ -155,7 +132,7 @@ fn verify_quote__should_reject_valid_quote_with_mismatched_collateral() { set_valid_timestamp_context(); let contract = TeeVerifier::default(); let quote = make_quote_bytes(); - let mut collateral = make_collateral(); + let mut collateral = collateral(); collateral.tcb_info = String::from("{}"); // When @@ -191,7 +168,7 @@ fn hex_arr(s: &str) -> [u8; N] { #[test] fn verify_quote_args_fixture__should_match_committed_file() { let mut expected = borsh::to_vec(&make_quote_bytes()).unwrap(); - expected.extend(borsh::to_vec(&make_collateral()).unwrap()); + expected.extend(borsh::to_vec(&collateral()).unwrap()); let path = concat!( env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/verify_quote_args.borsh" diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 559baa2a30..acd0eefe0b 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -14,7 +14,7 @@ near-sdk = { workspace = true, features = ["non-contract-usage"] } serde_json = { workspace = true } serde_yaml = { workspace = true } sha2 = { workspace = true } -tee-verifier-interface = { workspace = true } +tee-verifier-interface = { workspace = true, features = ["serde"] } [dev-dependencies] bs58 = { workspace = true } diff --git a/crates/test-utils/assets/README.md b/crates/test-utils/assets/README.md index 323ae2d879..3b23b54604 100644 --- a/crates/test-utils/assets/README.md +++ b/crates/test-utils/assets/README.md @@ -39,7 +39,6 @@ This will regenerate the following files: - `near_p2p_public_key.pub` - `near_account_public_key.pub` - `app_compose.json` -- `collateral.json` - `quote.json` - `tcb_info.json` - `launcher_image_compose.yaml` @@ -48,7 +47,7 @@ This will regenerate the following files: All files will be written into the specified output directory. `public_data.json` is the endpoint response verbatim, so its collateral byte fields are arrays, while -`collateral.json` holds the same bytes hex-encoded for the parser. +`collateral.json` holds the same bytes hex-encoded for the contract DTO. 4. Update `VALID_ATTESTATION_TIMESTAMP` in `crates/test-utils/src/attestation.rs` to a Unix timestamp after the date when the measurements were taken. This ensures that the tests will consider the measurements valid. @@ -87,16 +86,17 @@ All files will be written into the specified output directory. > will be managed entirely through on-chain voting (`vote_add_os_measurement`), and these > files will no longer need to be kept in sync with the deployed OS image. -8. Regenerate the verifier's borsh argument fixture and refresh the report values - the verifier test hardcodes (`mr_config_id`, `rt_mr3`, `report_data` change with - every new node): +8. Regenerate the derived fixtures — `collateral.json` (the captured collateral, hex-encoded for the + contract DTO) and the verifier's borsh arguments — then refresh the report values the verifier test + hardcodes (`mr_config_id`, `rt_mr3`, `report_data` change with every new node): ```shell + UPDATE_FIXTURES=1 cargo test -p test-utils collateral_fixture UPDATE_FIXTURES=1 cargo test -p tee-verifier --test verify_quote verify_quote_args_fixture cargo test -p tee-verifier --test verify_quote ``` - The second run fails on `verify_quote__should_return_verified_td10_report_for_valid_fixture` + The last run fails on `verify_quote__should_return_verified_td10_report_for_valid_fixture` and prints the values actually produced; copy them into `crates/tee-verifier/tests/verify_quote.rs`. diff --git a/crates/test-utils/assets/create-assets.sh b/crates/test-utils/assets/create-assets.sh index 92a53262ee..b3f71c1db4 100755 --- a/crates/test-utils/assets/create-assets.sh +++ b/crates/test-utils/assets/create-assets.sh @@ -36,19 +36,6 @@ jq -j '.near_signer_public_key' "$INPUT_FILE" > "$OUTPUT_DIR/near_account_public # Extract app_compose.json. We set 4 width indentation, and remove trailing newline, so it matches the original string in tests. printf '%s' "$(jq -r --indent 4 '.tee_participant_info.Dstack.tcb_info.app_compose' "$INPUT_FILE")" > "$OUTPUT_DIR/app_compose.json" -# The endpoint emits the DER/signature fields as byte arrays (serde_bytes) while the fixture parser -# reads them as hex, and the PEM chains keep the NUL terminator from the quote's C strings. Fields -# already in the target form pass through unchanged. -jq -r 'def tohex: - if type == "array" then "0123456789abcdef" as $h - | map($h[(. / 16 | floor):(. / 16 | floor) + 1] + $h[(. % 16):(. % 16) + 1]) | join("") - else . end; - def strip_nul: if type == "string" then split("\u0000")[0] else . end; - .tee_participant_info.Dstack.collateral - | (.root_ca_crl, .pck_crl, .tcb_info_signature, .qe_identity_signature) |= tohex - | (.pck_crl_issuer_chain, .tcb_info_issuer_chain, .qe_identity_issuer_chain, - .pck_certificate_chain) |= strip_nul' "$INPUT_FILE" > "$OUTPUT_DIR/collateral.json" - # Extract quote jq -c '.tee_participant_info.Dstack.quote' "$INPUT_FILE" > "$OUTPUT_DIR/quote.json" diff --git a/crates/test-utils/src/attestation.rs b/crates/test-utils/src/attestation.rs index 4ddb6abc1a..100e9eeec0 100644 --- a/crates/test-utils/src/attestation.rs +++ b/crates/test-utils/src/attestation.rs @@ -7,10 +7,11 @@ use mpc_primitives::hash::{LauncherDockerComposeHash, LauncherImageHash, NodeIma use near_mpc_contract_interface::types::HexVec; use serde_json::Value; use sha2::{Digest, Sha256}; -use tee_verifier_interface::VerifiedReport; +use tee_verifier_interface::{Collateral, VerifiedReport}; pub const TEST_TCB_INFO_STRING: &str = include_str!("../assets/tcb_info.json"); pub const TEST_COLLATERAL_STRING: &str = include_str!("../assets/collateral.json"); +pub const TEST_PUBLIC_DATA_STRING: &str = include_str!("../assets/public_data.json"); pub const TEST_APP_COMPOSE_STRING: &str = include_str!("../assets/app_compose.json"); pub const TEST_APP_COMPOSE_WITH_SERVICES_STRING: &str = include_str!("../assets/app_compose_with_services.json"); @@ -58,10 +59,27 @@ pub fn image_digest() -> NodeImageHash { NodeImageHash::from(digest) } -pub fn collateral() -> Value { - TEST_COLLATERAL_STRING +/// The captured collateral, with the NUL terminators the quote's C strings carry stripped. +fn captured_collateral() -> Value { + let public_data: Value = TEST_PUBLIC_DATA_STRING .parse() - .expect("Quote collateral file is a valid json.") + .expect("public_data.json is valid json"); + let mut collateral = public_data["tee_participant_info"]["Dstack"]["collateral"].clone(); + for field in collateral + .as_object_mut() + .expect("collateral is a json object") + .values_mut() + { + if let Some(text) = field.as_str() { + *field = Value::from(text.trim_end_matches('\0')); + } + } + collateral +} + +/// Collateral in the shape `/public_data` returns, so plain serde is enough to read it. +pub fn collateral() -> Collateral { + serde_json::from_value(captured_collateral()).expect("captured collateral deserializes") } pub fn quote() -> QuoteBytes { @@ -104,11 +122,8 @@ pub fn account_secret_key() -> &'static str { } pub fn mock_dstack_attestation_inner() -> DstackAttestation { - let quote = quote(); - let collateral = mpc_attestation::collateral::collateral_from_str(TEST_COLLATERAL_STRING) - .expect("collateral.json is valid collateral"); let tcb_info: TcbInfo = serde_json::from_str(TEST_TCB_INFO_STRING).unwrap(); - DstackAttestation::new(quote, collateral, tcb_info) + DstackAttestation::new(quote(), collateral(), tcb_info) } pub fn mock_dstack_attestation() -> Attestation { @@ -155,6 +170,43 @@ mod tests { near_p2p_tls_key(); } + /// `collateral.json` is the captured collateral with its byte fields hex-encoded, which is the + /// shape the contract DTO reads. Regenerate with: + /// + /// UPDATE_FIXTURES=1 cargo test -p test-utils collateral_fixture + #[test] + fn collateral_fixture__should_match_the_captured_public_data() { + // Given + const BYTE_FIELDS: [&str; 4] = [ + "root_ca_crl", + "pck_crl", + "tcb_info_signature", + "qe_identity_signature", + ]; + let mut collateral = captured_collateral(); + for name in BYTE_FIELDS { + let bytes: Vec = serde_json::from_value(collateral[name].clone()) + .unwrap_or_else(|_| panic!("{name} is a byte array")); + collateral[name] = Value::from(hex::encode(bytes)); + } + + // When + let expected = format!( + "{}\n", + serde_json::to_string_pretty(&collateral).expect("collateral serializes") + ); + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/collateral.json"); + if std::env::var_os("UPDATE_FIXTURES").is_some() { + std::fs::write(path, &expected).expect("collateral.json is writable"); + } + + // Then + assert_eq!( + expected, TEST_COLLATERAL_STRING, + "collateral.json is stale; regenerate with UPDATE_FIXTURES=1" + ); + } + #[test] fn account_secret_key__should_pair_with_account_public_key() { // Given: a NEAR ed25519 secret key is base58 of `seed || public_key`.