Skip to main content

igvmfilegen/signed_measurement/
snp.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Support for creating SNP ID blocks
5
6use super::SHA_384_OUTPUT_SIZE_BYTES;
7use crate::file_loader::DEFAULT_COMPATIBILITY_MASK;
8use igvm::IgvmDirectiveHeader;
9use igvm::IgvmInitializationHeader;
10use igvm_defs::IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY;
11use igvm_defs::IGVM_VHS_SNP_ID_BLOCK_SIGNATURE;
12use igvm_defs::IgvmPageDataType;
13use igvm_defs::PAGE_SIZE_4K;
14use sha2::Digest;
15use sha2::Sha384;
16use std::collections::HashMap;
17use thiserror::Error;
18use x86defs::snp::SnpPageInfo;
19use x86defs::snp::SnpPageType;
20use x86defs::snp::SnpPspIdBlock;
21use zerocopy::IntoBytes;
22
23#[derive(Debug, Error)]
24pub enum Error {
25    #[error("invalid parameter area index")]
26    InvalidParameterAreaIndex,
27    #[error("failed to sign temporary SNP ID block: {0}")]
28    TempSigning(String),
29}
30
31const SNP_ID_KEY_ALGORITHM_ECDSA_P384_SHA384: u32 = 1;
32const SNP_ECDSA_CURVE_P384: u32 = 2;
33const SNP_ECC_KEY_SIZE_BYTES: usize = 48;
34const SNP_ECC_COMPONENT_SIZE_BYTES: usize = 72;
35
36/// Iterate through all headers, creating a launch digest which is then signed,
37/// returning the launch digest. Also emits a temporarily-signed
38/// [`IgvmDirectiveHeader::SnpIdBlock`] directive (the presence of this directive
39/// signals the IGVM loader to set `id_block_en = 1` at launch time).
40pub fn generate_snp_measurement(
41    initialization_headers: &[IgvmInitializationHeader],
42    directive_headers: &mut Vec<IgvmDirectiveHeader>,
43    svn: u32,
44) -> Result<[u8; SHA_384_OUTPUT_SIZE_BYTES], Error> {
45    let mut parameter_area_table = HashMap::new();
46    const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize;
47    let snp_compatibility_mask = DEFAULT_COMPATIBILITY_MASK;
48
49    let mut launch_digest: [u8; SHA_384_OUTPUT_SIZE_BYTES] = [0; SHA_384_OUTPUT_SIZE_BYTES];
50    let zero_page: [u8; PAGE_SIZE_4K as usize] = [0; PAGE_SIZE_4K as usize];
51    let mut hasher = Sha384::new();
52
53    // Hash the contents of empty 4K page, used when file does not carry data
54    hasher.update(zero_page.as_bytes());
55    let zero_digest = hasher.finalize();
56
57    // Reuse the same vec for padding out data to 4k.
58    let mut padding_vec = vec![0; PAGE_SIZE_4K_USIZE];
59
60    let mut measure_page = |page_type: SnpPageType, gpa: u64, page_data: Option<&[u8]>| {
61        let mut hash = Sha384::new();
62        let hash_contents = match page_data {
63            Some(data) => {
64                match data.len() {
65                    0 => zero_digest,
66                    _ if data.len() < PAGE_SIZE_4K_USIZE => {
67                        padding_vec.fill(0);
68                        padding_vec[..data.len()].copy_from_slice(data);
69                        hash.update(&padding_vec);
70                        hash.finalize()
71                    }
72                    PAGE_SIZE_4K_USIZE => {
73                        hash.update(data);
74                        hash.finalize()
75                    }
76                    _ => {
77                        // TODO SNP: Need to check the PSP spec how to measure 2MB
78                        // pages. Fail for now, as they shouldn't exist.
79                        todo!(
80                            "unable to measure greater than 4k pages, len: {}",
81                            data.len()
82                        )
83                    }
84                }
85            }
86            None => [0; SHA_384_OUTPUT_SIZE_BYTES].into(),
87        };
88
89        let info = SnpPageInfo {
90            digest_current: launch_digest,
91            contents: hash_contents.into(),
92            length: size_of::<SnpPageInfo>() as u16,
93            page_type,
94            imi_page_bit: 0,
95            lower_vmpl_permissions: 0,
96            gpa,
97        };
98
99        let mut hash = Sha384::new();
100        hash.update(info.as_bytes());
101        launch_digest = hash.finalize().into();
102    };
103
104    let mut policy: u64 = 0;
105
106    for header in initialization_headers {
107        if let IgvmInitializationHeader::GuestPolicy {
108            policy: snp_policy,
109            compatibility_mask,
110        } = header
111        {
112            assert_eq!(
113                compatibility_mask & snp_compatibility_mask,
114                snp_compatibility_mask
115            );
116            policy = *snp_policy;
117        }
118    }
119    assert_ne!(policy, 0);
120
121    // Loop over all the page data to build the digest
122    for header in directive_headers.iter() {
123        // Skip headers that have compatibility masks that do not match snp.
124        if header
125            .compatibility_mask()
126            .map(|mask| mask & snp_compatibility_mask != snp_compatibility_mask)
127            .unwrap_or(false)
128        {
129            continue;
130        }
131
132        match header {
133            IgvmDirectiveHeader::ErrorRange { .. } => todo!("error range not implemented"),
134            IgvmDirectiveHeader::ParameterArea {
135                number_of_bytes,
136                parameter_area_index,
137                initial_data: _,
138            } => {
139                assert_eq!(
140                    parameter_area_table.contains_key(&parameter_area_index),
141                    false
142                );
143                assert_eq!(number_of_bytes % PAGE_SIZE_4K, 0);
144                parameter_area_table.insert(parameter_area_index, number_of_bytes);
145            }
146            IgvmDirectiveHeader::PageData {
147                gpa,
148                compatibility_mask,
149                flags,
150                data_type,
151                data,
152            } => {
153                assert_eq!(
154                    compatibility_mask & snp_compatibility_mask,
155                    snp_compatibility_mask
156                );
157
158                // Skip shared pages.
159                if flags.shared() {
160                    continue;
161                }
162
163                let (page_type, data) = match *data_type {
164                    IgvmPageDataType::SECRETS => (SnpPageType::SECRETS, None),
165                    IgvmPageDataType::CPUID_DATA | IgvmPageDataType::CPUID_XF => {
166                        (SnpPageType::CPUID, None)
167                    }
168                    _ => {
169                        if flags.unmeasured() {
170                            (SnpPageType::UNMEASURED, None)
171                        } else {
172                            (SnpPageType::NORMAL, Some(data.as_bytes()))
173                        }
174                    }
175                };
176
177                measure_page(page_type, *gpa, data);
178            }
179            IgvmDirectiveHeader::ParameterInsert(param) => {
180                assert_eq!(
181                    param.compatibility_mask & snp_compatibility_mask,
182                    snp_compatibility_mask
183                );
184
185                let parameter_area_size = parameter_area_table
186                    .get(&param.parameter_area_index)
187                    .ok_or(Error::InvalidParameterAreaIndex)?;
188
189                for gpa in (param.gpa..param.gpa + *parameter_area_size).step_by(PAGE_SIZE_4K_USIZE)
190                {
191                    measure_page(SnpPageType::UNMEASURED, gpa, None)
192                }
193            }
194            IgvmDirectiveHeader::SnpVpContext {
195                gpa,
196                compatibility_mask,
197                vp_index: _,
198                vmsa,
199            } => {
200                assert_eq!(
201                    compatibility_mask & snp_compatibility_mask,
202                    snp_compatibility_mask
203                );
204
205                let vmsa_bytes = vmsa.as_ref().as_bytes();
206                measure_page(SnpPageType::VMSA, *gpa, Some(vmsa_bytes));
207            }
208            _ => {}
209        }
210    }
211
212    // Underhill family ID for the SNP ID block.
213    const UNDERHILL_FAMILY_ID: [u8; 16] = [
214        0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
215        0x00,
216    ];
217    let family_id = UNDERHILL_FAMILY_ID;
218    let image_id = *b"underhill\0\0\0\0\0\0\0";
219
220    // Generate the PSP ID block format, hash with SHA-384.
221    let psp_id_block = SnpPspIdBlock {
222        ld: launch_digest,
223        version: 0x1,
224        guest_svn: svn,
225        policy,
226        family_id,
227        image_id,
228    };
229
230    // Print the ID block for reference.
231    tracing::info!("SNP ID Block {:x?}", psp_id_block);
232
233    // Generate a temporary key and sign the ID block hash.
234    let (id_key_signature, id_public_key) = sign_id_block_with_temp_key(&psp_id_block)?;
235    directive_headers.push(IgvmDirectiveHeader::SnpIdBlock {
236        compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
237        author_key_enabled: 0,
238        reserved: [0; 3],
239        ld: psp_id_block.ld,
240        family_id: psp_id_block.family_id,
241        image_id: psp_id_block.image_id,
242        version: psp_id_block.version,
243        guest_svn: psp_id_block.guest_svn,
244        id_key_algorithm: SNP_ID_KEY_ALGORITHM_ECDSA_P384_SHA384,
245        author_key_algorithm: 0,
246        id_key_signature: Box::new(id_key_signature),
247        id_public_key: Box::new(id_public_key),
248        author_key_signature: Box::new(IGVM_VHS_SNP_ID_BLOCK_SIGNATURE {
249            r_comp: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
250            s_comp: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
251        }),
252        author_public_key: Box::new(IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY {
253            curve: 0,
254            reserved: 0,
255            qx: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
256            qy: [0; SNP_ECC_COMPONENT_SIZE_BYTES],
257        }),
258    });
259
260    Ok(psp_id_block.ld)
261}
262
263/// Zero-pads and reverses a big-endian ECC component into a 72-byte
264/// little-endian array as required by the PSP ID block format.
265fn padded_le_component(input_be: &[u8]) -> [u8; SNP_ECC_COMPONENT_SIZE_BYTES] {
266    let mut out = [0u8; SNP_ECC_COMPONENT_SIZE_BYTES];
267    for (dst, src) in out.iter_mut().zip(input_be.iter().rev()) {
268        *dst = *src;
269    }
270    out
271}
272
273/// Generate a temporary ECDSA P-384 key pair using the selected `crypto`
274/// backend, sign the SHA-384 hash of the ID block, and return the signature
275/// + public key in the format expected by `IGVM_VHS_SNP_ID_BLOCK`.
276fn sign_id_block_with_temp_key(
277    id_block: &SnpPspIdBlock,
278) -> Result<
279    (
280        IGVM_VHS_SNP_ID_BLOCK_SIGNATURE,
281        IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY,
282    ),
283    Error,
284> {
285    use crypto::ecdsa::{EcdsaCurve, EcdsaKeyPair};
286
287    // Generate a random P-384 key pair for ECDSA signing.
288    let key = EcdsaKeyPair::generate(EcdsaCurve::P384)
289        .map_err(|e| Error::TempSigning(format!("EcdsaKeyPair::generate: {e}")))?;
290
291    // Hash the ID block with SHA-384.
292    let mut hash = Sha384::new();
293    hash.update(id_block.as_bytes());
294    let id_block_hash: [u8; SHA_384_OUTPUT_SIZE_BYTES] = hash.finalize().into();
295
296    use base64::Engine as _;
297    let b64 = base64::engine::general_purpose::STANDARD;
298    tracing::info!("Input Hash Base64: {}", b64.encode(id_block_hash));
299    tracing::info!("Using Temporary Signing Key");
300
301    // Sign the hash. Returns r || s in big-endian, each 48 bytes for P-384.
302    let signature = key
303        .sign_prehash(&id_block_hash)
304        .map_err(|e| Error::TempSigning(format!("sign_prehash: {e}")))?;
305
306    if signature.len() != SNP_ECC_KEY_SIZE_BYTES * 2 {
307        return Err(Error::TempSigning(format!(
308            "unexpected signature size {}",
309            signature.len()
310        )));
311    }
312
313    let (sig_r_be, sig_s_be) = signature.split_at(SNP_ECC_KEY_SIZE_BYTES);
314    let id_key_signature = IGVM_VHS_SNP_ID_BLOCK_SIGNATURE {
315        r_comp: padded_le_component(sig_r_be),
316        s_comp: padded_le_component(sig_s_be),
317    };
318
319    tracing::info!("Signature R Base64: {}", b64.encode(sig_r_be));
320    tracing::info!("Signature S Base64: {}", b64.encode(sig_s_be));
321
322    // Export the public key as Qx || Qy in big-endian, each 48 bytes for P-384.
323    let public_key = key
324        .public_key_bytes()
325        .map_err(|e| Error::TempSigning(format!("public_key_bytes: {e}")))?;
326
327    if public_key.len() != SNP_ECC_KEY_SIZE_BYTES * 2 {
328        return Err(Error::TempSigning(format!(
329            "unexpected public key size {}",
330            public_key.len()
331        )));
332    }
333
334    let (qx_be, qy_be) = public_key.split_at(SNP_ECC_KEY_SIZE_BYTES);
335
336    tracing::info!("Public Key Qx Base64: {}", b64.encode(qx_be));
337    tracing::info!("Public Key Qy Base64: {}", b64.encode(qy_be));
338    let id_public_key = IGVM_VHS_SNP_ID_BLOCK_PUBLIC_KEY {
339        curve: SNP_ECDSA_CURVE_P384,
340        reserved: 0,
341        qx: padded_le_component(qx_be),
342        qy: padded_le_component(qy_be),
343    };
344
345    Ok((id_key_signature, id_public_key))
346}