igvmfilegen/signed_measurement/
vbs.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Support for VBS measurements
5
6use super::SHA_256_OUTPUT_SIZE_BYTES;
7use crate::file_loader::DEFAULT_COMPATIBILITY_MASK;
8use igvm::IgvmDirectiveHeader;
9use igvm_defs::IgvmPageDataType;
10use igvm_defs::PAGE_SIZE_4K;
11use igvm_defs::VbsDigestAlgorithm;
12use igvm_defs::VbsSigningAlgorithm;
13use igvm_defs::VbsVpContextRegister;
14use sha2::Digest;
15use sha2::Sha256;
16use std::collections::HashMap;
17use thiserror::Error;
18use vbs_defs::BootMeasurementType;
19use vbs_defs::VBS_POLICY_FLAGS;
20use vbs_defs::VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA;
21use vbs_defs::VBS_VM_GPA_PAGE_BOOT_METADATA;
22use vbs_defs::VBS_VP_CHUNK_SIZE_BYTES;
23use vbs_defs::VM_GPA_PAGE_READABLE;
24use vbs_defs::VM_GPA_PAGE_WRITABLE;
25use vbs_defs::VbsChunkHeader;
26use vbs_defs::VbsRegisterChunk;
27use vbs_defs::VpGpaPageChunk;
28use zerocopy::IntoBytes;
29
30#[derive(Debug, Error)]
31pub enum Error {
32    #[error("invalid parameter area index")]
33    InvalidParameterAreaIndex,
34}
35
36/// Iterate through all headers, creating a boot measurement which is then signed,
37/// returning an [`IgvmDirectiveHeader::VbsMeasurement`]
38pub fn generate_vbs_measurement(
39    directive_headers: &[IgvmDirectiveHeader],
40    enable_debug: bool,
41    svn: u32,
42) -> Result<[u8; SHA_256_OUTPUT_SIZE_BYTES], Error> {
43    const VBS_COMPATIBILITY_MASK: u32 = DEFAULT_COMPATIBILITY_MASK;
44
45    let mut digest = VbsDigestor::new()?;
46    let mut parameter_area_table = HashMap::new();
47    let mut bsp_regs = Vec::new();
48
49    for header in directive_headers {
50        // Skip headers that have compatibility masks that do not match vbs.
51        if header
52            .compatibility_mask()
53            .map(|mask| mask & VBS_COMPATIBILITY_MASK != VBS_COMPATIBILITY_MASK)
54            .unwrap_or(false)
55        {
56            continue;
57        }
58
59        match header {
60            IgvmDirectiveHeader::PageData {
61                gpa,
62                compatibility_mask,
63                flags,
64                data_type,
65                data,
66            } => {
67                assert_eq!(
68                    compatibility_mask & VBS_COMPATIBILITY_MASK,
69                    VBS_COMPATIBILITY_MASK
70                );
71
72                assert_eq!(*data_type, IgvmPageDataType::NORMAL);
73
74                // Skip shared pages.
75                if flags.shared() {
76                    continue;
77                }
78
79                let boot_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new()
80                    .with_acceptance(0)
81                    .with_data_unmeasured(flags.unmeasured());
82                digest.record_gpa_page(gpa / PAGE_SIZE_4K, 1, boot_metadata, data)?;
83            }
84            IgvmDirectiveHeader::ParameterInsert(param) => {
85                let page_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new()
86                    .with_acceptance(0)
87                    .with_data_unmeasured(true);
88                let parameter_area_size = parameter_area_table
89                    .get(&param.parameter_area_index)
90                    .ok_or(Error::InvalidParameterAreaIndex)?;
91                digest.record_gpa_page(
92                    param.gpa / PAGE_SIZE_4K,
93                    parameter_area_size / PAGE_SIZE_4K,
94                    page_metadata,
95                    &[],
96                )?;
97            }
98            IgvmDirectiveHeader::X64VbsVpContext {
99                vtl,
100                registers,
101                compatibility_mask,
102            } => {
103                assert_eq!(
104                    compatibility_mask & VBS_COMPATIBILITY_MASK,
105                    VBS_COMPATIBILITY_MASK
106                );
107                // The Vbs measurement format requires the cpu context to be measured last, measure at end
108                let vtl_registers: Vec<VbsVpContextRegister> = registers
109                    .iter()
110                    .map(|r| r.into_vbs_vp_context_reg(*vtl))
111                    .collect();
112                bsp_regs.push(vtl_registers);
113            }
114            IgvmDirectiveHeader::AArch64VbsVpContext {
115                vtl,
116                registers,
117                compatibility_mask,
118            } => {
119                assert_eq!(
120                    compatibility_mask & VBS_COMPATIBILITY_MASK,
121                    VBS_COMPATIBILITY_MASK
122                );
123                // The Vbs measurement format requires the cpu context to be measured last, measure at end
124                let vtl_registers: Vec<VbsVpContextRegister> = registers
125                    .iter()
126                    .map(|r| r.into_vbs_vp_context_reg(*vtl))
127                    .collect();
128                bsp_regs.push(vtl_registers);
129            }
130            IgvmDirectiveHeader::ErrorRange {
131                gpa,
132                compatibility_mask,
133                size_bytes,
134            } => {
135                assert_eq!(
136                    compatibility_mask & VBS_COMPATIBILITY_MASK,
137                    VBS_COMPATIBILITY_MASK
138                );
139                let page_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new()
140                    .with_acceptance(VM_GPA_PAGE_READABLE | VM_GPA_PAGE_WRITABLE)
141                    .with_data_unmeasured(true);
142                digest.record_gpa_page(
143                    *gpa / PAGE_SIZE_4K,
144                    (*size_bytes as u64).div_ceil(PAGE_SIZE_4K),
145                    page_metadata,
146                    &[],
147                )?;
148            }
149            IgvmDirectiveHeader::ParameterArea {
150                number_of_bytes,
151                parameter_area_index,
152                initial_data: _,
153            } => {
154                if parameter_area_table.contains_key(parameter_area_index) {
155                    return Err(Error::InvalidParameterAreaIndex);
156                }
157                parameter_area_table.insert(parameter_area_index, *number_of_bytes);
158            }
159            _ => {}
160        }
161    }
162
163    // Measure all registers in each VTL as last step in measurement
164    for set in bsp_regs {
165        for reg in set {
166            digest.record_vp_register(reg)?;
167        }
168    }
169
170    // Identifier constants chosen to maintain compatibility with internal tooling
171    const MSFT_PRODUCT_ID: u32 = u32::from_le_bytes(*b"msft");
172    const VBS_MODULE_ID: u32 = u32::from_le_bytes(*b"vbs\0");
173    const VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT: u32 = 0x1;
174
175    let boot_measurement = VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA {
176        version: VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT,
177        product_id: MSFT_PRODUCT_ID,
178        module_id: VBS_MODULE_ID,
179        security_version: svn,
180        security_policy: VBS_POLICY_FLAGS::new().with_debug(enable_debug),
181        boot_digest_algo: VbsDigestAlgorithm::SHA256.0,
182        signing_algo: VbsSigningAlgorithm::ECDSA_P384.0,
183        boot_measurement_digest: digest.finish_digest(),
184    };
185    // Print the signing data for reference, not currently used.
186    tracing::info!("Boot Measurement {:x?}", boot_measurement);
187    Ok(boot_measurement.boot_measurement_digest)
188}
189
190struct VbsDigestor {
191    digest: [u8; SHA_256_OUTPUT_SIZE_BYTES],
192}
193
194impl VbsDigestor {
195    fn new() -> Result<VbsDigestor, Error> {
196        Ok(VbsDigestor {
197            digest: [0; SHA_256_OUTPUT_SIZE_BYTES],
198        })
199    }
200
201    fn record_gpa_page(
202        &mut self,
203        gpa_page_base: u64,
204        page_count: u64,
205        page_metadata: VBS_VM_GPA_PAGE_BOOT_METADATA,
206        mut data: &[u8],
207    ) -> Result<(), Error> {
208        for page in 0..page_count {
209            let import_data_len: usize = match page_metadata.data_unmeasured() {
210                true => 0,
211                false => std::cmp::min(PAGE_SIZE_4K as usize, data.len()),
212            };
213            let (import_data, data_remaining) = data.split_at(import_data_len);
214            data = data_remaining;
215
216            // If page is under 4K bytes, pad to full length which will be hashed with page and chunk data
217            let padding = vec![0; PAGE_SIZE_4K as usize - import_data.len()];
218            let page_number = gpa_page_base + page;
219            let chunk = VpGpaPageChunk {
220                header: VbsChunkHeader {
221                    byte_count: VBS_VP_CHUNK_SIZE_BYTES as u32,
222                    chunk_type: BootMeasurementType::VP_GPA_PAGE,
223                    reserved: 0,
224                },
225                metadata: page_metadata.into(),
226                page_number,
227            };
228            self.create_record_entry(&[chunk.as_bytes(), import_data, &padding])?;
229        }
230        Ok(())
231    }
232
233    fn record_vp_register(&mut self, reg: VbsVpContextRegister) -> Result<(), Error> {
234        let chunk = VbsRegisterChunk {
235            header: VbsChunkHeader {
236                byte_count: size_of::<VbsRegisterChunk>() as u32,
237                chunk_type: BootMeasurementType::VP_REGISTER,
238                reserved: 0,
239            },
240            reserved: 0,
241            vtl: reg.vtl,
242            reserved2: 0,
243            reserved3: 0,
244            reserved4: 0,
245            name: reg.register_name.into(),
246            value: reg.register_value,
247        };
248        self.create_record_entry(&[chunk.as_bytes()])?;
249        Ok(())
250    }
251
252    fn create_record_entry(&mut self, chunks: &[&[u8]]) -> Result<(), Error> {
253        let mut hasher = Sha256::new();
254        hasher.update(self.digest.as_bytes());
255        for chunk in chunks {
256            hasher.update(chunk);
257        }
258        self.digest = hasher.finalize().into();
259        Ok(())
260    }
261
262    fn finish_digest(&self) -> [u8; SHA_256_OUTPUT_SIZE_BYTES] {
263        self.digest
264    }
265}