Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions crates/mpc-attestation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name = "mpc-attestation"
version = { workspace = true }
license = { workspace = true }
edition = { workspace = true }
build = "build.rs"

[dependencies]
attestation = { workspace = true }
Expand All @@ -15,6 +16,11 @@ serde_json = { workspace = true }
sha2 = { workspace = true }
sha3 = { workspace = true }

[build-dependencies]
hex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

[dev-dependencies]
dcap-qvl = { workspace = true }
rstest = { workspace = true }
Expand Down
96 changes: 96 additions & 0 deletions crates/mpc-attestation/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;

fn main() {
// Location of assets/*.json
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let assets_dir = PathBuf::from(manifest_dir).join("assets");

// Find all tcb_info*.json files (prod, dev, future ones)
let mut measurement_files = Vec::new();
for entry in fs::read_dir(&assets_dir).unwrap() {
Copy link
Contributor

Choose a reason for hiding this comment

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

@gilcu3 is it safe to have unwrap()s in build.rs? 🤔 Since it runs at compile time, I assume it’s fine, but just double-checking.

Copy link
Contributor

Choose a reason for hiding this comment

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

I assumed it is fine as well, for that reason

let entry = entry.unwrap();
let path = entry.path();

if path.extension().and_then(|x| x.to_str()) == Some("json")
&& path.file_name().unwrap().to_str().unwrap().starts_with("tcb_info")
{
measurement_files.push(path);
}
}

// Output file
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let out_file = out_dir.join("measurements_generated.rs");
let mut f = File::create(out_file).unwrap();

// Write prelude
writeln!(
f,
"// AUTO-GENERATED FILE. DO NOT EDIT.\n\
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The module path in the generated code assumes the structure attestation::measurements::*. Consider adding a comment or documentation explaining that this generated code must be included in a module that has access to these imports, or make the imports more explicit with full paths.

Suggested change
"// AUTO-GENERATED FILE. DO NOT EDIT.\n\
"// AUTO-GENERATED FILE. DO NOT EDIT.\n\
// NOTE: This generated code assumes the module path `attestation::measurements::*` is available.\n\
// Ensure this file is included in a module that has access to these imports.\n\

Copilot uses AI. Check for mistakes.
use attestation::measurements::*;
pub const EXPECTED_MEASUREMENTS: &[ExpectedMeasurements] = &[\n"
)
.unwrap();

// Process each file
for path in measurement_files {
let json_str = fs::read_to_string(&path).unwrap();
let tcb: serde_json::Value = serde_json::from_str(&json_str).unwrap();

// extract 4 RTMRs + MRTD
let mrtd = decode_hex(tcb["mrtd"].as_str().unwrap());
let rtmr0 = decode_hex(tcb["rtmr0"].as_str().unwrap());
let rtmr1 = decode_hex(tcb["rtmr1"].as_str().unwrap());
let rtmr2 = decode_hex(tcb["rtmr2"].as_str().unwrap());
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The build script panics with unwrap() at multiple points when parsing JSON. If a JSON file is malformed or missing required fields, the build will fail with an unhelpful panic. Consider using expect() with descriptive messages that include the filename and what field is missing or invalid. For example, "Failed to parse {filename}: missing 'mrtd' field" or "Failed to decode hex in {filename} for field 'rtmr0'".

Copilot uses AI. Check for mistakes.

// extract key-provider digest
let mut key_provider_digest = None;
if let Some(events) = tcb["event_log"].as_array() {
for event in events {
if event["event"].as_str().unwrap() == "key-provider" {
key_provider_digest =
Some(decode_hex(event["digest"].as_str().unwrap()));
break;
}
}
}

let key_provider_digest =
key_provider_digest.expect("key-provider event not found");

// Emit Rust struct
writeln!(
f,
" ExpectedMeasurements {{ \
rtmrs: Measurements {{ \
mrtd: {:?}, \
rtmr0: {:?}, \
rtmr1: {:?}, \
rtmr2: {:?} \
}}, \
key_provider_event_digest: {:?}, \
}},",
mrtd, rtmr0, rtmr1, rtmr2, key_provider_digest
)
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The generated Rust code uses the Debug formatting for byte arrays which will produce a verbose output like [1, 2, 3, ...]. Consider formatting these arrays as hex literals instead (e.g., *b"hex_string_here" or using a custom formatting function) to make the generated code more compact and easier to read.

Copilot uses AI. Check for mistakes.
.unwrap();
}

// Close the array
writeln!(f, "];").unwrap();
}

/// Decode a hex string into a fixed 48-byte array
fn decode_hex(hex: &str) -> [u8; 48] {
let bytes = hex::decode(hex).expect("invalid hex");
assert!(
bytes.len() == 48,
"expected 48-byte measurement, got {} bytes",
bytes.len()
);
let mut arr = [0u8; 48];
arr.copy_from_slice(&bytes);
arr
}
84 changes: 84 additions & 0 deletions crates/mpc-attestation/measurements_build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 📘 TCB Measurements Build Guide

## 📁 Location of JSON Files
TCB measurement JSON files now live in:

```
crates/mpc-attestation/assets/
```

Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

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

The documentation states "Human-readable TCB measurement JSON files live in:" but doesn't mention that these files should not be edited directly in production. Consider adding a note about the security implications of modifying these files and the need for proper validation/approval processes when updating measurements.

Suggested change
> **⚠️ Security Warning:**
> **Do not edit TCB measurement JSON files directly in production environments.**
> These files are critical for attestation and system security.
> Any updates must go through proper validation and approval processes to prevent accidental or malicious changes.

Copilot uses AI. Check for mistakes.
Typical files:

```
tcb_info.json
tcb_info_dev.json
```

json format is taken directly from the node `/public_data` endpoint.

You can add more files in the future (e.g., staging, new image versions).
Every file matching the prefix:

```
tcb_info*
```

will be automatically included at build time.

---

## 🔄 How to Update Measurements
1. Replace or edit the JSON files under:

```
crates/mpc-attestation/assets/
```

2. Run a rebuild:

```bash
cargo clean -p mpc-attestation
cargo build -p mpc-attestation
```

The build script will:

- parse the JSON files
- decode the measurements
- generate Rust static byte arrays
- embed them into the final WASM contract

No runtime parsing and no JSON reading inside the contract.

---


## 🧬 Location of the Generated Measurements File
During build, Cargo produces:

```
target/debug/build/mpc-attestation-*/out/measurements_generated.rs
```

or in release mode:

```
target/release/build/mpc-attestation-*/out/measurements_generated.rs
```

This file contains:

```rust
pub const EXPECTED_MEASUREMENTS: &[ExpectedMeasurements] = &[
ExpectedMeasurements { … },
ExpectedMeasurements { … },
];
```

And is included into the crate via:

```rust
include!(concat!(env!("OUT_DIR"), "/measurements_generated.rs"));
```
Comment on lines +106 to +108
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should generate a crate that can be included using normal imports instead of using include!(...).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Before addressing this, please see my alternative proposal in: #1659


This file is **auto-generated** and should **not be committed** to the repo.
30 changes: 22 additions & 8 deletions crates/mpc-attestation/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ use attestation::{
report_data::ReportData,
};

include!(concat!(env!("OUT_DIR"), "/measurements_generated.rs"));

pub struct ExpectedMeasurementsSet;

impl ExpectedMeasurementsSet {
pub fn all() -> &'static [ExpectedMeasurements] {
EXPECTED_MEASUREMENTS
}
}

pub use attestation::attestation::{DstackAttestation, VerificationError};

use mpc_primitives::hash::{LauncherDockerComposeHash, MpcDockerImageHash};
Expand Down Expand Up @@ -70,21 +80,25 @@ impl Attestation {
}
};

//todo remove this
// Embedded JSON assets
const TCB_INFO_STRING_PROD: &str = include_str!("../assets/tcb_info.json");
// const TCB_INFO_STRING_PROD: &str = include_str!("../assets/tcb_info.json");
// TODO Security #1433 - remove dev measurements from production builds after testing is complete.
const TCB_INFO_STRING_DEV: &str = include_str!("../assets/tcb_info_dev.json");
// TCB_INFO_STRING_DEV: &str = include_str!("../assets/tcb_info_dev.json");

//todo remove this
//let accepted_measurements = ExpectedMeasurements::from_embedded_tcb_info(&[
// TCB_INFO_STRING_PROD,
// TCB_INFO_STRING_DEV,
//])
// .map_err(VerificationError::EmbeddedMeasurementsParsing)?;

let accepted_measurements = ExpectedMeasurements::from_embedded_tcb_info(&[
TCB_INFO_STRING_PROD,
TCB_INFO_STRING_DEV,
])
.map_err(VerificationError::EmbeddedMeasurementsParsing)?;
let accepted_measurements = ExpectedMeasurementsSet::all();

attestation.verify(
expected_report_data,
timestamp_seconds,
&accepted_measurements,
accepted_measurements,
)
}

Expand Down
Loading