Skip to content

Commit e2518aa

Browse files
committed
feat: Use procedural macro to include expected measurements at compile time
1 parent a9db169 commit e2518aa

File tree

10 files changed

+423
-94
lines changed

10 files changed

+423
-94
lines changed

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"crates/contract-history",
1010
"crates/contract-interface",
1111
"crates/devnet",
12+
"crates/include-measurements",
1213
"crates/mpc-attestation",
1314
"crates/node",
1415
"crates/node-types",
@@ -31,6 +32,7 @@ license = "MIT"
3132
attestation = { path = "crates/attestation" }
3233
contract-history = { path = "crates/contract-history" }
3334
contract-interface = { path = "crates/contract-interface" }
35+
include-measurements = { path = "crates/include-measurements" }
3436
mpc-attestation = { path = "crates/mpc-attestation" }
3537
mpc-contract = { path = "crates/contract", features = [
3638
"dev-utils",

crates/attestation/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ pub mod collateral;
99
pub mod measurements;
1010
pub mod quote;
1111
pub mod report_data;
12+
pub mod tcb_info;
1213

1314
pub use dstack_sdk_types::dstack::{EventLog, TcbInfo};

crates/attestation/src/measurements.rs

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
use alloc::string::String;
2-
use alloc::vec;
3-
use alloc::vec::Vec;
42
use borsh::{BorshDeserialize, BorshSerialize};
53
use serde::{Deserialize, Serialize};
64
use serde_with::{Bytes, serde_as};
75

8-
use dstack_sdk_types::dstack::{EventLog, TcbInfo as DstackTcbInfo};
9-
10-
use crate::attestation::KEY_PROVIDER_EVENT;
11-
126
/// Required measurements for TEE attestation verification (a.k.a. RTMRs checks). These values
137
/// define the trusted baseline that TEE environments must match during verification. They
148
/// should be updated when the underlying TEE environment changes.
@@ -45,84 +39,6 @@ pub struct ExpectedMeasurements {
4539
pub key_provider_event_digest: [u8; 48],
4640
}
4741

48-
impl ExpectedMeasurements {
49-
/// Loads expected measurements from the embedded TCB info file for TEE attestation verification.
50-
/// This implementation uses a cached computation to avoid runtime JSON parsing and hex decoding,
51-
/// improving performance especially in smart contract environments where every cycle counts.
52-
///
53-
/// The TCB info contains hex-encoded measurement values that are decoded once and cached for
54-
/// all subsequent calls, ensuring consistent measurements across both production and test environments.
55-
///
56-
/// TODO(#737): Define a process for updating these static RTMRs going forward, since they are already outdated.
57-
/// $ git rev-parse HEAD
58-
/// fbdf2e76fb6bd9142277fdd84809de87d86548ef
59-
///
60-
/// See also: https://github.com/Dstack-TEE/meta-dstack?tab=readme-ov-file#reproducible-build-the-guest-image
61-
/// Load all supported TCB info measurement sets (e.g., production + dev).
62-
pub fn from_embedded_tcb_info(
63-
tcb_info_strings: &[&str],
64-
) -> Result<Vec<Self>, MeasurementsError> {
65-
// Helper closure to parse one TCB info JSON
66-
let parse_tcb_info = |json_str: &str| -> Result<ExpectedMeasurements, MeasurementsError> {
67-
let tcb_info: DstackTcbInfo =
68-
serde_json::from_str(json_str).map_err(|_| MeasurementsError::InvalidTcbInfo)?;
69-
70-
let decode_measurement =
71-
|name: &str, hex_value: &str| -> Result<[u8; 48], MeasurementsError> {
72-
let decoded = hex::decode(hex_value).map_err(|_| {
73-
MeasurementsError::InvalidHexValue(name.into(), hex_value.into())
74-
})?;
75-
let decoded_len = decoded.len();
76-
decoded
77-
.try_into()
78-
.map_err(|_| MeasurementsError::InvalidLength(name.into(), decoded_len))
79-
};
80-
81-
let rtmrs = Measurements {
82-
rtmr0: decode_measurement("rtmr0", &tcb_info.rtmr0)?,
83-
rtmr1: decode_measurement("rtmr1", &tcb_info.rtmr1)?,
84-
rtmr2: decode_measurement("rtmr2", &tcb_info.rtmr2)?,
85-
mrtd: decode_measurement("mrtd", &tcb_info.mrtd)?,
86-
};
87-
88-
let key_provider_event_digest_encoded =
89-
Self::get_key_provider_digest(&tcb_info.event_log)?;
90-
let key_provider_event_digest =
91-
decode_measurement(KEY_PROVIDER_EVENT, key_provider_event_digest_encoded)?;
92-
93-
Ok(ExpectedMeasurements {
94-
rtmrs,
95-
key_provider_event_digest,
96-
})
97-
};
98-
99-
let mut results = vec![];
100-
for s in tcb_info_strings {
101-
results.push(parse_tcb_info(s)?);
102-
}
103-
104-
Ok(results)
105-
}
106-
107-
/// The expected SHA-384 digest for the `key-provider` event, not the event payload.
108-
///
109-
/// Digest format:
110-
/// digest = SHA384( event_type + ":" + "key-provider" + ":"+payload) )
111-
///
112-
/// If the key provider is `local-sgx` then:
113-
/// Payload format: sha256 {"name":"local-sgx", "id": "<mr_enclave of the provider>"}
114-
fn get_key_provider_digest(event_log: &[EventLog]) -> Result<&str, MeasurementsError> {
115-
let key_provider_events: Vec<&EventLog> = event_log
116-
.iter()
117-
.filter(|e| e.event == KEY_PROVIDER_EVENT)
118-
.collect();
119-
if key_provider_events.len() != 1 {
120-
return Err(MeasurementsError::InvalidTcbInfo);
121-
};
122-
Ok(&key_provider_events[0].digest)
123-
}
124-
}
125-
12642
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
12743
pub enum MeasurementsError {
12844
#[error("no TD10 report")]

crates/attestation/src/tcb_info.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use alloc::string::String;
2+
use alloc::vec::Vec;
3+
use serde::{Deserialize, Serialize};
4+
use serde_with::{FromInto, hex::Hex, serde_as};
5+
6+
#[serde_as]
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
pub struct TcbInfo {
9+
pub mrtd: HexBytes<48>,
10+
pub rtmr0: HexBytes<48>,
11+
pub rtmr1: HexBytes<48>,
12+
pub rtmr2: HexBytes<48>,
13+
pub rtmr3: HexBytes<48>,
14+
#[serde_as(as = "FromInto<HexBytesOrEmpty<32>>")]
15+
pub os_image_hash: Option<HexBytes<32>>,
16+
pub compose_hash: HexBytes<32>,
17+
pub device_id: HexBytes<32>,
18+
pub app_compose: String,
19+
pub event_log: Vec<EventLog>,
20+
}
21+
22+
#[serde_as]
23+
#[derive(Debug, Clone, Serialize, Deserialize)]
24+
pub struct EventLog {
25+
pub imr: u32,
26+
pub event_type: u32,
27+
#[serde_as(as = "Hex")]
28+
pub digest: [u8; 48],
29+
pub event: String,
30+
pub event_payload: String,
31+
}
32+
33+
#[serde_as]
34+
#[derive(
35+
Debug,
36+
Clone,
37+
PartialEq,
38+
Eq,
39+
PartialOrd,
40+
Ord,
41+
Hash,
42+
Serialize,
43+
Deserialize,
44+
derive_more::From,
45+
derive_more::AsRef,
46+
derive_more::Deref,
47+
)]
48+
#[serde(transparent)]
49+
pub struct HexBytes<const N: usize>(#[serde_as(as = "Hex")] [u8; N]);
50+
51+
#[serde_as]
52+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
53+
pub enum HexBytesOrEmpty<const N: usize> {
54+
#[serde(untagged)]
55+
Some(HexBytes<N>),
56+
#[serde(untagged)]
57+
Empty(HexBytes<0>),
58+
}
59+
60+
impl<const N: usize> From<HexBytesOrEmpty<N>> for Option<HexBytes<N>> {
61+
fn from(value: HexBytesOrEmpty<N>) -> Self {
62+
match value {
63+
HexBytesOrEmpty::Some(hex_bytes) => Some(hex_bytes),
64+
HexBytesOrEmpty::Empty(_) => None,
65+
}
66+
}
67+
}
68+
69+
impl<const N: usize> From<Option<HexBytes<N>>> for HexBytesOrEmpty<N> {
70+
fn from(value: Option<HexBytes<N>>) -> Self {
71+
match value {
72+
Some(hex_bytes) => HexBytesOrEmpty::Some(hex_bytes),
73+
None => HexBytesOrEmpty::Empty(HexBytes([])),
74+
}
75+
}
76+
}
77+
78+
#[cfg(test)]
79+
#[allow(non_snake_case)]
80+
mod tests {
81+
use super::*;
82+
use serde_json;
83+
84+
#[test]
85+
fn TcbInfo__should_deserialize_from_real_test_data() {
86+
// Given
87+
const TCB_INFO_JSON: &str = include_str!("../../test-utils/assets/tcb_info.json");
88+
89+
// When
90+
let tcb_info: TcbInfo = serde_json::from_str(TCB_INFO_JSON).unwrap();
91+
92+
// Then
93+
assert_eq!(
94+
hex::encode(*tcb_info.rtmr0),
95+
"e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46"
96+
)
97+
}
98+
99+
#[test]
100+
fn TcbInfo__should_fail_deserialization_with_invalid_hex_length() {
101+
// Given
102+
let json = r#"{
103+
"mrtd": "invalid_length",
104+
"rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46",
105+
"rtmr1": "a7b523278d4f914ee8df0ec80cd1c3d498cbf1152b0c5eaf65bad9425072874a3fcf891e8b01713d3d9937e3e0d26c15",
106+
"rtmr2": "dbf4924c07f5066f3dc6859844184344306aa3263817153dcaee85af97d23e0c0b96efe0731d8865a8747e51b9e351ac",
107+
"rtmr3": "e0d4a068296ebdfc4c9cbf4777663c65c0da4405b8380f28e344f1fab52490264944ff8ccfde112b85eb1d997785e2ac",
108+
"compose_hash": "3efecc42bdef4cb42fa354e9b84fe00e9d82b5397a739e0b03188ab80d72ed81",
109+
"device_id": "7a82191bd4dedb9d716e3aa422963cf1009f36e3068404a0322feca1ce517dc9",
110+
"app_compose": "test_compose",
111+
"event_log": []
112+
}"#;
113+
114+
// When
115+
let result: Result<TcbInfo, _> = serde_json::from_str(json);
116+
117+
// Then
118+
assert!(result.is_err());
119+
}
120+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "include-measurements"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
7+
[lib]
8+
proc-macro = true
9+
10+
[dependencies]
11+
anyhow = "1.0"
12+
attestation = { path = "../attestation" }
13+
hex = "0.4"
14+
proc-macro2 = "1.0"
15+
quote = "1.0"
16+
serde_json = "1.0"
17+
syn = "2.0"
18+
19+
[dev-dependencies]
20+
attestation = { path = "../attestation" }
21+
hex = "0.4"

0 commit comments

Comments
 (0)