igvmfilegen/signed_measurement/
tdx.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Support for creating TDX MRTD
5
6use super::SHA_384_OUTPUT_SIZE_BYTES;
7use crate::file_loader::DEFAULT_COMPATIBILITY_MASK;
8use igvm::IgvmDirectiveHeader;
9use igvm_defs::PAGE_SIZE_4K;
10use sha2::Digest;
11use sha2::Sha384;
12use std::collections::HashMap;
13use thiserror::Error;
14use zerocopy::FromBytes;
15use zerocopy::Immutable;
16use zerocopy::IntoBytes;
17use zerocopy::KnownLayout;
18
19#[derive(Debug, Error)]
20pub enum Error {
21    #[error("invalid parameter area index")]
22    InvalidParameterAreaIndex,
23}
24
25/// Measure adding a page to TD.
26#[repr(C)]
27#[derive(Debug, Clone, Copy, IntoBytes, Immutable, KnownLayout, FromBytes)]
28pub struct TdxPageAdd {
29    /// MEM.PAGE.ADD
30    pub operation: [u8; 16],
31    /// Must be aligned to a page size boundary.
32    pub gpa: u64,
33    /// Reserved mbz.
34    pub mbz: [u8; 104],
35}
36
37const TDX_EXTEND_CHUNK_SIZE: usize = 256;
38
39/// Measure adding a chunk of data to TD.
40#[repr(C)]
41#[derive(Debug, Clone, Copy, IntoBytes, Immutable, KnownLayout, FromBytes)]
42pub struct TdxMrExtend {
43    /// MR.EXTEND
44    pub operation: [u8; 16],
45    /// Aligned to a 256B boundary.
46    pub gpa: u64,
47    /// Reserved mbz.
48    pub mbz: [u8; 104],
49    /// Data to measure.
50    pub data: [u8; TDX_EXTEND_CHUNK_SIZE],
51}
52
53/// Iterate through all headers to create the MRTD.
54pub fn generate_tdx_measurement(
55    directive_headers: &[IgvmDirectiveHeader],
56) -> Result<[u8; SHA_384_OUTPUT_SIZE_BYTES], Error> {
57    let mut parameter_area_table = HashMap::new();
58    const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize;
59    let tdx_compatibility_mask = DEFAULT_COMPATIBILITY_MASK;
60    // Reuse the same vec for padding out data to 4k.
61    let mut padding_vec = vec![0; PAGE_SIZE_4K_USIZE];
62    let mut hasher = Sha384::new();
63
64    let mut measure_page = |gpa: u64, page_data: Option<&[u8]>| {
65        // Measure the page being added.
66        let page_add = TdxPageAdd {
67            operation: *b"MEM.PAGE.ADD\0\0\0\0",
68            gpa,
69            mbz: [0; 104],
70        };
71        hasher.update(page_add.as_bytes());
72
73        // Possibly measure the page contents in chunks.
74        if let Some(data) = page_data {
75            let data = match data.len() {
76                0 => None,
77                PAGE_SIZE_4K_USIZE => Some(data),
78                _ if data.len() < PAGE_SIZE_4K_USIZE => {
79                    padding_vec.fill(0);
80                    padding_vec[..data.len()].copy_from_slice(data);
81                    Some(padding_vec.as_slice())
82                }
83                _ => {
84                    panic!("Unexpected data size");
85                }
86            };
87
88            // Hash the contents of the 4K page, 256 bytes at a time.
89            for offset in (0..PAGE_SIZE_4K).step_by(TDX_EXTEND_CHUNK_SIZE) {
90                let mut mr_extend = TdxMrExtend {
91                    operation: *b"MR.EXTEND\0\0\0\0\0\0\0",
92                    gpa: gpa + offset,
93                    mbz: [0; 104],
94                    data: [0; TDX_EXTEND_CHUNK_SIZE],
95                };
96
97                // Copy in data for chunk if it exists.
98                if let Some(data) = data {
99                    mr_extend.data.copy_from_slice(
100                        &data[offset as usize..offset as usize + TDX_EXTEND_CHUNK_SIZE],
101                    );
102                }
103                hasher.update(mr_extend.as_bytes());
104            }
105        };
106    };
107
108    // Loop over all the page data to build the digest
109    for header in directive_headers {
110        // Skip headers that have compatibility masks that do not match TDX.
111        if header
112            .compatibility_mask()
113            .map(|mask| mask & tdx_compatibility_mask != tdx_compatibility_mask)
114            .unwrap_or(false)
115        {
116            continue;
117        }
118
119        match header {
120            IgvmDirectiveHeader::ParameterArea {
121                number_of_bytes,
122                parameter_area_index,
123                initial_data: _,
124            } => {
125                assert_eq!(
126                    parameter_area_table.contains_key(&parameter_area_index),
127                    false
128                );
129                assert_eq!(number_of_bytes % PAGE_SIZE_4K, 0);
130                parameter_area_table.insert(parameter_area_index, number_of_bytes);
131            }
132            IgvmDirectiveHeader::PageData {
133                gpa,
134                compatibility_mask,
135                flags,
136                data_type: _,
137                data,
138            } => {
139                assert_eq!(
140                    compatibility_mask & tdx_compatibility_mask,
141                    tdx_compatibility_mask
142                );
143
144                // Skip shared pages.
145                if flags.shared() {
146                    continue;
147                }
148
149                // If data is unmeasured, only measure the GPA.
150                let data = if flags.unmeasured() {
151                    None
152                } else {
153                    Some(data.as_bytes())
154                };
155
156                measure_page(*gpa, data);
157            }
158            IgvmDirectiveHeader::ParameterInsert(param) => {
159                assert_eq!(
160                    param.compatibility_mask & tdx_compatibility_mask,
161                    tdx_compatibility_mask
162                );
163
164                let parameter_area_size = parameter_area_table
165                    .get(&param.parameter_area_index)
166                    .ok_or(Error::InvalidParameterAreaIndex)?;
167
168                for gpa in (param.gpa..param.gpa + *parameter_area_size).step_by(PAGE_SIZE_4K_USIZE)
169                {
170                    measure_page(gpa, None);
171                }
172            }
173            _ => {}
174        }
175    }
176
177    let mrtd: [u8; SHA_384_OUTPUT_SIZE_BYTES] = hasher.finalize().into();
178    tracing::info!("MRTD: {}", hex::encode_upper(mrtd));
179    Ok(mrtd)
180}