diff --git a/igvm/src/c_api.rs b/igvm/src/c_api.rs index ec80682..afdcdb4 100644 --- a/igvm/src/c_api.rs +++ b/igvm/src/c_api.rs @@ -290,7 +290,7 @@ fn get_header( .initialization_headers .get(index as usize) .ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)? - .write_binary_header(&mut header_binary) + .write_binary_header(&mut header_binary, &mut FileDataSerializer::new(0)) .map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?; } IgvmHeaderSection::HEADER_SECTION_DIRECTIVE => { @@ -322,17 +322,30 @@ fn get_header_data( let igvm = handle_lock.get_mut()?; let mut header_data = FileDataSerializer::new(0); - if section == IgvmHeaderSection::HEADER_SECTION_DIRECTIVE { - let header = igvm - .file - .directive_headers - .get(index as usize) - .ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?; - header - .write_binary_header(&mut Vec::::new(), &mut header_data) - .map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?; - } else { - return Err(IgvmResult::IGVMAPI_INVALID_PARAMETER); + match section { + IgvmHeaderSection::HEADER_SECTION_INITIALIZATION => { + let header = igvm + .file + .initialization_headers + .get(index as usize) + .ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?; + header + .write_binary_header(&mut Vec::::new(), &mut header_data) + .map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?; + } + IgvmHeaderSection::HEADER_SECTION_DIRECTIVE => { + let header = igvm + .file + .directive_headers + .get(index as usize) + .ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?; + header + .write_binary_header(&mut Vec::::new(), &mut header_data) + .map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?; + } + _ => { + return Err(IgvmResult::IGVMAPI_INVALID_PARAMETER); + } } let header_data = header_data.take(); diff --git a/igvm/src/lib.rs b/igvm/src/lib.rs index 1398cce..0100a04 100644 --- a/igvm/src/lib.rs +++ b/igvm/src/lib.rs @@ -344,6 +344,17 @@ pub enum IgvmInitializationHeader { vp_index: u16, vtl: Vtl, }, + CorimDocument { + compatibility_mask: u32, + // FUTURE: have the corim document in a typed structure, with the + // required per-plaform fields. + document: Vec, + }, + CorimSignature { + compatibility_mask: u32, + // FUTURE: have the corim signature in a typed structure + signature: Vec, + }, } impl IgvmInitializationHeader { @@ -357,9 +368,13 @@ impl IgvmInitializationHeader { IgvmInitializationHeader::PageTableRelocationRegion { .. } => { size_of::() } + IgvmInitializationHeader::CorimDocument { .. } => size_of::(), + IgvmInitializationHeader::CorimSignature { .. } => { + size_of::() + } }; - size_of::() + additional + align_8(size_of::() + additional) } /// Get the [`IgvmVariableHeaderType`] for the initialization header. @@ -376,6 +391,12 @@ impl IgvmInitializationHeader { IgvmInitializationHeader::PageTableRelocationRegion { .. } => { IgvmVariableHeaderType::IGVM_VHT_PAGE_TABLE_RELOCATION_REGION } + IgvmInitializationHeader::CorimDocument { .. } => { + IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT + } + IgvmInitializationHeader::CorimSignature { .. } => { + IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE + } } } @@ -454,14 +475,23 @@ impl IgvmInitializationHeader { Ok(()) } + // TODO: validate CoRIM document has the minimum fields required + // described by the corresponding specification for that platform. + IgvmInitializationHeader::CorimDocument { .. } => Ok(()), + // TODO: validate CoRIM signature has the right fields, and + // correctly signs the corresponding document. This requires crypto + // crates and might need to be gated behind a feature flag. + IgvmInitializationHeader::CorimSignature { .. } => Ok(()), } } /// Create a new [`IgvmInitializationHeader`] from the binary slice provided. /// Returns the remaining slice of unused bytes. - fn new_from_binary_split( - mut variable_headers: &[u8], - ) -> Result<(Self, &[u8]), BinaryHeaderError> { + fn new_from_binary_split<'a>( + mut variable_headers: &'a [u8], + file_data: &'a [u8], + file_data_start: u32, + ) -> Result<(Self, &'a [u8]), BinaryHeaderError> { let IGVM_VHS_VARIABLE_HEADER { typ, length } = read_header::(&mut variable_headers)?; @@ -469,6 +499,23 @@ impl IgvmInitializationHeader { let length = length as usize; + // Extract file data from a given file offset with the given size. File + // offset of 0 results in no data. + let extract_file_data = + |file_offset: u32, size: usize| -> Result, BinaryHeaderError> { + if file_offset == 0 { + return Ok(Vec::new()); + } + + let start = (file_offset - file_data_start) as usize; + let end = start + size; + + file_data + .get(start..end) + .ok_or(BinaryHeaderError::InvalidDataSize) + .map(|slice| slice.to_vec()) + }; + let header = match typ { IgvmVariableHeaderType::IGVM_VHT_GUEST_POLICY if length == size_of::() => @@ -550,6 +597,44 @@ impl IgvmInitializationHeader { vtl: vtl.try_into().map_err(|_| BinaryHeaderError::InvalidVtl)?, } } + IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT + if length == size_of::() => + { + let IGVM_VHS_CORIM_DOCUMENT { + compatibility_mask, + reserved, + file_offset, + size_bytes, + } = read_header(&mut variable_headers)?; + + if reserved != 0 { + return Err(BinaryHeaderError::ReservedNotZero); + } + + IgvmInitializationHeader::CorimDocument { + compatibility_mask, + document: extract_file_data(file_offset, size_bytes as usize)?, + } + } + IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE + if length == size_of::() => + { + let IGVM_VHS_CORIM_SIGNATURE { + compatibility_mask, + reserved, + file_offset, + size_bytes, + } = read_header(&mut variable_headers)?; + + if reserved != 0 { + return Err(BinaryHeaderError::ReservedNotZero); + } + + IgvmInitializationHeader::CorimSignature { + compatibility_mask, + signature: extract_file_data(file_offset, size_bytes as usize)?, + } + } _ => return Err(BinaryHeaderError::InvalidVariableHeaderType), }; @@ -572,10 +657,20 @@ impl IgvmInitializationHeader { PageTableRelocationRegion { compatibility_mask, .. } => Some(*compatibility_mask), + CorimDocument { + compatibility_mask, .. + } => Some(*compatibility_mask), + CorimSignature { + compatibility_mask, .. + } => Some(*compatibility_mask), } } - fn write_binary_header(&self, variable_headers: &mut Vec) -> Result<(), BinaryHeaderError> { + fn write_binary_header( + &self, + variable_headers: &mut Vec, + file_data: &mut FileDataSerializer, + ) -> Result<(), BinaryHeaderError> { // Only serialize this header if valid. self.validate()?; @@ -665,6 +760,48 @@ impl IgvmInitializationHeader { variable_headers, ); } + IgvmInitializationHeader::CorimDocument { + compatibility_mask, + document, + } => { + let file_offset = file_data.write_file_data(document); + + let corim_document = IGVM_VHS_CORIM_DOCUMENT { + compatibility_mask: *compatibility_mask, + reserved: 0, + file_offset, + size_bytes: document + .len() + .try_into() + .expect("corim document size must fit in u32"), + }; + append_header( + &corim_document, + IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT, + variable_headers, + ); + } + IgvmInitializationHeader::CorimSignature { + compatibility_mask, + signature, + } => { + let file_offset = file_data.write_file_data(signature); + + let corim_signature = IGVM_VHS_CORIM_SIGNATURE { + compatibility_mask: *compatibility_mask, + reserved: 0, + file_offset, + size_bytes: signature + .len() + .try_into() + .expect("corim signature size must fit in u32"), + }; + append_header( + &corim_signature, + IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE, + variable_headers, + ); + } } Ok(()) @@ -947,6 +1084,12 @@ pub enum BinaryHeaderError { UnsupportedX64Register(#[from] registers::UnsupportedRegister), #[error("unsupported AArch64 register")] UnsupportedAArch64Register(#[from] registers::UnsupportedRegister), + #[error("multiple corim documents for a given compatibility mask {0:x}")] + MultipleCorimDocuments(u32), + #[error("multiple corim signatures for a given compatibility mask {0:x}")] + MultipleCorimSignatures(u32), + #[error("corim document missing for compatibility mask {0:x} with corresponding signature")] + MissingCorimDocument(u32), } impl IgvmDirectiveHeader { @@ -1675,9 +1818,9 @@ impl IgvmDirectiveHeader { return Err(BinaryHeaderError::UnalignedAddress(*gpa)); } } - //TODO: validate SNP + // TODO: validate SNP IgvmDirectiveHeader::SnpIdBlock { .. } => {} - //TODO: validate VBS + // TODO: validate VBS IgvmDirectiveHeader::VbsMeasurement { .. } => {} } @@ -2423,6 +2566,14 @@ impl IgvmFile { Ok(()) }; + // Track which compatibility masks have had a corim document header, + // only one allowed per compatibility mask. + let mut corim_document_seen: [bool; 32] = [false; 32]; + + // Track which compatibility masks have a corim document signature, only + // one allowed per compatibility mask. + let mut corim_document_signature_seen: [bool; 32] = [false; 32]; + for header in initialization_headers { // Each individual header needs to be valid. header @@ -2504,7 +2655,46 @@ impl IgvmFile { }) } // TODO: validate SNP policy compatibility mask specifies SNP - _ => {} + IgvmInitializationHeader::GuestPolicy { .. } => {} + IgvmInitializationHeader::CorimDocument { + compatibility_mask, .. + } => { + // Validate that there is at most 1 corim document header + // for a given compatibility mask. + for single_mask in extract_individual_masks(*compatibility_mask) { + let mask_index = single_mask.trailing_zeros() as usize; + if corim_document_seen[mask_index] { + return Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleCorimDocuments(single_mask), + )); + } + corim_document_seen[mask_index] = true; + } + } + IgvmInitializationHeader::CorimSignature { + compatibility_mask, .. + } => { + // Validate that there is at most 1 corim document signature + // for a given compatibility mask. + for single_mask in extract_individual_masks(*compatibility_mask) { + let mask_index = single_mask.trailing_zeros() as usize; + + if corim_document_signature_seen[mask_index] { + return Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleCorimSignatures(single_mask), + )); + } + corim_document_signature_seen[mask_index] = true; + + // There must be a corim document for this compatibility + // mask, before this header. + if !corim_document_seen[mask_index] { + return Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MissingCorimDocument(single_mask), + )); + } + } + } } } @@ -2699,17 +2889,17 @@ impl IgvmFile { } } + // dedup file data + let mut file_data = FileDataSerializer::new(file_data_section_start); + // Add initialization headers for header in &self.initialization_headers { header - .write_binary_header(&mut variable_header_binary) - .map_err(Error::InvalidBinaryDirectiveHeader)?; + .write_binary_header(&mut variable_header_binary, &mut file_data) + .map_err(Error::InvalidBinaryInitializationHeader)?; assert_eq!(variable_header_binary.len() % 8, 0); } - // dedup file data - let mut file_data = FileDataSerializer::new(file_data_section_start); - // Add directive headers for header in &self.directive_headers { header @@ -2945,9 +3135,12 @@ impl IgvmFile { } } - let (header, new_slice) = - IgvmInitializationHeader::new_from_binary_split(variable_headers) - .map_err(Error::InvalidBinaryInitializationHeader)?; + let (header, new_slice) = IgvmInitializationHeader::new_from_binary_split( + variable_headers, + file_data, + file_data_start, + ) + .map_err(Error::InvalidBinaryInitializationHeader)?; variable_headers = new_slice; @@ -3325,6 +3518,12 @@ impl IgvmFile { IgvmInitializationHeader::PageTableRelocationRegion { compatibility_mask, .. } => fixup_mask(compatibility_mask), + IgvmInitializationHeader::CorimDocument { + compatibility_mask, .. + } => fixup_mask(compatibility_mask), + IgvmInitializationHeader::CorimSignature { + compatibility_mask, .. + } => fixup_mask(compatibility_mask), } } @@ -4105,6 +4304,53 @@ mod tests { } } + /// Test an initialization variable header matches the supplied args. Also + /// tests round-trip serialization/deserialization. + fn test_init_variable_header( + header: IgvmInitializationHeader, + file_data_offset: u32, + header_type: IgvmVariableHeaderType, + expected_variable_binary_header: T, + expected_file_data: Option>, + ) { + let mut binary_header = Vec::new(); + let mut file_data = FileDataSerializer::new(file_data_offset as usize); + + header + .write_binary_header(&mut binary_header, &mut file_data) + .unwrap(); + + let file_data = file_data.take(); + + let common_header = IGVM_VHS_VARIABLE_HEADER::read_from_prefix(&binary_header[..]) + .expect("variable header must be present") + .0; + + assert_eq!(common_header.typ, header_type); + assert_eq!( + align_8(common_header.length as usize), + size_of_val(&expected_variable_binary_header) + ); + assert_eq!( + &binary_header[size_of_val(&common_header)..], + expected_variable_binary_header.as_bytes() + ); + + match &expected_file_data { + Some(data) => assert_eq!(data, &file_data), + None => assert!(file_data.is_empty()), + } + + let (reserialized_header, remaining) = IgvmInitializationHeader::new_from_binary_split( + &binary_header, + &file_data, + file_data_offset, + ) + .unwrap(); + assert!(remaining.is_empty()); + assert_eq!(header, reserialized_header); + } + // Test get binary header for each type. #[test] fn test_page_data() { @@ -4687,5 +4933,242 @@ mod tests { ) } - // Test serialize and deserialize + #[test] + fn test_corim_document() { + let file_data_offset = 0x5000; + let document: Vec = vec![0xA1, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + + let raw_header = IGVM_VHS_CORIM_DOCUMENT { + compatibility_mask: 0x1, + file_offset: file_data_offset, + size_bytes: document.len() as u32, + reserved: 0, + }; + + let header = IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: document.clone(), + }; + + test_init_variable_header( + header, + file_data_offset, + IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT, + raw_header, + Some(document), + ); + } + + #[test] + fn test_corim_signature() { + let file_data_offset = 0x6000; + let signature: Vec = vec![0xD2, 0x84, 0x43, 0xA1, 0x01, 0x26, 0xA0, 0x44]; + + let raw_header = IGVM_VHS_CORIM_SIGNATURE { + compatibility_mask: 0x1, + file_offset: file_data_offset, + size_bytes: signature.len() as u32, + reserved: 0, + }; + + let header = IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x1, + signature: signature.clone(), + }; + + test_init_variable_header( + header, + file_data_offset, + IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE, + raw_header, + Some(signature), + ); + } + + mod corim { + use super::*; + + // TODO: when corim payload validation is added, these tests need to be + // updated to have real documents. + + fn validate(headers: &[IgvmInitializationHeader]) -> Result<(), Error> { + IgvmFile::validate_initialization_headers( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + headers, + ) + .map(|_| ()) + } + + #[test] + fn test_basic_roundtrip() { + let data1 = vec![1; PAGE_SIZE_4K as usize]; + let corim_doc = vec![0xA1, 0x02, 0x03, 0x04]; + let corim_sig = vec![0xD2, 0x84, 0x43, 0xA1]; + + let file = IgvmFile { + revision: IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + initialization_headers: vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: corim_doc.clone(), + }, + IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x1, + signature: corim_sig.clone(), + }, + ], + directive_headers: vec![new_page_data(0, 1, &data1)], + }; + + let mut binary_file = Vec::new(); + file.serialize(&mut binary_file).unwrap(); + + let deserialized = IgvmFile::new_from_binary(&binary_file, None).unwrap(); + assert_igvm_equal(&file, &deserialized); + } + + #[test] + fn test_corim_document_and_signature_valid() { + let headers = vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02, 0x03], + }, + IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x04, 0x05, 0x06], + }, + ]; + assert!(validate(&headers).is_ok()); + } + + #[test] + fn test_corim_document_without_signature_valid() { + let headers = vec![IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02, 0x03], + }]; + assert!(validate(&headers).is_ok()); + } + + #[test] + fn test_multiple_corim_documents_error() { + let headers = vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x03, 0x04], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleCorimDocuments(0x1) + )) + )); + } + + #[test] + fn test_multiple_corim_documents_different_masks_valid() { + let headers = vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x2, + document: vec![0x03, 0x04], + }, + ]; + assert!(validate(&headers).is_ok()); + } + + #[test] + fn test_multiple_corim_signatures_error() { + let headers = vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x03, 0x04], + }, + IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x05, 0x06], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleCorimSignatures(0x1) + )) + )); + } + + #[test] + fn test_corim_signature_without_document_error() { + let headers = vec![IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x01, 0x02], + }]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MissingCorimDocument(0x1) + )) + )); + } + + #[test] + fn test_corim_signature_wrong_mask_missing_document() { + let headers = vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmInitializationHeader::CorimSignature { + compatibility_mask: 0x2, + signature: vec![0x03, 0x04], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MissingCorimDocument(0x2) + )) + )); + } + + #[test] + fn test_corim_combined_mask_duplicate_document() { + let headers = vec![ + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x3, + document: vec![0x01, 0x02], + }, + IgvmInitializationHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x03, 0x04], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleCorimDocuments(0x1) + )) + )); + } + } } diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 45f836f..fd7291b 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -248,6 +248,12 @@ pub enum IgvmVariableHeaderType { /// A page table relocation region described by /// [`IGVM_VHS_PAGE_TABLE_RELOCATION`]. IGVM_VHT_PAGE_TABLE_RELOCATION_REGION = 0x103, + /// A Corim document structure described by [`IGVM_VHS_CORIM_DOCUMENT`]. + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + IGVM_VHT_CORIM_DOCUMENT = 0x104, + /// A Corim signature structure described by [`IGVM_VHS_CORIM_SIGNATURE`]. + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + IGVM_VHT_CORIM_SIGNATURE = 0x105, // These are IGVM_VHT_RANGE_DIRECTIVE structures. /// A parameter area structure described by [`IGVM_VHS_PARAMETER_AREA`]. @@ -1237,3 +1243,52 @@ pub enum VbsSigningAlgorithm { /// ECDSA P384. ECDSA_P384 = 0x1, } + +/// A structure defining a CoRIM CBOR document for a given platform. +/// +/// The data described by this header is a CBOR CoRIM document. There may only +/// be one for a given platform. There may be an associated COSE_Sign1 structure +/// for this document, see [`IGVM_VHS_CORIM_SIGNATURE`]. +/// +/// The CoRIM document must adhere to the following specifications for each +/// platform: +/// +/// | Platform | Specification | +/// |---------------|---------------| +/// | Intel TDX | TBD | +/// | VBS | TBD | +/// | AMD SEV-SNP | TBD | +/// | ARM CCA | TBD | +#[repr(C)] +#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct IGVM_VHS_CORIM_DOCUMENT { + /// Compatibility mask. + pub compatibility_mask: u32, + /// File offset for the CoRIM CBOR payload. + pub file_offset: u32, + /// Size in bytes of the CoRIM CBOR payload. + pub size_bytes: u32, + /// Reserved. + pub reserved: u32, +} + +/// This structure descibres a COSE_Sign1 structure for a detached CoRIM CBOR +/// payload for a given platform. The payload measured by this CBOR is described +/// the corresponding [`IGVM_VHS_CORIM_DOCUMENT`] structure, which must be +/// defined before this structure. +/// +/// For more information on the structure described by this header, see the +/// COSE_Sign1 structure described in section 4.2 in RFC +/// https://datatracker.ietf.org/doc/draft-ietf-rats-corim/. +#[repr(C)] +#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct IGVM_VHS_CORIM_SIGNATURE { + /// Compatibility mask. + pub compatibility_mask: u32, + /// File offset for the COSE_Sign1 measurement payload. + pub file_offset: u32, + /// Size in bytes of the COSE_Sign1 measurement payload. + pub size_bytes: u32, + /// Reserved. + pub reserved: u32, +}