Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"crates/contract-history",
"crates/contract-interface",
"crates/devnet",
"crates/include-measurements",
"crates/mpc-attestation",
"crates/node",
"crates/node-types",
Expand All @@ -31,6 +32,7 @@ license = "MIT"
attestation = { path = "crates/attestation" }
contract-history = { path = "crates/contract-history" }
contract-interface = { path = "crates/contract-interface" }
include-measurements = { path = "crates/include-measurements" }
mpc-attestation = { path = "crates/mpc-attestation" }
mpc-contract = { path = "crates/contract", features = [
"dev-utils",
Expand Down
1 change: 1 addition & 0 deletions crates/attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ pub mod collateral;
pub mod measurements;
pub mod quote;
pub mod report_data;
pub mod tcb_info;

pub use dstack_sdk_types::dstack::{EventLog, TcbInfo};
84 changes: 0 additions & 84 deletions crates/attestation/src/measurements.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use serde_with::{Bytes, serde_as};

use dstack_sdk_types::dstack::{EventLog, TcbInfo as DstackTcbInfo};

use crate::attestation::KEY_PROVIDER_EVENT;

/// Required measurements for TEE attestation verification (a.k.a. RTMRs checks). These values
/// define the trusted baseline that TEE environments must match during verification. They
/// should be updated when the underlying TEE environment changes.
Expand Down Expand Up @@ -45,84 +39,6 @@ pub struct ExpectedMeasurements {
pub key_provider_event_digest: [u8; 48],
}

impl ExpectedMeasurements {
/// Loads expected measurements from the embedded TCB info file for TEE attestation verification.
/// This implementation uses a cached computation to avoid runtime JSON parsing and hex decoding,
/// improving performance especially in smart contract environments where every cycle counts.
///
/// The TCB info contains hex-encoded measurement values that are decoded once and cached for
/// all subsequent calls, ensuring consistent measurements across both production and test environments.
///
/// TODO(#737): Define a process for updating these static RTMRs going forward, since they are already outdated.
/// $ git rev-parse HEAD
/// fbdf2e76fb6bd9142277fdd84809de87d86548ef
///
/// See also: https://github.com/Dstack-TEE/meta-dstack?tab=readme-ov-file#reproducible-build-the-guest-image
/// Load all supported TCB info measurement sets (e.g., production + dev).
pub fn from_embedded_tcb_info(
tcb_info_strings: &[&str],
) -> Result<Vec<Self>, MeasurementsError> {
// Helper closure to parse one TCB info JSON
let parse_tcb_info = |json_str: &str| -> Result<ExpectedMeasurements, MeasurementsError> {
let tcb_info: DstackTcbInfo =
serde_json::from_str(json_str).map_err(|_| MeasurementsError::InvalidTcbInfo)?;

let decode_measurement =
|name: &str, hex_value: &str| -> Result<[u8; 48], MeasurementsError> {
let decoded = hex::decode(hex_value).map_err(|_| {
MeasurementsError::InvalidHexValue(name.into(), hex_value.into())
})?;
let decoded_len = decoded.len();
decoded
.try_into()
.map_err(|_| MeasurementsError::InvalidLength(name.into(), decoded_len))
};

let rtmrs = Measurements {
rtmr0: decode_measurement("rtmr0", &tcb_info.rtmr0)?,
rtmr1: decode_measurement("rtmr1", &tcb_info.rtmr1)?,
rtmr2: decode_measurement("rtmr2", &tcb_info.rtmr2)?,
mrtd: decode_measurement("mrtd", &tcb_info.mrtd)?,
};

let key_provider_event_digest_encoded =
Self::get_key_provider_digest(&tcb_info.event_log)?;
let key_provider_event_digest =
decode_measurement(KEY_PROVIDER_EVENT, key_provider_event_digest_encoded)?;

Ok(ExpectedMeasurements {
rtmrs,
key_provider_event_digest,
})
};

let mut results = vec![];
for s in tcb_info_strings {
results.push(parse_tcb_info(s)?);
}

Ok(results)
}

/// The expected SHA-384 digest for the `key-provider` event, not the event payload.
///
/// Digest format:
/// digest = SHA384( event_type + ":" + "key-provider" + ":"+payload) )
///
/// If the key provider is `local-sgx` then:
/// Payload format: sha256 {"name":"local-sgx", "id": "<mr_enclave of the provider>"}
fn get_key_provider_digest(event_log: &[EventLog]) -> Result<&str, MeasurementsError> {
let key_provider_events: Vec<&EventLog> = event_log
.iter()
.filter(|e| e.event == KEY_PROVIDER_EVENT)
.collect();
if key_provider_events.len() != 1 {
return Err(MeasurementsError::InvalidTcbInfo);
};
Ok(&key_provider_events[0].digest)
}
}

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MeasurementsError {
#[error("no TD10 report")]
Expand Down
120 changes: 120 additions & 0 deletions crates/attestation/src/tcb_info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use serde_with::{FromInto, hex::Hex, serde_as};

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TcbInfo {
pub mrtd: HexBytes<48>,
pub rtmr0: HexBytes<48>,
pub rtmr1: HexBytes<48>,
pub rtmr2: HexBytes<48>,
pub rtmr3: HexBytes<48>,
#[serde_as(as = "FromInto<HexBytesOrEmpty<32>>")]
pub os_image_hash: Option<HexBytes<32>>,
pub compose_hash: HexBytes<32>,
pub device_id: HexBytes<32>,
pub app_compose: String,
pub event_log: Vec<EventLog>,
}

#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventLog {
pub imr: u32,
pub event_type: u32,
#[serde_as(as = "Hex")]
pub digest: [u8; 48],
pub event: String,
pub event_payload: String,
}

#[serde_as]
#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
derive_more::From,
derive_more::AsRef,
derive_more::Deref,
)]
#[serde(transparent)]
pub struct HexBytes<const N: usize>(#[serde_as(as = "Hex")] [u8; N]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note: I was surprised we didn't have this somewhere already. I think we could make use of this type in many more places if we break it out as a shared utility later.


#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum HexBytesOrEmpty<const N: usize> {
#[serde(untagged)]
Some(HexBytes<N>),
#[serde(untagged)]
Empty(HexBytes<0>),
}

impl<const N: usize> From<HexBytesOrEmpty<N>> for Option<HexBytes<N>> {
fn from(value: HexBytesOrEmpty<N>) -> Self {
match value {
HexBytesOrEmpty::Some(hex_bytes) => Some(hex_bytes),
HexBytesOrEmpty::Empty(_) => None,
}
}
}

impl<const N: usize> From<Option<HexBytes<N>>> for HexBytesOrEmpty<N> {
fn from(value: Option<HexBytes<N>>) -> Self {
match value {
Some(hex_bytes) => HexBytesOrEmpty::Some(hex_bytes),
None => HexBytesOrEmpty::Empty(HexBytes([])),
}
}
}

#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
use super::*;
use serde_json;

#[test]
fn TcbInfo__should_deserialize_from_real_test_data() {
// Given
const TCB_INFO_JSON: &str = include_str!("../../test-utils/assets/tcb_info.json");

// When
let tcb_info: TcbInfo = serde_json::from_str(TCB_INFO_JSON).unwrap();

// Then
assert_eq!(
hex::encode(*tcb_info.rtmr0),
"e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46"
)
}

#[test]
fn TcbInfo__should_fail_deserialization_with_invalid_hex_length() {
// Given
let json = r#"{
"mrtd": "invalid_length",
"rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46",
"rtmr1": "a7b523278d4f914ee8df0ec80cd1c3d498cbf1152b0c5eaf65bad9425072874a3fcf891e8b01713d3d9937e3e0d26c15",
"rtmr2": "dbf4924c07f5066f3dc6859844184344306aa3263817153dcaee85af97d23e0c0b96efe0731d8865a8747e51b9e351ac",
"rtmr3": "e0d4a068296ebdfc4c9cbf4777663c65c0da4405b8380f28e344f1fab52490264944ff8ccfde112b85eb1d997785e2ac",
"compose_hash": "3efecc42bdef4cb42fa354e9b84fe00e9d82b5397a739e0b03188ab80d72ed81",
"device_id": "7a82191bd4dedb9d716e3aa422963cf1009f36e3068404a0322feca1ce517dc9",
"app_compose": "test_compose",
"event_log": []
}"#;

// When
let result: Result<TcbInfo, _> = serde_json::from_str(json);

// Then
assert!(result.is_err());
}
}
21 changes: 21 additions & 0 deletions crates/include-measurements/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "include-measurements"
version.workspace = true
edition.workspace = true
license.workspace = true

[lib]
proc-macro = true

[dependencies]
anyhow = "1.0"
attestation = { path = "../attestation" }
hex = "0.4"
proc-macro2 = "1.0"
quote = "1.0"
serde_json = "1.0"
syn = "2.0"

[dev-dependencies]
attestation = { path = "../attestation" }
hex = "0.4"
Loading