From 754315b4896da99e2f2e1bfc293a4264a86f8845 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Tue, 11 Nov 2025 15:21:42 -0800 Subject: [PATCH 01/11] igvm_defs: introduce corim measurement header --- igvm_defs/src/lib.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 45f836f..8be5737 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -341,6 +341,10 @@ pub enum IgvmVariableHeaderType { /// specified by a structure of type [`IGVM_VHS_PARAMETER`]. #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] IGVM_VHT_ENVIRONMENT_INFO_PARAMETER = 0x313, + /// A Corim measurement structure described by [`IGVM_VHS_CORIM_MEASUREMENT`]. + /// FIXME: should this be an init header to be early in the file? + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + IGVM_VHT_CORIM_MEASUREMENT = 0x314, } /// The range of header types for platform structures. @@ -1237,3 +1241,29 @@ pub enum VbsSigningAlgorithm { /// ECDSA P384. ECDSA_P384 = 0x1, } + +/// A structure defining a CoRIM measurement payload for a given platform. +/// +/// The payload described by this header is a COSE_Sign1 structure described in +/// section 4.2 in RFC https://datatracker.ietf.org/doc/draft-ietf-rats-corim/, which is a COSE_Sign1 structure with a CBOR corim payload. +/// +/// The CoRIM payload 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)] +struct IGVM_VHS_CORIM_MEASUREMENT { + /// Compatibility mask. + pub compatibility_mask: u32, + /// File offset for the CoRIM measurement payload. + pub file_offset: u32, + /// Size in bytes of the CoRIM measurement payload. + pub size_bytes: u32, + /// Reserved. + pub reserved: u32, +} From ab3dc4b8b92beae6555093551b4ada1d975aba35 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Wed, 12 Nov 2025 15:34:04 -0800 Subject: [PATCH 02/11] split corim measurement and payload --- igvm_defs/src/lib.rs | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 8be5737..07cb057 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -345,6 +345,10 @@ pub enum IgvmVariableHeaderType { /// FIXME: should this be an init header to be early in the file? #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] IGVM_VHT_CORIM_MEASUREMENT = 0x314, + /// A Corim signature structure described by [`IGVM_VHS_CORIM_SIGNATURE`]. + /// FIXME: should this be an init header to be early in the file? + #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] + IGVM_VHT_CORIM_SIGNATURE = 0x315, } /// The range of header types for platform structures. @@ -1242,12 +1246,15 @@ pub enum VbsSigningAlgorithm { ECDSA_P384 = 0x1, } -/// A structure defining a CoRIM measurement payload for a given platform. +/// A structure defining a CoRIM CBOR payload for a given platform. TODO: rename +/// to remove measurement? /// -/// The payload described by this header is a COSE_Sign1 structure described in -/// section 4.2 in RFC https://datatracker.ietf.org/doc/draft-ietf-rats-corim/, which is a COSE_Sign1 structure with a CBOR corim payload. +/// The payload described by this header is a CBOR CoRIM payload. There may only +/// be one for a given platform. There may be an associated COSE_Sign1 structure +/// wrapping this payload, see [`IGVM_VHS_CORIM_SIGNATURE`]. /// -/// The CoRIM payload must adhere to the following specifications for each platform: +/// The CoRIM payload must adhere to the following specifications for each +/// platform: /// /// | Platform | Specification | /// |----------|---------------| @@ -1260,9 +1267,34 @@ pub enum VbsSigningAlgorithm { struct IGVM_VHS_CORIM_MEASUREMENT { /// Compatibility mask. pub compatibility_mask: u32, - /// File offset for the CoRIM measurement payload. + /// 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 is a signed COSE_Sign1 structure wrapping a CoRIM CBOR payload for a +/// given platform. The payload measured by this CBOR is described the +/// corresponding [`IGVM_VHS_CORIM_MEASUREMENT`] structure. There cannot be this +/// structure without that one. +/// +/// Note that a user may choose to create a single CBOR containing this +/// COSE_Sign1 with the payload filled in by the other corim measurement +/// structure. +/// +/// The payload described by this header is a COSE_Sign1 structure described in +/// section 4.2 in RFC https://datatracker.ietf.org/doc/draft-ietf-rats-corim/, +/// which is a COSE_Sign1 structure with a CBOR corim payload. +#[repr(C)] +#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] +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 CoRIM measurement payload. + /// Size in bytes of the COSE_Sign1 measurement payload. pub size_bytes: u32, /// Reserved. pub reserved: u32, From ed9defa12da8f674d563ee1cddaa3e36609c8e41 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Tue, 10 Feb 2026 15:38:12 -0800 Subject: [PATCH 03/11] corim: update header names and clarify docs --- igvm_defs/src/lib.rs | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 07cb057..215cb16 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -341,10 +341,10 @@ pub enum IgvmVariableHeaderType { /// specified by a structure of type [`IGVM_VHS_PARAMETER`]. #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] IGVM_VHT_ENVIRONMENT_INFO_PARAMETER = 0x313, - /// A Corim measurement structure described by [`IGVM_VHS_CORIM_MEASUREMENT`]. + /// A Corim document structure described by [`IGVM_VHS_CORIM_DOCUMENT`]. /// FIXME: should this be an init header to be early in the file? #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] - IGVM_VHT_CORIM_MEASUREMENT = 0x314, + IGVM_VHT_CORIM_DOCUMENT = 0x314, /// A Corim signature structure described by [`IGVM_VHS_CORIM_SIGNATURE`]. /// FIXME: should this be an init header to be early in the file? #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] @@ -1246,12 +1246,11 @@ pub enum VbsSigningAlgorithm { ECDSA_P384 = 0x1, } -/// A structure defining a CoRIM CBOR payload for a given platform. TODO: rename -/// to remove measurement? +/// A structure defining a CoRIM CBOR document for a given platform. /// -/// The payload described by this header is a CBOR CoRIM payload. There may only +/// 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 -/// wrapping this payload, see [`IGVM_VHS_CORIM_SIGNATURE`]. +/// for this document, see [`IGVM_VHS_CORIM_SIGNATURE`]. /// /// The CoRIM payload must adhere to the following specifications for each /// platform: @@ -1264,7 +1263,7 @@ pub enum VbsSigningAlgorithm { /// | ARM CCA | TBD | #[repr(C)] #[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] -struct IGVM_VHS_CORIM_MEASUREMENT { +struct IGVM_VHS_CORIM_DOCUMENT { /// Compatibility mask. pub compatibility_mask: u32, /// File offset for the CoRIM CBOR payload. @@ -1275,18 +1274,14 @@ struct IGVM_VHS_CORIM_MEASUREMENT { pub reserved: u32, } -/// This is a signed COSE_Sign1 structure wrapping a CoRIM CBOR payload for a -/// given platform. The payload measured by this CBOR is described the -/// corresponding [`IGVM_VHS_CORIM_MEASUREMENT`] structure. There cannot be this -/// structure without that one. +/// 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. There cannot be +/// this structure without that one. /// -/// Note that a user may choose to create a single CBOR containing this -/// COSE_Sign1 with the payload filled in by the other corim measurement -/// structure. -/// -/// The payload described by this header is a COSE_Sign1 structure described in -/// section 4.2 in RFC https://datatracker.ietf.org/doc/draft-ietf-rats-corim/, -/// which is a COSE_Sign1 structure with a CBOR corim payload. +/// 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)] struct IGVM_VHS_CORIM_SIGNATURE { From 928beb5fbb780cbeb02f59359b44e328b6b36ac8 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Wed, 11 Feb 2026 16:55:40 -0800 Subject: [PATCH 04/11] igvm: add corim header and basic validation --- igvm/src/lib.rs | 188 ++++++++++++++++++++++++++++++++++++++++++- igvm_defs/src/lib.rs | 8 +- 2 files changed, 188 insertions(+), 8 deletions(-) diff --git a/igvm/src/lib.rs b/igvm/src/lib.rs index 1398cce..7e72bf2 100644 --- a/igvm/src/lib.rs +++ b/igvm/src/lib.rs @@ -760,6 +760,17 @@ pub enum IgvmDirectiveHeader { signature: Box<[u8; 256]>, public_key: Box<[u8; 512]>, }, + 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 fmt::Display for IgvmDirectiveHeader { @@ -947,6 +958,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 { @@ -974,6 +991,8 @@ impl IgvmDirectiveHeader { IgvmDirectiveHeader::ErrorRange { .. } => size_of::(), IgvmDirectiveHeader::SnpIdBlock { .. } => size_of::(), IgvmDirectiveHeader::VbsMeasurement { .. } => size_of::(), + IgvmDirectiveHeader::CorimDocument { .. } => size_of::(), + IgvmDirectiveHeader::CorimSignature { .. } => size_of::(), }; align_8(size_of::() + additional) @@ -1021,6 +1040,12 @@ impl IgvmDirectiveHeader { IgvmDirectiveHeader::EnvironmentInfo(_) => { IgvmVariableHeaderType::IGVM_VHT_ENVIRONMENT_INFO_PARAMETER } + IgvmDirectiveHeader::CorimDocument { .. } => { + IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT + } + IgvmDirectiveHeader::CorimSignature { .. } => { + IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE + } } } @@ -1421,6 +1446,48 @@ impl IgvmDirectiveHeader { variable_headers, ) } + IgvmDirectiveHeader::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, + ); + } + IgvmDirectiveHeader::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(()) @@ -1470,6 +1537,12 @@ impl IgvmDirectiveHeader { VbsMeasurement { compatibility_mask, .. } => Some(*compatibility_mask), + CorimDocument { + compatibility_mask, .. + } => Some(*compatibility_mask), + CorimSignature { + compatibility_mask, .. + } => Some(*compatibility_mask), } } @@ -1518,6 +1591,12 @@ impl IgvmDirectiveHeader { VbsMeasurement { compatibility_mask, .. } => Some(compatibility_mask), + CorimDocument { + compatibility_mask, .. + } => Some(compatibility_mask), + CorimSignature { + compatibility_mask, .. + } => Some(compatibility_mask), } } @@ -1675,10 +1754,17 @@ impl IgvmDirectiveHeader { return Err(BinaryHeaderError::UnalignedAddress(*gpa)); } } - //TODO: validate SNP + // TODO: validate SNP IgvmDirectiveHeader::SnpIdBlock { .. } => {} - //TODO: validate VBS + // TODO: validate VBS IgvmDirectiveHeader::VbsMeasurement { .. } => {} + // TODO: validate CoRIM document has the minimum fields required + // described by the corresponding specification for that platform. + IgvmDirectiveHeader::CorimDocument { .. } => {} + // 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. + IgvmDirectiveHeader::CorimSignature { .. } => {} } Ok(()) @@ -2053,6 +2139,44 @@ impl IgvmDirectiveHeader { { IgvmDirectiveHeader::DeviceTree(read_header(&mut variable_headers)?) } + 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); + } + + IgvmDirectiveHeader::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); + } + + IgvmDirectiveHeader::CorimSignature { + compatibility_mask, + signature: extract_file_data(file_offset, size_bytes as usize)?, + } + } _ => return Err(BinaryHeaderError::InvalidVariableHeaderType), }; @@ -2534,6 +2658,14 @@ impl IgvmFile { } let mut parameter_areas: BTreeMap = BTreeMap::new(); + // 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]; + // TODO: validate parameter usage offset falls within parameter area size for header in directive_headers { @@ -2648,6 +2780,52 @@ impl IgvmFile { IgvmDirectiveHeader::ErrorRange { .. } => {} // TODO: Validate ErrorRange IgvmDirectiveHeader::SnpIdBlock { .. } => {} // TODO: Validate Snp IgvmDirectiveHeader::VbsMeasurement { .. } => {} // TODO: Validate Vbs + IgvmDirectiveHeader::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::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MultipleCorimDocuments(single_mask), + )); + } + corim_document_seen[mask_index] = true; + } + + // TODO: validate actual corim document is what is expected + // for the given platform. Requires parsing the CBOR + // payload. + } + IgvmDirectiveHeader::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::InvalidBinaryDirectiveHeader( + 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::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MissingCorimDocument(single_mask), + )); + } + } + + // TODO: Validate signature is correct for the given corim + // document. + } } } @@ -3346,7 +3524,9 @@ impl IgvmFile { | SnpIdBlock { .. } | VbsMeasurement { .. } | X64VbsVpContext { .. } - | AArch64VbsVpContext { .. } => {} + | AArch64VbsVpContext { .. } + | CorimDocument { .. } + | CorimSignature { .. } => {} ParameterArea { parameter_area_index, .. @@ -4687,5 +4867,5 @@ mod tests { ) } - // Test serialize and deserialize + // Test corim header serialization and basic validation } diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 215cb16..a24c739 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -1263,7 +1263,7 @@ pub enum VbsSigningAlgorithm { /// | ARM CCA | TBD | #[repr(C)] #[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes)] -struct IGVM_VHS_CORIM_DOCUMENT { +pub struct IGVM_VHS_CORIM_DOCUMENT { /// Compatibility mask. pub compatibility_mask: u32, /// File offset for the CoRIM CBOR payload. @@ -1276,15 +1276,15 @@ struct IGVM_VHS_CORIM_DOCUMENT { /// 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. There cannot be -/// this structure without that one. +/// 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)] -struct IGVM_VHS_CORIM_SIGNATURE { +pub struct IGVM_VHS_CORIM_SIGNATURE { /// Compatibility mask. pub compatibility_mask: u32, /// File offset for the COSE_Sign1 measurement payload. From 4a7b17b2ff9686b5bff79351d016e6cd2b9a871a Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Fri, 13 Feb 2026 11:11:23 -0800 Subject: [PATCH 05/11] corim: add basic corim unittests --- igvm/src/lib.rs | 253 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 1 deletion(-) diff --git a/igvm/src/lib.rs b/igvm/src/lib.rs index 7e72bf2..8a0feec 100644 --- a/igvm/src/lib.rs +++ b/igvm/src/lib.rs @@ -4867,5 +4867,256 @@ mod tests { ) } - // Test corim header serialization and basic validation + #[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 = IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: document.clone(), + }; + + test_variable_header( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + header, + file_data_offset, + IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT, + raw_header, + Some(document), + None, + ); + } + + #[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 = IgvmDirectiveHeader::CorimSignature { + compatibility_mask: 0x1, + signature: signature.clone(), + }; + + test_variable_header( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + header, + file_data_offset, + IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE, + raw_header, + Some(signature), + None, + ); + } + + mod corim { + use super::*; + + // TODO: when corim payload validation is added, these tests need to be + // updated to have real documents. + + fn validate(headers: &[IgvmDirectiveHeader]) -> Result<(), Error> { + IgvmFile::validate_directive_headers( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + headers, + DirectiveHeaderValidationInfo { + used_vp_idents: Vec::new(), + page_table_regions: Vec::new(), + }, + ) + } + + #[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![], + directive_headers: vec![ + new_page_data(0, 1, &data1), + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: corim_doc.clone(), + }, + IgvmDirectiveHeader::CorimSignature { + compatibility_mask: 0x1, + signature: corim_sig.clone(), + }, + ], + }; + + 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![ + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02, 0x03], + }, + IgvmDirectiveHeader::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![IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02, 0x03], + }]; + assert!(validate(&headers).is_ok()); + } + + #[test] + fn test_multiple_corim_documents_error() { + let headers = vec![ + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x03, 0x04], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MultipleCorimDocuments(0x1) + )) + )); + } + + #[test] + fn test_multiple_corim_documents_different_masks_valid() { + let headers = vec![ + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x2, + document: vec![0x03, 0x04], + }, + ]; + assert!(validate(&headers).is_ok()); + } + + #[test] + fn test_multiple_corim_signatures_error() { + let headers = vec![ + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmDirectiveHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x03, 0x04], + }, + IgvmDirectiveHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x05, 0x06], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MultipleCorimSignatures(0x1) + )) + )); + } + + #[test] + fn test_corim_signature_without_document_error() { + let headers = vec![IgvmDirectiveHeader::CorimSignature { + compatibility_mask: 0x1, + signature: vec![0x01, 0x02], + }]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MissingCorimDocument(0x1) + )) + )); + } + + #[test] + fn test_corim_signature_wrong_mask_missing_document() { + let headers = vec![ + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x01, 0x02], + }, + IgvmDirectiveHeader::CorimSignature { + compatibility_mask: 0x2, + signature: vec![0x03, 0x04], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MissingCorimDocument(0x2) + )) + )); + } + + #[test] + fn test_corim_combined_mask_duplicate_document() { + let headers = vec![ + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x3, + document: vec![0x01, 0x02], + }, + IgvmDirectiveHeader::CorimDocument { + compatibility_mask: 0x1, + document: vec![0x03, 0x04], + }, + ]; + assert!(matches!( + validate(&headers), + Err(Error::InvalidBinaryDirectiveHeader( + BinaryHeaderError::MultipleCorimDocuments(0x1) + )) + )); + } + } } From 4f58ded18864e7ae7cef0324eb77e36d7ae07030 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Wed, 18 Mar 2026 15:16:33 -0700 Subject: [PATCH 06/11] feedback: move header to init header, fix docs --- igvm_defs/src/lib.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index a24c739..6a1a253 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`]. @@ -341,14 +347,6 @@ pub enum IgvmVariableHeaderType { /// specified by a structure of type [`IGVM_VHS_PARAMETER`]. #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] IGVM_VHT_ENVIRONMENT_INFO_PARAMETER = 0x313, - /// A Corim document structure described by [`IGVM_VHS_CORIM_DOCUMENT`]. - /// FIXME: should this be an init header to be early in the file? - #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] - IGVM_VHT_CORIM_DOCUMENT = 0x314, - /// A Corim signature structure described by [`IGVM_VHS_CORIM_SIGNATURE`]. - /// FIXME: should this be an init header to be early in the file? - #[cfg_attr(docsrs, doc(cfg(feature = "unstable")))] - IGVM_VHT_CORIM_SIGNATURE = 0x315, } /// The range of header types for platform structures. @@ -1252,7 +1250,7 @@ pub enum VbsSigningAlgorithm { /// be one for a given platform. There may be an associated COSE_Sign1 structure /// for this document, see [`IGVM_VHS_CORIM_SIGNATURE`]. /// -/// The CoRIM payload must adhere to the following specifications for each +/// The CoRIM document must adhere to the following specifications for each /// platform: /// /// | Platform | Specification | From ef31f47642bf3ee4cbcdc31623452cc40cd7f219 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Mon, 23 Mar 2026 15:51:35 -0700 Subject: [PATCH 07/11] feedback: fix igvm code after moving to init headers --- igvm/src/c_api.rs | 2 +- igvm/src/lib.rs | 520 +++++++++++++++++++++++++--------------------- 2 files changed, 287 insertions(+), 235 deletions(-) diff --git a/igvm/src/c_api.rs b/igvm/src/c_api.rs index ec80682..aa2d92d 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 => { diff --git a/igvm/src/lib.rs b/igvm/src/lib.rs index 8a0feec..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(()) @@ -760,17 +897,6 @@ pub enum IgvmDirectiveHeader { signature: Box<[u8; 256]>, public_key: Box<[u8; 512]>, }, - 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 fmt::Display for IgvmDirectiveHeader { @@ -991,8 +1117,6 @@ impl IgvmDirectiveHeader { IgvmDirectiveHeader::ErrorRange { .. } => size_of::(), IgvmDirectiveHeader::SnpIdBlock { .. } => size_of::(), IgvmDirectiveHeader::VbsMeasurement { .. } => size_of::(), - IgvmDirectiveHeader::CorimDocument { .. } => size_of::(), - IgvmDirectiveHeader::CorimSignature { .. } => size_of::(), }; align_8(size_of::() + additional) @@ -1040,12 +1164,6 @@ impl IgvmDirectiveHeader { IgvmDirectiveHeader::EnvironmentInfo(_) => { IgvmVariableHeaderType::IGVM_VHT_ENVIRONMENT_INFO_PARAMETER } - IgvmDirectiveHeader::CorimDocument { .. } => { - IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT - } - IgvmDirectiveHeader::CorimSignature { .. } => { - IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE - } } } @@ -1446,48 +1564,6 @@ impl IgvmDirectiveHeader { variable_headers, ) } - IgvmDirectiveHeader::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, - ); - } - IgvmDirectiveHeader::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(()) @@ -1537,12 +1613,6 @@ impl IgvmDirectiveHeader { VbsMeasurement { compatibility_mask, .. } => Some(*compatibility_mask), - CorimDocument { - compatibility_mask, .. - } => Some(*compatibility_mask), - CorimSignature { - compatibility_mask, .. - } => Some(*compatibility_mask), } } @@ -1591,12 +1661,6 @@ impl IgvmDirectiveHeader { VbsMeasurement { compatibility_mask, .. } => Some(compatibility_mask), - CorimDocument { - compatibility_mask, .. - } => Some(compatibility_mask), - CorimSignature { - compatibility_mask, .. - } => Some(compatibility_mask), } } @@ -1758,13 +1822,6 @@ impl IgvmDirectiveHeader { IgvmDirectiveHeader::SnpIdBlock { .. } => {} // TODO: validate VBS IgvmDirectiveHeader::VbsMeasurement { .. } => {} - // TODO: validate CoRIM document has the minimum fields required - // described by the corresponding specification for that platform. - IgvmDirectiveHeader::CorimDocument { .. } => {} - // 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. - IgvmDirectiveHeader::CorimSignature { .. } => {} } Ok(()) @@ -2139,44 +2196,6 @@ impl IgvmDirectiveHeader { { IgvmDirectiveHeader::DeviceTree(read_header(&mut variable_headers)?) } - 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); - } - - IgvmDirectiveHeader::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); - } - - IgvmDirectiveHeader::CorimSignature { - compatibility_mask, - signature: extract_file_data(file_offset, size_bytes as usize)?, - } - } _ => return Err(BinaryHeaderError::InvalidVariableHeaderType), }; @@ -2547,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 @@ -2628,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), + )); + } + } + } } } @@ -2658,14 +2724,6 @@ impl IgvmFile { } let mut parameter_areas: BTreeMap = BTreeMap::new(); - // 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]; - // TODO: validate parameter usage offset falls within parameter area size for header in directive_headers { @@ -2780,52 +2838,6 @@ impl IgvmFile { IgvmDirectiveHeader::ErrorRange { .. } => {} // TODO: Validate ErrorRange IgvmDirectiveHeader::SnpIdBlock { .. } => {} // TODO: Validate Snp IgvmDirectiveHeader::VbsMeasurement { .. } => {} // TODO: Validate Vbs - IgvmDirectiveHeader::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::InvalidBinaryDirectiveHeader( - BinaryHeaderError::MultipleCorimDocuments(single_mask), - )); - } - corim_document_seen[mask_index] = true; - } - - // TODO: validate actual corim document is what is expected - // for the given platform. Requires parsing the CBOR - // payload. - } - IgvmDirectiveHeader::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::InvalidBinaryDirectiveHeader( - 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::InvalidBinaryDirectiveHeader( - BinaryHeaderError::MissingCorimDocument(single_mask), - )); - } - } - - // TODO: Validate signature is correct for the given corim - // document. - } } } @@ -2877,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 @@ -3123,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; @@ -3503,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), } } @@ -3524,9 +3545,7 @@ impl IgvmFile { | SnpIdBlock { .. } | VbsMeasurement { .. } | X64VbsVpContext { .. } - | AArch64VbsVpContext { .. } - | CorimDocument { .. } - | CorimSignature { .. } => {} + | AArch64VbsVpContext { .. } => {} ParameterArea { parameter_area_index, .. @@ -4285,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() { @@ -4879,22 +4945,17 @@ mod tests { reserved: 0, }; - let header = IgvmDirectiveHeader::CorimDocument { + let header = IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: document.clone(), }; - test_variable_header( - IgvmRevision::V2 { - arch: Arch::X64, - page_size: PAGE_SIZE_4K as u32, - }, + test_init_variable_header( header, file_data_offset, IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT, raw_header, Some(document), - None, ); } @@ -4910,22 +4971,17 @@ mod tests { reserved: 0, }; - let header = IgvmDirectiveHeader::CorimSignature { + let header = IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x1, signature: signature.clone(), }; - test_variable_header( - IgvmRevision::V2 { - arch: Arch::X64, - page_size: PAGE_SIZE_4K as u32, - }, + test_init_variable_header( header, file_data_offset, IgvmVariableHeaderType::IGVM_VHT_CORIM_SIGNATURE, raw_header, Some(signature), - None, ); } @@ -4935,18 +4991,15 @@ mod tests { // TODO: when corim payload validation is added, these tests need to be // updated to have real documents. - fn validate(headers: &[IgvmDirectiveHeader]) -> Result<(), Error> { - IgvmFile::validate_directive_headers( + fn validate(headers: &[IgvmInitializationHeader]) -> Result<(), Error> { + IgvmFile::validate_initialization_headers( IgvmRevision::V2 { arch: Arch::X64, page_size: PAGE_SIZE_4K as u32, }, headers, - DirectiveHeaderValidationInfo { - used_vp_idents: Vec::new(), - page_table_regions: Vec::new(), - }, ) + .map(|_| ()) } #[test] @@ -4961,18 +5014,17 @@ mod tests { page_size: PAGE_SIZE_4K as u32, }, platform_headers: vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], - initialization_headers: vec![], - directive_headers: vec![ - new_page_data(0, 1, &data1), - IgvmDirectiveHeader::CorimDocument { + initialization_headers: vec![ + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: corim_doc.clone(), }, - IgvmDirectiveHeader::CorimSignature { + IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x1, signature: corim_sig.clone(), }, ], + directive_headers: vec![new_page_data(0, 1, &data1)], }; let mut binary_file = Vec::new(); @@ -4985,11 +5037,11 @@ mod tests { #[test] fn test_corim_document_and_signature_valid() { let headers = vec![ - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x01, 0x02, 0x03], }, - IgvmDirectiveHeader::CorimSignature { + IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x1, signature: vec![0x04, 0x05, 0x06], }, @@ -4999,7 +5051,7 @@ mod tests { #[test] fn test_corim_document_without_signature_valid() { - let headers = vec![IgvmDirectiveHeader::CorimDocument { + let headers = vec![IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x01, 0x02, 0x03], }]; @@ -5009,18 +5061,18 @@ mod tests { #[test] fn test_multiple_corim_documents_error() { let headers = vec![ - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x01, 0x02], }, - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x03, 0x04], }, ]; assert!(matches!( validate(&headers), - Err(Error::InvalidBinaryDirectiveHeader( + Err(Error::InvalidBinaryInitializationHeader( BinaryHeaderError::MultipleCorimDocuments(0x1) )) )); @@ -5029,11 +5081,11 @@ mod tests { #[test] fn test_multiple_corim_documents_different_masks_valid() { let headers = vec![ - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x01, 0x02], }, - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x2, document: vec![0x03, 0x04], }, @@ -5044,22 +5096,22 @@ mod tests { #[test] fn test_multiple_corim_signatures_error() { let headers = vec![ - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x01, 0x02], }, - IgvmDirectiveHeader::CorimSignature { + IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x1, signature: vec![0x03, 0x04], }, - IgvmDirectiveHeader::CorimSignature { + IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x1, signature: vec![0x05, 0x06], }, ]; assert!(matches!( validate(&headers), - Err(Error::InvalidBinaryDirectiveHeader( + Err(Error::InvalidBinaryInitializationHeader( BinaryHeaderError::MultipleCorimSignatures(0x1) )) )); @@ -5067,13 +5119,13 @@ mod tests { #[test] fn test_corim_signature_without_document_error() { - let headers = vec![IgvmDirectiveHeader::CorimSignature { + let headers = vec![IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x1, signature: vec![0x01, 0x02], }]; assert!(matches!( validate(&headers), - Err(Error::InvalidBinaryDirectiveHeader( + Err(Error::InvalidBinaryInitializationHeader( BinaryHeaderError::MissingCorimDocument(0x1) )) )); @@ -5082,18 +5134,18 @@ mod tests { #[test] fn test_corim_signature_wrong_mask_missing_document() { let headers = vec![ - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x01, 0x02], }, - IgvmDirectiveHeader::CorimSignature { + IgvmInitializationHeader::CorimSignature { compatibility_mask: 0x2, signature: vec![0x03, 0x04], }, ]; assert!(matches!( validate(&headers), - Err(Error::InvalidBinaryDirectiveHeader( + Err(Error::InvalidBinaryInitializationHeader( BinaryHeaderError::MissingCorimDocument(0x2) )) )); @@ -5102,18 +5154,18 @@ mod tests { #[test] fn test_corim_combined_mask_duplicate_document() { let headers = vec![ - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x3, document: vec![0x01, 0x02], }, - IgvmDirectiveHeader::CorimDocument { + IgvmInitializationHeader::CorimDocument { compatibility_mask: 0x1, document: vec![0x03, 0x04], }, ]; assert!(matches!( validate(&headers), - Err(Error::InvalidBinaryDirectiveHeader( + Err(Error::InvalidBinaryInitializationHeader( BinaryHeaderError::MultipleCorimDocuments(0x1) )) )); From f90720431f9e24dee6d0016109c855a69b99c4bc Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Mon, 23 Mar 2026 17:03:59 -0700 Subject: [PATCH 08/11] c_api: fix get_header_data to correctly return header data for new init corim headers --- igvm/src/c_api.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/igvm/src/c_api.rs b/igvm/src/c_api.rs index aa2d92d..afdcdb4 100644 --- a/igvm/src/c_api.rs +++ b/igvm/src/c_api.rs @@ -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(); From f0a2058fdce159a6f97bf148915fb8366770171f Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Mon, 23 Mar 2026 17:07:38 -0700 Subject: [PATCH 09/11] feedback: fix igvm_defs markdown table --- igvm_defs/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 6a1a253..7907207 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -1253,12 +1253,12 @@ pub enum VbsSigningAlgorithm { /// 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 | +/// | 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 { From 07fa6dfe4938a005d5525776623bb7c3a4ad8239 Mon Sep 17 00:00:00 2001 From: Chris Oo Date: Mon, 23 Mar 2026 17:22:16 -0700 Subject: [PATCH 10/11] feedback: delete extra space --- igvm_defs/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index 7907207..fd7291b 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -1272,7 +1272,7 @@ pub struct IGVM_VHS_CORIM_DOCUMENT { pub reserved: u32, } -/// This structure descibres a COSE_Sign1 structure for a detached CoRIM CBOR +/// 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. From 5ee1b986906dce69a04a8106c8be368cd46159b9 Mon Sep 17 00:00:00 2001 From: Ming-Wei Shih Date: Sat, 11 Apr 2026 00:36:58 +0000 Subject: [PATCH 11/11] igvm: add CoRIM launch endorsement module Add the corim module that generates and validates CoRIM (Concise Reference Integrity Manifest) documents for IGVM launch endorsements per draft-ietf-rats-corim-10. The crate produces CBOR-encoded, tag-501-wrapped CoRIM documents containing: - A reference-values triple with the launch measurement digest - A conditional-endorsement-series triple mapping the digest to an exact SVN (#6.552) Supported platforms (via IgvmPlatformType): - Intel TDX (SHA-384, mkey "MRTD") - AMD SEV-SNP (SHA-384, mkey "MEASUREMENT") - Microsoft VBS (SHA-256, mkey "MEASUREMENT") Key design decisions: - Self-contained CBOR builder using ciborium::Value (no full corim crate dependency) - Strict profile enforcement: profile URI required, exactly one CoMID, CES required, only exact SVN, tag-id verified via UUIDv5 - Profile URI: tag:microsoft.com,2026:igvm-launch-endorsement/v1 Signed-off-by: Ming-Wei Shih --- Cargo.toml | 3 + igvm/Cargo.toml | 4 + igvm/src/c_api.rs | 8 + igvm/src/corim/launch_endorsement/builder.rs | 242 ++++++ igvm/src/corim/launch_endorsement/mod.rs | 356 ++++++++ igvm/src/corim/launch_endorsement/profile.rs | 87 ++ igvm/src/corim/mod.rs | 20 + igvm/src/lib.rs | 81 +- igvm/src/measurement/mod.rs | 41 + igvm/src/measurement/snp.rs | 242 ++++++ igvm/src/measurement/tdx.rs | 157 ++++ igvm/src/measurement/vbs.rs | 378 +++++++++ igvm/src/serializer.rs | 818 +++++++++++++++++++ 13 files changed, 2436 insertions(+), 1 deletion(-) create mode 100644 igvm/src/corim/launch_endorsement/builder.rs create mode 100644 igvm/src/corim/launch_endorsement/mod.rs create mode 100644 igvm/src/corim/launch_endorsement/profile.rs create mode 100644 igvm/src/corim/mod.rs create mode 100644 igvm/src/measurement/mod.rs create mode 100644 igvm/src/measurement/snp.rs create mode 100644 igvm/src/measurement/tdx.rs create mode 100644 igvm/src/measurement/vbs.rs create mode 100644 igvm/src/serializer.rs diff --git a/Cargo.toml b/Cargo.toml index d835130..919334d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,14 @@ igvm = { path = "igvm", version = "0.4.0" } anyhow = "1.0" bitfield-struct = "0.12" +corim = "0.1" crc32fast = { version = "1.3.2", default-features = false } hex = { version = "0.4", default-features = false } open-enum = "0.5.2" range_map_vec = "0.2.0" +sha2 = "0.10" static_assertions = "1.1" thiserror = "2.0" tracing = "0.1" +uuid = { version = "1", features = ["v5"] } zerocopy = { version = "0.8.14", features = ["derive"] } diff --git a/igvm/Cargo.toml b/igvm/Cargo.toml index 17a76c3..42d3dd5 100644 --- a/igvm/Cargo.toml +++ b/igvm/Cargo.toml @@ -40,12 +40,15 @@ crate-type = ["staticlib", "rlib"] igvm_defs = { workspace = true, features = ["unstable"] } bitfield-struct.workspace = true +corim = { workspace = true, optional = true } range_map_vec.workspace = true crc32fast.workspace = true hex = { workspace = true, features = ["alloc"] } open-enum.workspace = true +sha2 = { workspace = true, optional = true } thiserror.workspace = true tracing.workspace = true +uuid = { workspace = true, optional = true } zerocopy = { workspace = true, features = ["alloc"] } static_assertions.workspace = true @@ -53,3 +56,4 @@ static_assertions.workspace = true default = [] capi = ["igvm-c"] igvm-c = [] # Add exports that allow the library to be used from C +corim = ["dep:corim", "dep:uuid", "dep:sha2"] # CoRIM launch endorsement support diff --git a/igvm/src/c_api.rs b/igvm/src/c_api.rs index afdcdb4..213e280 100644 --- a/igvm/src/c_api.rs +++ b/igvm/src/c_api.rs @@ -60,6 +60,10 @@ pub enum IgvmResult { IGVMAPI_UNSUPPORTED_PAGE_SIZE = -25, IGVMAPI_INVALID_FIXED_HEADER_ARCH = -26, IGVMAPI_MERGE_REVISION = -27, + #[cfg(feature = "corim")] + IGVMAPI_CORIM_GENERATION = -28, + #[cfg(feature = "corim")] + IGVMAPI_MEASUREMENT_FAILED = -29, } type IgvmHandle = i32; @@ -164,6 +168,10 @@ fn translate_error(error: Error) -> IgvmResult { Error::UnsupportedPageSize(_) => IgvmResult::IGVMAPI_UNSUPPORTED_PAGE_SIZE, Error::InvalidFixedHeaderArch(_) => IgvmResult::IGVMAPI_INVALID_FIXED_HEADER_ARCH, Error::MergeRevision => IgvmResult::IGVMAPI_MERGE_REVISION, + #[cfg(feature = "corim")] + Error::CorimGeneration(_) => IgvmResult::IGVMAPI_CORIM_GENERATION, + #[cfg(feature = "corim")] + Error::MeasurementFailed(_) => IgvmResult::IGVMAPI_MEASUREMENT_FAILED, } } diff --git a/igvm/src/corim/launch_endorsement/builder.rs b/igvm/src/corim/launch_endorsement/builder.rs new file mode 100644 index 0000000..641753e --- /dev/null +++ b/igvm/src/corim/launch_endorsement/builder.rs @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! CoRIM launch-endorsement builder. +//! +//! Uses the [`corim`] crate's typed builder API instead of manual CBOR +//! tree construction. + +use corim::builder::ComidBuilder; +use corim::builder::CorimBuilder; +use corim::types::common::MeasuredElement; +use corim::types::common::TagIdChoice; +use corim::types::corim::CorimId; +use corim::types::corim::ProfileChoice; +use corim::types::environment::ClassMap; +use corim::types::environment::EnvironmentMap; +use corim::types::measurement::Digest; +use corim::types::measurement::MeasurementMap; +use corim::types::measurement::MeasurementValuesMap; +use corim::types::measurement::SvnChoice; +use corim::types::triples::CesCondition; +use corim::types::triples::ConditionalEndorsementSeriesTriple; +use corim::types::triples::ConditionalSeriesRecord; +use corim::types::triples::ReferenceTriple; +use igvm_defs::IgvmPlatformType; +use uuid::Uuid; + +use super::platform_info; +use super::profile::PROFILE_URI; +use super::Error; +use super::TAG_ID_NAMESPACE; + +// `ResolvedMeasurement` is intentionally profile-specific. When a second +// profile arrives, common shape — e.g., `(mkey, digest_alg, digest)` — +// can be promoted to `crate::corim` if duplication justifies it. + +/// A measurement resolved by the serializer, ready for CBOR encoding. +#[derive(Debug, Clone)] +pub(crate) struct ResolvedMeasurement { + pub mkey: String, + pub digest_alg: i64, + pub digest: Vec, +} + +/// Build a complete CoRIM launch endorsement as tag-501-wrapped CBOR bytes. +/// +/// Emits a single reference-values triple containing the measurement, +/// plus a single CES triple that selects on that measurement and +/// endorses `svn`. +pub(crate) fn build_corim_bytes( + platform: IgvmPlatformType, + measurement: &ResolvedMeasurement, + svn: u64, +) -> Result, Error> { + let info = platform_info(platform).expect("platform validated by LaunchEndorsement"); + + let env = EnvironmentMap { + class: Some(ClassMap { + class_id: None, + vendor: Some(info.vendor.into()), + model: Some(info.model.into()), + layer: None, + index: None, + }), + instance: None, + group: None, + }; + + let tag_id = Uuid::new_v5( + &TAG_ID_NAMESPACE, + format!("{}/{}", info.vendor, info.model).as_bytes(), + ) + .to_string(); + + let ref_meas = build_measurement_map(measurement); + + // CES selection: same measurement-map as the reference value. + let ces_selection = build_measurement_map(measurement); + + let ces_addition = MeasurementMap { + mkey: None, + mval: MeasurementValuesMap { + svn: Some(SvnChoice::ExactValue(svn)), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + }; + + let ces_triple = ConditionalEndorsementSeriesTriple::new( + CesCondition { + environment: env.clone(), + claims_list: Vec::new(), + authorized_by: None, + }, + vec![ConditionalSeriesRecord::new( + vec![ces_selection], + vec![ces_addition], + )], + ); + + // Build CoMID + let comid = ComidBuilder::new(TagIdChoice::Text(tag_id)) + .add_reference_triple(ReferenceTriple::new(env, vec![ref_meas])) + .add_conditional_endorsement_series(ces_triple) + .build() + .map_err(|e| Error::Build(Box::new(e)))?; + + // Build CoRIM with profile URI + let corim_id = format!("{}/{}/launch-endorsement", info.vendor, info.model); + CorimBuilder::new(CorimId::Text(corim_id)) + .set_profile(ProfileChoice::Uri(PROFILE_URI.into())) + .add_comid_tag(comid) + .map_err(|e| Error::Build(Box::new(e)))? + .build_bytes() + .map_err(|e| Error::Build(Box::new(e))) +} + +fn build_measurement_map(m: &ResolvedMeasurement) -> MeasurementMap { + MeasurementMap { + mkey: Some(MeasuredElement::Text(m.mkey.clone())), + mval: MeasurementValuesMap { + digests: Some(vec![Digest::new(m.digest_alg, m.digest.clone())]), + ..MeasurementValuesMap::default() + }, + authorized_by: None, + } +} + +#[cfg(test)] +mod tests { + use igvm_defs::IgvmPlatformType; + + use crate::corim::launch_endorsement::Error; + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + + fn build_and_decode( + le: LaunchEndorsement, + ) -> ( + corim::types::corim::CorimMap, + Vec, + ) { + // Tests use a fixed digest in place of the IGVM file's + // auto-computed launch measurement. + let platform = le.platform(); + let kind = *le.measurement_kinds().iter().next().unwrap(); + let (mkey, alg, len) = super::super::measurement_info(platform, kind).unwrap(); + let measurement = super::ResolvedMeasurement { + mkey: mkey.to_string(), + digest_alg: alg, + digest: vec![0xAA; len], + }; + + let svn = le.triples()[0].svn(); + + let bytes = super::build_corim_bytes(platform, &measurement, svn).unwrap(); + corim::validate::decode_and_validate(&bytes).unwrap() + } + + #[test] + fn amd_sev_snp_round_trip() { + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + le.endorse(1) + .with(MeasurementKind::Launch) + .unwrap() + .finish() + .unwrap(); + + let (corim, comids) = build_and_decode(le); + assert_eq!(corim.id.to_string(), "AMD/SEV-SNP/launch-endorsement"); + assert_eq!(comids.len(), 1); + let tag_id = comids[0].tag_identity.tag_id.to_string(); + assert_eq!(tag_id, "77e8061e-4634-5e53-a848-d1d09e996843"); + } + + #[test] + fn intel_tdx_round_trip() { + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::TDX).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + le.endorse(5) + .with(MeasurementKind::Launch) + .unwrap() + .finish() + .unwrap(); + + let (corim, _) = build_and_decode(le); + assert_eq!(corim.id.to_string(), "Intel/TDX/launch-endorsement"); + } + + #[test] + fn microsoft_vbs_round_trip() { + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::VSM_ISOLATION).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + le.endorse(2) + .with(MeasurementKind::Launch) + .unwrap() + .finish() + .unwrap(); + + let (corim, _) = build_and_decode(le); + assert_eq!(corim.id.to_string(), "Microsoft/VBS/launch-endorsement"); + } + + #[test] + fn unsupported_platform_rejected() { + let err = LaunchEndorsement::for_platform(IgvmPlatformType::NATIVE).unwrap_err(); + assert!( + matches!(err, Error::UnsupportedPlatform(IgvmPlatformType::NATIVE)), + "got: {err:?}" + ); + } + + #[test] + fn select_unpopulated_kind_rejected() { + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + let err = le.endorse(1).with(MeasurementKind::Launch).unwrap_err(); + assert!(matches!(err, Error::MeasurementNotPopulated { .. })); + } + + #[test] + fn duplicate_selection_rejected() { + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + let err = le + .endorse(1) + .with(MeasurementKind::Launch) + .unwrap() + .with(MeasurementKind::Launch) + .unwrap_err(); + assert!(matches!(err, Error::DuplicateSelection { .. })); + } + + #[test] + fn empty_selection_rejected() { + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + let err = le.endorse(1).finish().unwrap_err(); + assert!(matches!(err, Error::EmptySelection)); + } +} diff --git a/igvm/src/corim/launch_endorsement/mod.rs b/igvm/src/corim/launch_endorsement/mod.rs new file mode 100644 index 0000000..604447a --- /dev/null +++ b/igvm/src/corim/launch_endorsement/mod.rs @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! Launch endorsement CoRIM profile. +//! +//! This module implements the IGVM launch endorsement CoRIM profile +//! (`tag:microsoft.com,2026:igvm-launch-endorsement/v1`), which produces +//! CoRIM documents containing a launch measurement reference value and +//! an SVN endorsement for supported CVM platforms. +//! +//! # Two-stage builder +//! +//! The user-facing API is a two-stage builder: +//! +//! 1. **Stage 1 — populate measurements.** Construct a [`LaunchEndorsement`] +//! via [`LaunchEndorsement::for_platform`], then mark each profile-defined +//! measurement as populated via +//! [`set_measurement`](LaunchEndorsement::set_measurement). The digest +//! bytes are taken from the IGVM file's auto-computed launch measurement +//! at serialization time. Every populated measurement becomes a +//! reference-value in the CoRIM. +//! +//! 2. **Stage 2 — define endorsement policy.** Call +//! [`endorse`](LaunchEndorsement::endorse) to start a [`CesTripleBuilder`] +//! for a given SVN, then select which populated measurements participate +//! in the CES triple via [`with`](CesTripleBuilder::with). Finalize with +//! [`finish`](CesTripleBuilder::finish) to obtain a [`CesTriple`]. +//! +//! Finally, call [`build`](LaunchEndorsement::build) to consume the +//! endorsement and produce a [`CorimTemplate`](crate::CorimTemplate) ready +//! for [`IgvmSerializer::add_corim`](crate::IgvmSerializer::add_corim). +//! +//! # Future: caller-supplied digests +//! +//! When future CoRIM profiles need caller-supplied digest bytes (e.g., for +//! TDX RTMRs or runtime-extended measurements), a `DigestSource`-like type +//! should be introduced at the [`crate::corim`] module level so it can be +//! shared across profiles, rather than re-introduced here. + +pub(crate) mod builder; +pub mod profile; + +use std::collections::HashSet; + +pub use igvm_defs::IgvmPlatformType; + +use crate::CorimTemplate; + +/// Fixed namespace UUID for deterministic CoMID tag-id derivation. +/// +/// `tag-id = UUIDv5(TAG_ID_NAMESPACE, "{vendor}/{model}")` +pub const TAG_ID_NAMESPACE: uuid::Uuid = uuid::Uuid::from_bytes([ + 0x85, 0xf3, 0xf1, 0xc2, 0x22, 0xa8, 0x44, 0x1e, 0xa1, 0xb9, 0xbc, 0xcf, 0xb6, 0x3e, 0xd5, 0xf7, +]); + +// ── Profile catalog ──────────────────────────────────────────────────── + +/// Internal record describing a platform's profile-defined measurement layout. +pub(crate) struct PlatformInfo { + pub vendor: &'static str, + pub model: &'static str, + pub mkey: &'static str, + pub digest_alg: i64, + pub digest_len: usize, +} + +/// Named Information Hash Algorithm ID for SHA-256 (RFC 6920). +const NI_SHA256: i64 = 1; +/// Named Information Hash Algorithm ID for SHA-384 (RFC 6920). +const NI_SHA384: i64 = 7; + +/// Canonical list of supported platforms. +pub(crate) fn known_platforms() -> &'static [PlatformInfo] { + &[ + PlatformInfo { + vendor: "Intel", + model: "TDX", + mkey: "MRTD", + digest_alg: NI_SHA384, + digest_len: 48, + }, + PlatformInfo { + vendor: "AMD", + model: "SEV-SNP", + mkey: "MEASUREMENT", + digest_alg: NI_SHA384, + digest_len: 48, + }, + PlatformInfo { + vendor: "Microsoft", + model: "VBS", + mkey: "MEASUREMENT", + digest_alg: NI_SHA256, + digest_len: 32, + }, + ] +} + +fn platform_info(platform: IgvmPlatformType) -> Option<&'static PlatformInfo> { + let (vendor, model) = match platform { + IgvmPlatformType::TDX => ("Intel", "TDX"), + IgvmPlatformType::SEV_SNP => ("AMD", "SEV-SNP"), + IgvmPlatformType::VSM_ISOLATION => ("Microsoft", "VBS"), + _ => return None, + }; + known_platforms() + .iter() + .find(|p| p.vendor == vendor && p.model == model) +} + +/// Look up the `(mkey, digest_alg, digest_len)` tuple for a profile-defined +/// measurement on the given platform. +/// +/// Returns `None` if the platform is not supported by this profile, or if +/// the measurement kind has no mapping for that platform. +pub fn measurement_info( + platform: IgvmPlatformType, + kind: MeasurementKind, +) -> Option<(&'static str, i64, usize)> { + let info = platform_info(platform)?; + match kind { + MeasurementKind::Launch => Some((info.mkey, info.digest_alg, info.digest_len)), + } +} + +// ── Public types ─────────────────────────────────────────────────────── + +/// Identifies a profile-defined measurement. +/// +/// The exact CBOR `mkey` text and hash algorithm are determined by the +/// profile and the target platform. Today only [`Self::Launch`] is +/// supported; future variants (e.g., `Rtmr(u8)` for TDX) can be added +/// without breaking existing callers. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum MeasurementKind { + /// The platform's primary launch measurement. + /// + /// - **TDX**: MRTD (SHA-384) + /// - **SEV-SNP**: launch digest (SHA-384) + /// - **VBS**: boot measurement digest (SHA-256) + Launch, +} + +/// Errors from launch endorsement generation. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// The platform type is not supported for CoRIM launch endorsements. + #[error( + "unsupported platform type {0:?}: only SEV_SNP, TDX, and \ + VSM_ISOLATION are supported for CoRIM launch endorsements" + )] + UnsupportedPlatform(IgvmPlatformType), + /// The measurement kind is not defined for this platform. + #[error("measurement {kind:?} is not defined for platform {platform:?}")] + UnsupportedMeasurement { + /// The platform that was queried. + platform: IgvmPlatformType, + /// The measurement kind that was rejected. + kind: MeasurementKind, + }, + /// A CES triple referenced a measurement that was not populated in + /// stage 1 (via [`LaunchEndorsement::set_measurement`]). + #[error( + "measurement {kind:?} is not populated; call \ + LaunchEndorsement::set_measurement first" + )] + MeasurementNotPopulated { + /// The kind that was referenced but not populated. + kind: MeasurementKind, + }, + /// The same measurement kind was selected twice in a single CES triple. + #[error("measurement {kind:?} already selected in this CES triple")] + DuplicateSelection { + /// The duplicated kind. + kind: MeasurementKind, + }, + /// A CES triple was finalized with an empty selection. + #[error("CES triple must select at least one measurement")] + EmptySelection, + /// CoRIM building or encoding failed. + #[error("CoRIM build failed")] + Build(#[source] Box), +} + +// ── Stage 1: LaunchEndorsement ───────────────────────────────────────── + +/// Profile-driven endorsement under construction. +/// +/// Constructed via [`for_platform`](Self::for_platform). Populate +/// measurements with [`set_measurement`](Self::set_measurement), then +/// build CES triples via [`endorse`](Self::endorse). Finalize with +/// [`build`](Self::build) to produce a [`CorimTemplate`]. +#[derive(Debug, Clone)] +pub struct LaunchEndorsement { + platform: IgvmPlatformType, + measurements: HashSet, + triples: Vec, +} + +impl LaunchEndorsement { + /// Start a new launch endorsement for the given platform. + /// + /// Returns [`Error::UnsupportedPlatform`] if the platform is not one + /// the profile supports. + pub fn for_platform(platform: IgvmPlatformType) -> Result { + if platform_info(platform).is_none() { + return Err(Error::UnsupportedPlatform(platform)); + } + Ok(Self { + platform, + measurements: HashSet::new(), + triples: Vec::new(), + }) + } + + /// The platform this endorsement targets. + pub fn platform(&self) -> IgvmPlatformType { + self.platform + } + + /// Mark a profile-defined measurement as populated. + /// + /// Each call is idempotent for the same `kind`. All populated + /// measurements are emitted as reference-values in the final CoRIM + /// document, with digest bytes taken from the IGVM file's + /// auto-computed launch measurement at serialization time. + /// + /// # Errors + /// + /// Returns [`Error::UnsupportedMeasurement`] if `kind` is not defined + /// for the platform. + pub fn set_measurement(&mut self, kind: MeasurementKind) -> Result<&mut Self, Error> { + if measurement_info(self.platform, kind).is_none() { + return Err(Error::UnsupportedMeasurement { + platform: self.platform, + kind, + }); + } + self.measurements.insert(kind); + Ok(self) + } + + /// Returns the measurement kinds populated so far. + pub fn populated_measurements(&self) -> impl Iterator + '_ { + self.measurements.iter().copied() + } + + /// Returns `true` if `kind` has been populated. + pub fn is_populated(&self, kind: MeasurementKind) -> bool { + self.measurements.contains(&kind) + } + + /// Start a Stage-2 CES triple builder that endorses `svn` when its + /// selected measurements all match. + pub fn endorse(&mut self, svn: u64) -> CesTripleBuilder<'_> { + CesTripleBuilder { + endorsement: self, + svn, + selected: Vec::new(), + } + } + + /// Returns the CES triples accumulated so far. + pub fn triples(&self) -> &[CesTriple] { + &self.triples + } + + /// Consume this endorsement and wrap it in a [`CorimTemplate`] ready + /// for [`IgvmSerializer::add_corim`](crate::IgvmSerializer::add_corim). + pub fn build(self) -> CorimTemplate { + CorimTemplate::LaunchEndorsement(self) + } + + /// Private accessor used by the serializer to enumerate populated + /// measurement kinds. + pub(crate) fn measurement_kinds(&self) -> &HashSet { + &self.measurements + } +} + +// ── Stage 2: CesTripleBuilder ────────────────────────────────────────── + +/// Builder for a single conditional-endorsement-series triple. +/// +/// Created by [`LaunchEndorsement::endorse`]. The lifetime ties the +/// builder to its parent endorsement so that [`with`](Self::with) can +/// validate selected measurements against the populated catalog. +#[derive(Debug)] +pub struct CesTripleBuilder<'a> { + endorsement: &'a mut LaunchEndorsement, + svn: u64, + selected: Vec, +} + +impl CesTripleBuilder<'_> { + /// Add a populated measurement to this CES triple's selection. + /// + /// # Errors + /// + /// - [`Error::MeasurementNotPopulated`] if `kind` was not populated + /// in stage 1 via [`LaunchEndorsement::set_measurement`]. + /// - [`Error::DuplicateSelection`] if `kind` is already in this + /// triple's selection. + pub fn with(mut self, kind: MeasurementKind) -> Result { + if !self.endorsement.measurements.contains(&kind) { + return Err(Error::MeasurementNotPopulated { kind }); + } + if self.selected.contains(&kind) { + return Err(Error::DuplicateSelection { kind }); + } + self.selected.push(kind); + Ok(self) + } + + /// Finalize this CES triple and append it to the parent endorsement. + /// + /// Returns [`Error::EmptySelection`] if no measurements were selected. + pub fn finish(self) -> Result<(), Error> { + if self.selected.is_empty() { + return Err(Error::EmptySelection); + } + self.endorsement.triples.push(CesTriple { + svn: self.svn, + selected: self.selected, + }); + Ok(()) + } +} + +// ── Finalized CES triple record ──────────────────────────────────────── + +/// A finalized conditional-endorsement-series triple. +/// +/// Produced by [`CesTripleBuilder::finish`] and owned by its parent +/// [`LaunchEndorsement`]. +#[derive(Debug, Clone)] +pub struct CesTriple { + svn: u64, + selected: Vec, +} + +impl CesTriple { + /// The SVN this triple endorses. + pub fn svn(&self) -> u64 { + self.svn + } + + /// The measurement kinds selected for this CES triple, in the order + /// they were added. + pub fn selected_measurements(&self) -> &[MeasurementKind] { + &self.selected + } +} diff --git a/igvm/src/corim/launch_endorsement/profile.rs b/igvm/src/corim/launch_endorsement/profile.rs new file mode 100644 index 0000000..c5cb617 --- /dev/null +++ b/igvm/src/corim/launch_endorsement/profile.rs @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! IGVM Launch Endorsement CoRIM Profile. +//! +//! This module defines the CoRIM profile for IGVM launch endorsements per +//! draft-ietf-rats-corim-10 §4.1.4. A profile constrains the base CoRIM +//! CDDL to a specific use case without changing the schema. +//! +//! # Profile URI +//! +//! ```text +//! tag:microsoft.com,2026:igvm-launch-endorsement/v1 +//! ``` +//! +//! # Semantics +//! +//! A CoRIM conforming to this profile carries exactly **one CoMID tag** with +//! the following structure: +//! +//! ## Environment (`class-map`) +//! +//! The Target Environment is identified by `vendor` (key 1) and `model` +//! (key 2) in the `class-map`. No `instance` or `group` is used — this is +//! a class-level endorsement that applies to all instances of the platform. +//! +//! Registered vendor/model pairs: +//! +//! | IgvmPlatformType | Vendor | Model | Digest Algorithm | Digest Length | +//! |------------------|---------------|-------------|------------------|---------------| +//! | TDX | `"Intel"` | `"TDX"` | SHA-384 (7) | 48 bytes | +//! | SEV-SNP | `"AMD"` | `"SEV-SNP"` | SHA-384 (7) | 48 bytes | +//! | VSM_ISOLATION | `"Microsoft"` | `"VBS"` | SHA-256 (1) | 32 bytes | +//! +//! ## Tag Identity (`tag-identity-map`) +//! +//! The `tag-id` is derived deterministically via UUIDv5 and encoded as a +//! **lowercase** hyphenated string per RFC 9562 §4: +//! +//! ```text +//! tag-id = lowercase(UUIDv5("85f3f1c2-22a8-441e-a1b9-bccfb63ed5f7", "{vendor}/{model}")) +//! ``` +//! +//! Validators MUST compare tag-ids **case-insensitively** for interoperability. +//! +//! ## Triples +//! +//! ### Required: `reference-triples` (key 0) +//! +//! Exactly one `reference-triple-record` containing: +//! - `environment`: `{ class: { vendor, model } }` +//! - `measurements`: one `measurement-map` with: +//! - `mkey`: text string identifying the evidence field (e.g., `"MRTD"` for TDX, `"MEASUREMENT"` for SEV-SNP and VBS) +//! - `mval.digests`: one `[alg-id, hash-bytes]` pair +//! +//! ### Required: `conditional-endorsement-series-triples` (key 8) +//! +//! Exactly one `conditional-endorsement-series-triple-record` containing: +//! - `condition`: the same environment, with an empty `claims-list` +//! - `series`: one entry mapping the digest to an exact SVN (`#6.552(uint)`) +//! +//! ## Constraints +//! +//! - The CoRIM MUST include the profile URI (`corim-map` key 3) set to +//! [`PROFILE_URI`]. Documents without the profile field MUST be rejected. +//! - The CoRIM MUST contain exactly one CoMID tag (`#6.506`). +//! - The CoMID `tag-id` MUST equal the lowercase string form of +//! `UUIDv5(TAG_ID_NAMESPACE, "{vendor}/{model}")` per RFC 9562 §4. +//! Validators MUST compare tag-ids case-insensitively. +//! - The CoMID MUST contain both `reference-triples` and +//! `conditional-endorsement-series-triples`. +//! - Only exact SVN (`#6.552`) is permitted. Minimum SVN (`#6.553`) and +//! untagged integers MUST be rejected. +//! - The vendor/model MUST match one of the registered platform pairs. +//! - The digest algorithm and length MUST match the platform's expected values. +//! - CBOR deterministic encoding SHOULD be used for map keys (already +//! ensured by emitting integer keys in ascending order). + +/// The profile URI for IGVM launch endorsement CoRIM documents. +/// +/// This URI is set in `corim-map` key 3 (`profile`) and signals to a +/// verifier that the document conforms to the constraints above. +/// +/// Format follows the tag URI scheme (RFC 4151): +/// `tag:,:` +pub const PROFILE_URI: &str = "tag:microsoft.com,2026:igvm-launch-endorsement/v1"; diff --git a/igvm/src/corim/mod.rs b/igvm/src/corim/mod.rs new file mode 100644 index 0000000..6017dd4 --- /dev/null +++ b/igvm/src/corim/mod.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! CoRIM (Concise Reference Integrity Manifest) support for IGVM. +//! +//! This module provides CoRIM document generation for IGVM attestation. +//! +//! Built on top of the [`corim`](https://github.com/Azure/corim) crate +//! for typed CoRIM/CoMID structures, CBOR encoding, and structural +//! validation per draft-ietf-rats-corim-10. +//! +//! # Modules +//! +//! - [`launch_endorsement`] — The launch endorsement profile + +pub mod launch_endorsement; + +// Re-export launch_endorsement types for convenience. +pub use launch_endorsement::Error; diff --git a/igvm/src/lib.rs b/igvm/src/lib.rs index 0100a04..3a5c67c 100644 --- a/igvm/src/lib.rs +++ b/igvm/src/lib.rs @@ -40,6 +40,27 @@ use zerocopy::KnownLayout; #[cfg(feature = "igvm-c")] pub mod c_api; +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +pub mod corim; + +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +pub mod measurement; + +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +mod serializer; +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +pub use serializer::IgvmPlatformMeasurement; +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +pub use serializer::IgvmSerializer; +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +pub use serializer::MeasurementOptions; + pub mod hv_defs; pub mod page_table; pub mod registers; @@ -1778,7 +1799,7 @@ impl IgvmDirectiveHeader { return Err(BinaryHeaderError::UnalignedAddress(*gpa)); } - if *number_of_bytes as u64 % PAGE_SIZE_4K != 0 { + if (*number_of_bytes as u64) % PAGE_SIZE_4K != 0 { return Err(BinaryHeaderError::UnalignedSize(*number_of_bytes as u64)); } } @@ -2255,6 +2276,19 @@ pub enum Error { InvalidFixedHeaderArch(u32), #[error("merged igvm files are not the same revision")] MergeRevision, + #[cfg(feature = "corim")] + #[error("CoRIM generation failed: {0}")] + CorimGeneration(String), + #[cfg(feature = "corim")] + #[error("measurement computation failed: {0}")] + MeasurementFailed(String), +} + +#[cfg(feature = "corim")] +impl From for Error { + fn from(e: crate::corim::launch_endorsement::Error) -> Self { + Error::CorimGeneration(e.to_string()) + } } /// Architecture for an IGVM file. @@ -2476,6 +2510,45 @@ impl FixedHeader { } } +/// Pre-defined CoRIM templates for IGVM endorsements. +/// +/// Each variant represents a fixed CoRIM structure with +/// well-defined semantics. The caller supplies only the variable parameters, +/// and the template determines the full CBOR layout. +/// +/// Used with [`IgvmSerializer::add_corim`]. +/// +/// # Future extensibility +/// +/// New CoRIM profiles (e.g., the Intel TDX profile) are added as new +/// variants of this enum; [`IgvmSerializer::add_corim`] grows a new +/// match arm to dispatch to the corresponding profile-specific builder. +/// +/// This enum is intentionally the abstraction boundary instead of a +/// `trait CorimProfile` — when a second profile lands, we'll have +/// concrete data to decide whether shared behavior justifies introducing +/// a trait. Until then, two concrete profile types side-by-side keep the +/// API surface small and avoid prematurely encoding launch-endorsement +/// assumptions (single platform, single SVN, CES-only) into a generic +/// shape. +#[cfg(feature = "corim")] +#[cfg_attr(docsrs, doc(cfg(feature = "corim")))] +#[derive(Debug, Clone)] +pub enum CorimTemplate { + /// A CoRIM produced by the launch endorsement profile. + /// + /// Built via the two-stage + /// [`LaunchEndorsement`](crate::corim::launch_endorsement::LaunchEndorsement) + /// builder; finalize with + /// [`LaunchEndorsement::build`](crate::corim::launch_endorsement::LaunchEndorsement::build) + /// to obtain this variant. + LaunchEndorsement(crate::corim::launch_endorsement::LaunchEndorsement), + /// The architectural CoRIM template defined by vendors. + Architectural, + /// A custom CoRIM template with user-provided bytes. + Custom(Vec), +} + impl IgvmFile { /// Check if the given platform headers are valid. /// @@ -3210,6 +3283,12 @@ impl IgvmFile { self.initialization_headers.as_slice() } + /// Get a mutable reference to the initialization headers in this file. + #[cfg(feature = "corim")] + pub(crate) fn initializations_mut(&mut self) -> &mut Vec { + &mut self.initialization_headers + } + /// Get the directive headers in this file. pub fn directives(&self) -> &[IgvmDirectiveHeader] { self.directive_headers.as_slice() diff --git a/igvm/src/measurement/mod.rs b/igvm/src/measurement/mod.rs new file mode 100644 index 0000000..9656275 --- /dev/null +++ b/igvm/src/measurement/mod.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! Launch measurement calculation for CVM platforms. +//! +//! Computes platform-specific launch digests from an IGVM file's headers, +//! matching the measurement algorithms used by the hardware/firmware: +//! +//! - **AMD SEV-SNP**: Iterates page data, chaining SHA-384 hashes through +//! `SnpPageInfo` structures per the AMD SEV-SNP firmware ABI. +//! - **Intel TDX**: Computes MRTD by hashing `MEM.PAGE.ADD` and `MR.EXTEND` +//! operations per the Intel TDX module specification. +//! - **Microsoft VBS**: Computes a SHA-256 boot measurement digest by hashing +//! page chunks and VP register state per the VBS measurement protocol. + +mod snp; +mod tdx; +mod vbs; + +pub use snp::generate_snp_measurement; +pub use tdx::generate_tdx_measurement; +pub use vbs::generate_vbs_measurement; + +pub(crate) const SHA_256_OUTPUT_SIZE: usize = 32; +pub(crate) const SHA_384_OUTPUT_SIZE: usize = 48; + +/// Errors from measurement calculation. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum MeasurementError { + /// A parameter area index referenced by a ParameterInsert was not found. + #[error("invalid parameter area index {0}")] + InvalidParameterAreaIndex(u32), + /// No guest policy found in initialization headers (SNP only). + #[error("no SNP guest policy found in initialization headers")] + MissingGuestPolicy, + /// Page data larger than 4K is not supported. + #[error("unsupported page data size: {0} bytes")] + UnsupportedPageSize(usize), +} diff --git a/igvm/src/measurement/snp.rs b/igvm/src/measurement/snp.rs new file mode 100644 index 0000000..1becc8b --- /dev/null +++ b/igvm/src/measurement/snp.rs @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! AMD SEV-SNP launch measurement (launch digest) calculation. +//! +//! Computes the launch digest by iterating IGVM directive headers and chaining +//! SHA-384 hashes through `SnpPageInfo` structures, matching the SNP firmware's +//! measurement algorithm. The fully-populated [`SnpPspIdBlock`] that is +//! suitable for offline signing is also emitted at `debug` level via +//! [`tracing`] for diagnostic inspection. + +use super::MeasurementError; +use super::SHA_384_OUTPUT_SIZE; +use crate::IgvmDirectiveHeader; +use crate::IgvmInitializationHeader; +use igvm_defs::IgvmPageDataType; +use igvm_defs::PAGE_SIZE_4K; +use sha2::Digest; +use sha2::Sha384; +use std::collections::HashMap; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize; + +// Local type definitions matching the AMD SEV-SNP firmware ABI. +// These mirror x86defs::snp types but are defined locally to avoid +// taking a dependency on hvlite-internal crates. + +/// SNP page type constants. +mod snp_page_type { + pub const NORMAL: u8 = 0x1; + pub const VMSA: u8 = 0x2; + pub const UNMEASURED: u8 = 0x4; + pub const SECRETS: u8 = 0x5; + pub const CPUID: u8 = 0x6; +} + +/// Structure used by SNP firmware to chain page measurements. +/// +/// See AMD SEV-SNP firmware ABI specification §7.3 (SNP_LAUNCH_UPDATE). +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] +struct SnpPageInfo { + digest_current: [u8; 48], + contents: [u8; 48], + length: u16, + page_type: u8, + imi_page_bit: u8, + lower_vmpl_permissions: u32, + gpa: u64, +} + +/// Structure containing the completed SNP measurement of the IGVM file. +/// The signature of the hash of this struct is the `id_key_signature` for +/// [`igvm_defs::IGVM_VHS_SNP_ID_BLOCK`]. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct SnpPspIdBlock { + /// completed launch digest of IGVM file + ld: [u8; 48], + /// family id of the guest + family_id: [u8; 16], + /// image id of the guest + image_id: [u8; 16], + /// Version of the ID block format, must be 0x1 + version: u32, + /// Software version of the guest + guest_svn: u32, + /// SNP Policy of the guest + policy: u64, +} + +/// Compute the SNP launch digest from IGVM headers. +/// +/// Iterates all initialization and directive headers for the given +/// compatibility mask, computing the chained SHA-384 measurement that +/// the SNP firmware would produce during `SNP_LAUNCH_UPDATE`. The +/// returned digest is the `ld` field of the [`SnpPspIdBlock`] structure; +/// the fully-populated structure is also emitted at `debug` level via +/// [`tracing`] for diagnostic inspection. +/// +/// `svn`, `family_id`, and `image_id` are not used in the digest +/// computation itself, but populate the corresponding fields of the +/// diagnostic ID block that is logged. +pub fn generate_snp_measurement( + initialization_headers: &[IgvmInitializationHeader], + directive_headers: &[IgvmDirectiveHeader], + compatibility_mask: u32, + svn: u32, + family_id: [u8; 16], + image_id: [u8; 16], +) -> Result<[u8; SHA_384_OUTPUT_SIZE], MeasurementError> { + let mut parameter_area_table = HashMap::new(); + let mut launch_digest = [0u8; SHA_384_OUTPUT_SIZE]; + + // Pre-compute hash of zero page (used when file does not carry data) + let zero_page = [0u8; PAGE_SIZE_4K_USIZE]; + let zero_digest: [u8; SHA_384_OUTPUT_SIZE] = { + let mut h = Sha384::new(); + h.update(zero_page); + h.finalize().into() + }; + + let mut padding_vec = vec![0u8; PAGE_SIZE_4K_USIZE]; + + let mut measure_page = + |page_type: u8, gpa: u64, page_data: Option<&[u8]>| -> Result<(), MeasurementError> { + let hash_contents: [u8; SHA_384_OUTPUT_SIZE] = match page_data { + Some(data) => match data.len() { + 0 => zero_digest, + len if len < PAGE_SIZE_4K_USIZE => { + padding_vec.fill(0); + padding_vec[..len].copy_from_slice(data); + let mut h = Sha384::new(); + h.update(&padding_vec); + h.finalize().into() + } + PAGE_SIZE_4K_USIZE => { + let mut h = Sha384::new(); + h.update(data); + h.finalize().into() + } + len => return Err(MeasurementError::UnsupportedPageSize(len)), + }, + None => [0u8; SHA_384_OUTPUT_SIZE], + }; + + let info = SnpPageInfo { + digest_current: launch_digest, + contents: hash_contents, + length: size_of::() as u16, + page_type, + imi_page_bit: 0, + lower_vmpl_permissions: 0, + gpa, + }; + + let mut h = Sha384::new(); + h.update(info.as_bytes()); + launch_digest = h.finalize().into(); + Ok(()) + }; + + // Find SNP guest policy + let policy = initialization_headers + .iter() + .find_map(|h| { + if let IgvmInitializationHeader::GuestPolicy { + policy, + compatibility_mask: mask, + } = h + { + if mask & compatibility_mask == compatibility_mask { + return Some(*policy); + } + } + None + }) + .ok_or(MeasurementError::MissingGuestPolicy)?; + + // Iterate directive headers + for header in directive_headers { + if header + .compatibility_mask() + .map(|mask| mask & compatibility_mask != compatibility_mask) + .unwrap_or(false) + { + continue; + } + + match header { + IgvmDirectiveHeader::ParameterArea { + number_of_bytes, + parameter_area_index, + initial_data: _, + } => { + parameter_area_table.insert(*parameter_area_index, *number_of_bytes); + } + IgvmDirectiveHeader::PageData { + gpa, + flags, + data_type, + data, + .. + } => { + if flags.shared() { + continue; + } + + let (page_type, data) = match *data_type { + IgvmPageDataType::SECRETS => (snp_page_type::SECRETS, None), + IgvmPageDataType::CPUID_DATA | IgvmPageDataType::CPUID_XF => { + (snp_page_type::CPUID, None) + } + _ => { + if flags.unmeasured() { + (snp_page_type::UNMEASURED, None) + } else { + (snp_page_type::NORMAL, Some(data.as_slice())) + } + } + }; + + measure_page(page_type, *gpa, data)?; + } + IgvmDirectiveHeader::ParameterInsert(param) => { + let parameter_area_size = parameter_area_table + .get(¶m.parameter_area_index) + .ok_or(MeasurementError::InvalidParameterAreaIndex( + param.parameter_area_index, + ))?; + + for gpa in (param.gpa..param.gpa + *parameter_area_size).step_by(PAGE_SIZE_4K_USIZE) + { + measure_page(snp_page_type::UNMEASURED, gpa, None)?; + } + } + IgvmDirectiveHeader::SnpVpContext { gpa, vmsa, .. } => { + let vmsa_bytes = vmsa.as_ref().as_bytes(); + measure_page(snp_page_type::VMSA, *gpa, Some(vmsa_bytes))?; + } + _ => {} + } + } + + // Generate the PSP ID block format, hash with SHA-384. + let psp_id_block = SnpPspIdBlock { + ld: launch_digest, + family_id, + image_id, + version: 0x1, + guest_svn: svn, + policy, + }; + // Emit the ID block for diagnostics; not currently consumed. + tracing::debug!("SNP ID Block {:x?}", psp_id_block); + Ok(psp_id_block.ld) +} diff --git a/igvm/src/measurement/tdx.rs b/igvm/src/measurement/tdx.rs new file mode 100644 index 0000000..5c0a779 --- /dev/null +++ b/igvm/src/measurement/tdx.rs @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! Intel TDX launch measurement (MRTD) calculation. +//! +//! Computes the MRTD by iterating IGVM directive headers and hashing +//! `MEM.PAGE.ADD` and `MR.EXTEND` operations, matching the TDX module's +//! measurement algorithm. + +use super::MeasurementError; +use super::SHA_384_OUTPUT_SIZE; +use crate::IgvmDirectiveHeader; +use igvm_defs::PAGE_SIZE_4K; +use sha2::Digest; +use sha2::Sha384; +use std::collections::HashMap; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize; +const TDX_EXTEND_CHUNK_SIZE: usize = 256; + +/// Structure for measuring a page addition to the TD. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] +struct TdxPageAdd { + /// `MEM.PAGE.ADD` operation identifier. + operation: [u8; 16], + /// Guest physical address (page-aligned). + gpa: u64, + /// Reserved, must be zero. + mbz: [u8; 104], +} + +/// Structure for measuring a 256-byte data chunk. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, FromBytes)] +struct TdxMrExtend { + /// `MR.EXTEND` operation identifier. + operation: [u8; 16], + /// Guest physical address (256-byte aligned). + gpa: u64, + /// Reserved, must be zero. + mbz: [u8; 104], + /// 256 bytes of data to measure. + data: [u8; TDX_EXTEND_CHUNK_SIZE], +} + +/// Compute the TDX MRTD from IGVM directive headers. +/// +/// Iterates all directive headers for the given compatibility mask, +/// computing the running SHA-384 hash of `MEM.PAGE.ADD` and `MR.EXTEND` +/// operations that the TDX module would perform during `TDH.MR.FINALIZE`. +pub fn generate_tdx_measurement( + directive_headers: &[IgvmDirectiveHeader], + compatibility_mask: u32, +) -> Result<[u8; SHA_384_OUTPUT_SIZE], MeasurementError> { + let mut parameter_area_table = HashMap::new(); + let mut padding_vec = vec![0u8; PAGE_SIZE_4K_USIZE]; + let mut hasher = Sha384::new(); + + let mut measure_page = |gpa: u64, page_data: Option<&[u8]>| -> Result<(), MeasurementError> { + // Measure the page being added. + let page_add = TdxPageAdd { + operation: *b"MEM.PAGE.ADD\0\0\0\0", + gpa, + mbz: [0; 104], + }; + hasher.update(page_add.as_bytes()); + + // Possibly measure the page contents in 256-byte chunks. + if let Some(data) = page_data { + let data = match data.len() { + 0 => None, + PAGE_SIZE_4K_USIZE => Some(data), + len if len < PAGE_SIZE_4K_USIZE => { + padding_vec.fill(0); + padding_vec[..len].copy_from_slice(data); + Some(padding_vec.as_slice()) + } + len => return Err(MeasurementError::UnsupportedPageSize(len)), + }; + + for offset in (0..PAGE_SIZE_4K).step_by(TDX_EXTEND_CHUNK_SIZE) { + let mut mr_extend = TdxMrExtend { + operation: *b"MR.EXTEND\0\0\0\0\0\0\0", + gpa: gpa + offset, + mbz: [0; 104], + data: [0; TDX_EXTEND_CHUNK_SIZE], + }; + + if let Some(data) = data { + mr_extend.data.copy_from_slice( + &data[offset as usize..offset as usize + TDX_EXTEND_CHUNK_SIZE], + ); + } + hasher.update(mr_extend.as_bytes()); + } + } + Ok(()) + }; + + for header in directive_headers { + if header + .compatibility_mask() + .map(|mask| mask & compatibility_mask != compatibility_mask) + .unwrap_or(false) + { + continue; + } + + match header { + IgvmDirectiveHeader::ParameterArea { + number_of_bytes, + parameter_area_index, + initial_data: _, + } => { + parameter_area_table.insert(*parameter_area_index, *number_of_bytes); + } + IgvmDirectiveHeader::PageData { + gpa, flags, data, .. + } => { + if flags.shared() { + continue; + } + + let data = if flags.unmeasured() { + None + } else { + Some(data.as_slice()) + }; + + measure_page(*gpa, data)?; + } + IgvmDirectiveHeader::ParameterInsert(param) => { + let parameter_area_size = parameter_area_table + .get(¶m.parameter_area_index) + .ok_or(MeasurementError::InvalidParameterAreaIndex( + param.parameter_area_index, + ))?; + + for gpa in (param.gpa..param.gpa + *parameter_area_size).step_by(PAGE_SIZE_4K_USIZE) + { + measure_page(gpa, None)?; + } + } + _ => {} + } + } + + let mrtd: [u8; SHA_384_OUTPUT_SIZE] = hasher.finalize().into(); + tracing::debug!("MRTD: {}", hex::encode_upper(mrtd)); + Ok(mrtd) +} diff --git a/igvm/src/measurement/vbs.rs b/igvm/src/measurement/vbs.rs new file mode 100644 index 0000000..d93a671 --- /dev/null +++ b/igvm/src/measurement/vbs.rs @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! Microsoft VBS launch measurement (boot digest) calculation. +//! +//! Computes the VBS boot measurement digest by iterating IGVM directive +//! headers and hashing page chunks and VP register state using SHA-256, +//! matching the VBS measurement protocol. +//! + +#![expect(non_camel_case_types)] + +use super::MeasurementError; +use super::SHA_256_OUTPUT_SIZE; +use crate::IgvmDirectiveHeader; +use bitfield_struct::bitfield; +use igvm_defs::IgvmPageDataType; +use igvm_defs::VbsDigestAlgorithm; +use igvm_defs::VbsSigningAlgorithm; +use igvm_defs::VbsVpContextRegister; +use igvm_defs::PAGE_SIZE_4K; +use open_enum::open_enum; +use sha2::Digest; +use sha2::Sha256; +use static_assertions::const_assert; +use std::collections::HashMap; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize; + +/// Full chunk size for VBS measurement (chunk header + page data). +const VBS_VP_CHUNK_SIZE_BYTES: usize = PAGE_SIZE_4K_USIZE + size_of::(); + +/// Acceptance flag indicating a GPA page is readable. +const VM_GPA_PAGE_READABLE: u64 = 0x1; +/// Acceptance flag indicating a GPA page is writable. +const VM_GPA_PAGE_WRITABLE: u64 = 0x2; + +/// Structure containing the completed VBS boot measurement of the IGVM file. +/// The signature of the hash of this struct is the signature for +/// [`igvm_defs::IGVM_VHS_VBS_MEASUREMENT`]. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, Debug)] +struct VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA { + /// The version of the signature structure + version: u32, + /// The user supplied product id + product_id: u32, + /// The user supplied module id + module_id: u32, + /// The user supplied svn + security_version: u32, + /// Security policy for the guest + security_policy: VBS_POLICY_FLAGS, + /// Algorithm that created the boot digest hash + boot_digest_algo: u32, + /// Algorithm that produces the signature + signing_algo: u32, + /// VBS Boot digest + boot_measurement_digest: [u8; 32], +} + +/// Chunk that is measured to generate digest. These consist of a 16 byte header +/// followed by data. This needs c style alignment to generate a consistent +/// measurement. Defined by the following struct in C: +/// ``` ignore +/// typedef struct _VBS_VM_BOOT_MEASUREMENT_CHUNK +/// { +/// UINT32 ByteCount; +/// VBS_VM_BOOT_MEASUREMENT_CHUNK_TYPE Type; +/// UINT64 Reserved; +/// +/// union +/// { +/// VBS_VM_BOOT_MEASUREMENT_CHUNK_VP_REGISTER VpRegister; +/// VBS_VM_BOOT_MEASUREMENT_CHUNK_VP_VTL_ENABLED VpVtlEnabled; +/// VBS_VM_BOOT_MEASUREMENT_CHUNK_GPA_PAGE GpaPage; +/// } u; +/// } VBS_VM_BOOT_MEASUREMENT_CHUNK, *PVBS_VM_BOOT_MEASUREMENT_CHUNK; +/// ``` +/// +/// Structure describing the chunk to be measured. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout)] +struct VbsChunkHeader { + /// The full size to be measured + byte_count: u32, + chunk_type: BootMeasurementType, + reserved: u64, +} + +/// Structure describing the register being measured. Will be padded to +/// [`VBS_VP_CHUNK_SIZE_BYTES`] when hashed to generate digest. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout)] +struct VbsRegisterChunk { + header: VbsChunkHeader, + reserved: u32, + vtl: u8, + reserved2: u8, + reserved3: u16, + reserved4: u32, + name: u32, + value: [u8; 16], +} +const_assert!(size_of::() <= VBS_VP_CHUNK_SIZE_BYTES); + +/// Structure describing the page to be measured. +/// Page data is hashed after struct to generate digest, if not a full page, +/// measurable data will be padded to [`VBS_VP_CHUNK_SIZE_BYTES`]. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout)] +struct VpGpaPageChunk { + header: VbsChunkHeader, + metadata: u64, + page_number: u64, +} + +#[open_enum] +#[derive(IntoBytes, Immutable, KnownLayout, Clone, Copy)] +#[repr(u32)] +enum BootMeasurementType { + VP_REGISTER = 0, + VP_VTL_ENABLED = 1, + VP_GPA_PAGE = 2, +} + +/// Flags indicating read and write acceptance of a GPA Page and whether it is +/// to be measured in the digest. +#[bitfield(u64)] +struct VBS_VM_GPA_PAGE_BOOT_METADATA { + #[bits(2)] + acceptance: u64, + #[bits(1)] + data_unmeasured: bool, + #[bits(61)] + reserved: u64, +} + +/// Flags defining the security policy for the guest. +#[bitfield(u32)] +#[derive(IntoBytes, Immutable, KnownLayout)] +struct VBS_POLICY_FLAGS { + /// Guest supports debugging + #[bits(1)] + debug: bool, + #[bits(31)] + reserved: u32, +} + +/// Compute the VBS boot measurement digest from IGVM directive headers. +/// +/// Iterates all directive headers for the given compatibility mask, computing +/// the chained SHA-256 measurement that VBS firmware would produce. The +/// returned digest is the `boot_measurement_digest` field of the +/// [`VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA`] structure. +/// +/// `svn` and `enable_debug` do not affect the returned digest. They are only +/// used to populate the `security_version` and `security_policy` fields of +/// the signed-data structure that is emitted via `tracing::debug!` for +/// diagnostic purposes. +pub fn generate_vbs_measurement( + directive_headers: &[IgvmDirectiveHeader], + compatibility_mask: u32, + enable_debug: bool, + svn: u32, +) -> Result<[u8; SHA_256_OUTPUT_SIZE], MeasurementError> { + let mut digest = VbsDigestor::new(); + let mut parameter_area_table = HashMap::new(); + let mut bsp_regs: Vec> = Vec::new(); + + for header in directive_headers { + // Skip headers that have compatibility masks that do not match vbs. + if header + .compatibility_mask() + .map(|mask| mask & compatibility_mask != compatibility_mask) + .unwrap_or(false) + { + continue; + } + + match header { + IgvmDirectiveHeader::PageData { + gpa, + compatibility_mask: _, + flags, + data_type, + data, + } => { + assert_eq!(*data_type, IgvmPageDataType::NORMAL); + + // Skip shared pages. + if flags.shared() { + continue; + } + + let boot_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new() + .with_acceptance(0) + .with_data_unmeasured(flags.unmeasured()); + digest.record_gpa_page(gpa / PAGE_SIZE_4K, 1, boot_metadata, data); + } + IgvmDirectiveHeader::ParameterInsert(param) => { + let page_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new() + .with_acceptance(0) + .with_data_unmeasured(true); + let parameter_area_size = parameter_area_table + .get(¶m.parameter_area_index) + .ok_or(MeasurementError::InvalidParameterAreaIndex( + param.parameter_area_index, + ))?; + digest.record_gpa_page( + param.gpa / PAGE_SIZE_4K, + parameter_area_size / PAGE_SIZE_4K, + page_metadata, + &[], + ); + } + IgvmDirectiveHeader::X64VbsVpContext { + vtl, + registers, + compatibility_mask: _, + } => { + // The VBS measurement format requires the cpu context to be + // measured last; collect now and apply at the end. + let vtl_registers: Vec = registers + .iter() + .map(|r| r.into_vbs_vp_context_reg(*vtl)) + .collect(); + bsp_regs.push(vtl_registers); + } + IgvmDirectiveHeader::AArch64VbsVpContext { + vtl, + registers, + compatibility_mask: _, + } => { + // The VBS measurement format requires the cpu context to be + // measured last; collect now and apply at the end. + let vtl_registers: Vec = registers + .iter() + .map(|r| r.into_vbs_vp_context_reg(*vtl)) + .collect(); + bsp_regs.push(vtl_registers); + } + IgvmDirectiveHeader::ErrorRange { + gpa, + compatibility_mask: _, + size_bytes, + } => { + let page_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new() + .with_acceptance(VM_GPA_PAGE_READABLE | VM_GPA_PAGE_WRITABLE) + .with_data_unmeasured(true); + digest.record_gpa_page( + *gpa / PAGE_SIZE_4K, + (*size_bytes as u64).div_ceil(PAGE_SIZE_4K), + page_metadata, + &[], + ); + } + IgvmDirectiveHeader::ParameterArea { + number_of_bytes, + parameter_area_index, + initial_data: _, + } => { + if parameter_area_table.contains_key(parameter_area_index) { + return Err(MeasurementError::InvalidParameterAreaIndex( + *parameter_area_index, + )); + } + parameter_area_table.insert(*parameter_area_index, *number_of_bytes); + } + _ => {} + } + } + + // Measure all registers in each VTL as last step in measurement. + for set in bsp_regs { + for reg in set { + digest.record_vp_register(reg); + } + } + + // Identifier constants chosen to maintain compatibility with internal tooling. + const MSFT_PRODUCT_ID: u32 = u32::from_le_bytes(*b"msft"); + const VBS_MODULE_ID: u32 = u32::from_le_bytes(*b"vbs\0"); + const VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT: u32 = 0x1; + + let boot_measurement = VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA { + version: VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT, + product_id: MSFT_PRODUCT_ID, + module_id: VBS_MODULE_ID, + security_version: svn, + security_policy: VBS_POLICY_FLAGS::new().with_debug(enable_debug), + boot_digest_algo: VbsDigestAlgorithm::SHA256.0, + signing_algo: VbsSigningAlgorithm::ECDSA_P384.0, + boot_measurement_digest: digest.finish_digest(), + }; + // Emit the signed-data struct for diagnostics; not currently consumed. + tracing::debug!("Boot Measurement {:x?}", boot_measurement); + Ok(boot_measurement.boot_measurement_digest) +} + +struct VbsDigestor { + digest: [u8; SHA_256_OUTPUT_SIZE], +} + +impl VbsDigestor { + fn new() -> Self { + Self { + digest: [0; SHA_256_OUTPUT_SIZE], + } + } + + fn record_gpa_page( + &mut self, + gpa_page_base: u64, + page_count: u64, + page_metadata: VBS_VM_GPA_PAGE_BOOT_METADATA, + mut data: &[u8], + ) { + for page in 0..page_count { + let import_data_len: usize = match page_metadata.data_unmeasured() { + true => 0, + false => std::cmp::min(PAGE_SIZE_4K_USIZE, data.len()), + }; + let (import_data, data_remaining) = data.split_at(import_data_len); + data = data_remaining; + + // If page is under 4K bytes, pad to full length which will be + // hashed with page and chunk data. + let padding = vec![0u8; PAGE_SIZE_4K_USIZE - import_data.len()]; + let page_number = gpa_page_base + page; + let chunk = VpGpaPageChunk { + header: VbsChunkHeader { + byte_count: VBS_VP_CHUNK_SIZE_BYTES as u32, + chunk_type: BootMeasurementType::VP_GPA_PAGE, + reserved: 0, + }, + metadata: page_metadata.into(), + page_number, + }; + self.create_record_entry(&[chunk.as_bytes(), import_data, &padding]); + } + } + + fn record_vp_register(&mut self, reg: VbsVpContextRegister) { + let chunk = VbsRegisterChunk { + header: VbsChunkHeader { + byte_count: size_of::() as u32, + chunk_type: BootMeasurementType::VP_REGISTER, + reserved: 0, + }, + reserved: 0, + vtl: reg.vtl, + reserved2: 0, + reserved3: 0, + reserved4: 0, + name: reg.register_name.into(), + value: reg.register_value, + }; + self.create_record_entry(&[chunk.as_bytes()]); + } + + fn create_record_entry(&mut self, chunks: &[&[u8]]) { + let mut hasher = Sha256::new(); + hasher.update(self.digest.as_slice()); + for chunk in chunks { + hasher.update(chunk); + } + self.digest = hasher.finalize().into(); + } + + fn finish_digest(&self) -> [u8; SHA_256_OUTPUT_SIZE] { + self.digest + } +} diff --git a/igvm/src/serializer.rs b/igvm/src/serializer.rs new file mode 100644 index 0000000..e8e136c --- /dev/null +++ b/igvm/src/serializer.rs @@ -0,0 +1,818 @@ +// SPDX-License-Identifier: MIT +// +// Copyright (c) Microsoft Corporation. + +//! IGVM file serializer with support for computing launch measurements +//! and attaching CoRIM endorsements before writing to binary format. +//! +//! [`IgvmSerializer`] borrows an immutable [`IgvmFile`] and provides a +//! builder-style API for enriching the output with per-platform launch +//! measurements and CoRIM documents, without mutating the original file. +//! +//! # Example +//! +//! ```rust,no_run +//! use igvm::corim::launch_endorsement::LaunchEndorsement; +//! use igvm::corim::launch_endorsement::MeasurementKind; +//! use igvm::IgvmFile; +//! use igvm::IgvmSerializer; +//! use igvm_defs::IgvmPlatformType; +//! +//! # fn example(file: &IgvmFile) -> Result<(), igvm::Error> { +//! // Construction eagerly computes the launch measurement for every +//! // measurable platform header in the file. +//! let mut serializer = IgvmSerializer::new(file)?; +//! +//! // Inspect the SNP measurement (already cached from `new`). +//! if let Some(m) = serializer.measurement_for(IgvmPlatformType::SEV_SNP) { +//! println!("SNP digest: {}", hex::encode(&m.digest)); +//! } +//! +//! // Stage 1: populate the launch measurement. +//! let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP)?; +//! le.set_measurement(MeasurementKind::Launch)?; +//! +//! // Stage 2: build a CES triple that endorses SVN 1 when the launch +//! // measurement matches. +//! le.endorse(1).with(MeasurementKind::Launch)?.finish()?; +//! +//! serializer.add_corim(IgvmPlatformType::SEV_SNP, le.build())?; +//! +//! // Serialize to binary +//! let mut output = Vec::new(); +//! serializer.serialize(&mut output)?; +//! # Ok(()) +//! # } +//! ``` + +use crate::CorimTemplate; +use crate::Error; +use crate::IgvmFile; +use crate::IgvmInitializationHeader; +use crate::IgvmPlatformHeader; +use igvm_defs::IgvmPlatformType; + +/// Per-platform context that callers can supply to influence measurement +/// diagnostics. +#[derive(Debug, Clone, Copy, Default)] +pub struct MeasurementOptions { + /// Security version number associated with the measurement. + pub svn: u32, + /// Whether the guest is configured to permit debugging. + pub enable_debug: bool, + /// SNP-only: 16-byte family identifier embedded in the diagnostic + /// `SnpPspIdBlock` logged by + /// [`crate::measurement::generate_snp_measurement`]. Ignored for + /// non-SNP platforms. + pub snp_family_id: [u8; 16], + /// SNP-only: 16-byte image identifier embedded in the diagnostic + /// `SnpPspIdBlock` logged by + /// [`crate::measurement::generate_snp_measurement`]. Ignored for + /// non-SNP platforms. + pub snp_image_id: [u8; 16], +} + +/// A per-platform launch measurement computed from an IGVM file's headers. +#[derive(Debug, Clone)] +pub struct IgvmPlatformMeasurement { + /// The platform type this measurement was computed for. + pub platform: IgvmPlatformType, + /// The compatibility mask associated with this platform. + pub compatibility_mask: u32, + /// The raw launch measurement digest bytes. + /// + /// Length depends on the platform: + /// - SEV-SNP: 48 bytes (SHA-384) + /// - TDX: 48 bytes (SHA-384) + /// - VBS: 32 bytes (SHA-256) + pub digest: Vec, +} + +/// Serializer that borrows an [`IgvmFile`] and enriches the output with +/// computed measurements and CoRIM endorsement documents. +/// +/// The underlying [`IgvmFile`] is never mutated. Additional initialization +/// headers (CoRIM documents) are accumulated in the serializer and merged +/// into the output during [`serialize`](IgvmSerializer::serialize). +#[derive(Debug)] +pub struct IgvmSerializer<'a> { + file: &'a IgvmFile, + measurements: Vec, + extra_init_headers: Vec, +} + +impl<'a> IgvmSerializer<'a> { + /// Create a new serializer for the given IGVM file. + /// + /// During construction, the launch measurement is computed for every + /// platform header in the file whose platform type has a measurement + /// profile defined by this crate (currently SEV-SNP, TDX, and VBS). + /// Platform headers with no measurement profile (e.g., + /// [`IgvmPlatformType::NATIVE`], [`IgvmPlatformType::SEV`], + /// [`IgvmPlatformType::SEV_ES`]) are silently skipped. + /// + /// # Errors + /// + /// Returns [`Error::MeasurementFailed`] if measurement computation + /// fails for any of the file's measurable platforms (e.g., SEV-SNP + /// without a [`IgvmInitializationHeader::GuestPolicy`] header). + pub fn new(file: &'a IgvmFile) -> Result { + Self::with_options(file, |_| MeasurementOptions::default()) + } + + /// Create a new serializer for the given IGVM file, supplying per-platform + /// [`MeasurementOptions`] used by the measurement diagnostics. + /// + /// [`new`](Self::new) is equivalent to calling this with an `options` + /// closure that always returns [`MeasurementOptions::default`]. + /// + /// The `options` closure is invoked once per measurable platform present + /// in the file. Currently only the VBS measurement consumes the supplied + /// values (for the diagnostic signed-data structure); SNP and TDX ignore + /// them. + /// + /// # Errors + /// + /// Returns [`Error::MeasurementFailed`] if measurement computation + /// fails for any of the file's measurable platforms. + pub fn with_options(file: &'a IgvmFile, mut options: F) -> Result + where + F: FnMut(IgvmPlatformType) -> MeasurementOptions, + { + let mut serializer = Self { + file, + measurements: Vec::new(), + extra_init_headers: Vec::new(), + }; + + // Eagerly compute the launch measurement for every supported + // platform present in the file. + let platforms: Vec = file + .platforms() + .iter() + .filter_map(|h| match h { + IgvmPlatformHeader::SupportedPlatform(info) + if Self::is_measurable(info.platform_type) => + { + Some(info.platform_type) + } + _ => None, + }) + .collect(); + for platform in platforms { + let platform_options = options(platform); + serializer.compute_measurement(platform, platform_options)?; + } + + Ok(serializer) + } + + /// Returns `true` if this crate's measurement profile knows how to + /// hash an IGVM file for the given platform type. + fn is_measurable(platform: IgvmPlatformType) -> bool { + matches!( + platform, + IgvmPlatformType::SEV_SNP | IgvmPlatformType::TDX | IgvmPlatformType::VSM_ISOLATION + ) + } + + /// Get a reference to the underlying IGVM file. + pub fn file(&self) -> &IgvmFile { + self.file + } + + /// Get all launch measurements computed for this file. + /// + /// One entry is present for every measurable platform header in the + /// file (see [`new`](Self::new)). + pub fn measurements(&self) -> &[IgvmPlatformMeasurement] { + &self.measurements + } + + /// Get the launch measurement for a specific platform, if the file + /// has a corresponding measurable platform header. + pub fn measurement_for(&self, platform: IgvmPlatformType) -> Option<&IgvmPlatformMeasurement> { + self.measurements.iter().find(|m| m.platform == platform) + } + + /// Look up the compatibility mask for a platform type from the file's + /// platform headers. + fn lookup_compatibility_mask(&self, platform: IgvmPlatformType) -> Result { + self.file + .platforms() + .iter() + .find_map(|h| match h { + IgvmPlatformHeader::SupportedPlatform(info) if info.platform_type == platform => { + Some(info.compatibility_mask) + } + _ => None, + }) + .ok_or_else(|| { + Error::MeasurementFailed(format!("no platform header found for {platform:?}")) + }) + } + + /// Internal: compute the launch measurement for a specific platform + /// and append it to the cache. Called eagerly from + /// [`with_options`](Self::with_options) for every measurable platform + /// header, using the per-platform [`MeasurementOptions`] supplied by + /// the caller. + #[cfg(feature = "corim")] + fn compute_measurement( + &mut self, + platform: IgvmPlatformType, + options: MeasurementOptions, + ) -> Result<(), Error> { + debug_assert!( + !self.measurements.iter().any(|m| m.platform == platform), + "compute_measurement called twice for {platform:?}" + ); + + let compatibility_mask = self.lookup_compatibility_mask(platform)?; + + let digest = match platform { + IgvmPlatformType::SEV_SNP => crate::measurement::generate_snp_measurement( + self.file.initializations(), + self.file.directives(), + compatibility_mask, + options.svn, + options.snp_family_id, + options.snp_image_id, + ) + .map_err(|e| Error::MeasurementFailed(e.to_string()))? + .to_vec(), + IgvmPlatformType::TDX => crate::measurement::generate_tdx_measurement( + self.file.directives(), + compatibility_mask, + ) + .map_err(|e| Error::MeasurementFailed(e.to_string()))? + .to_vec(), + IgvmPlatformType::VSM_ISOLATION => crate::measurement::generate_vbs_measurement( + self.file.directives(), + compatibility_mask, + options.enable_debug, + options.svn, + ) + .map_err(|e| Error::MeasurementFailed(e.to_string()))? + .to_vec(), + _ => { + return Err(Error::MeasurementFailed(format!( + "unsupported platform type for measurement: {platform:?}" + ))) + } + }; + + self.measurements.push(IgvmPlatformMeasurement { + platform, + compatibility_mask, + digest, + }); + + Ok(()) + } + + /// Attach a CoRIM endorsement for the given platform. + /// + /// The generated CoRIM document will be included as an + /// [`IgvmInitializationHeader::CorimDocument`] in the serialized output. + /// + /// For [`CorimTemplate::LaunchEndorsement`], populated measurements + /// take their digest bytes from the launch measurement computed at + /// construction time (see [`new`](Self::new)). + /// + /// # Arguments + /// + /// * `platform` — The target platform type. Must match a platform header + /// in the file, and must match the platform of the + /// [`LaunchEndorsement`](crate::corim::launch_endorsement::LaunchEndorsement) + /// in the template. + /// * `template` — The CoRIM template to instantiate. See + /// [`CorimTemplate`] for the supported variants. + #[cfg(feature = "corim")] + #[cfg_attr(docsrs, doc(cfg(feature = "corim")))] + pub fn add_corim( + &mut self, + platform: IgvmPlatformType, + template: CorimTemplate, + ) -> Result<(), Error> { + let compatibility_mask = self.lookup_compatibility_mask(platform)?; + + let corim_bytes = match template { + CorimTemplate::LaunchEndorsement(le) => { + if le.platform() != platform { + return Err(Error::CorimGeneration(format!( + "LaunchEndorsement targets {:?} but add_corim was \ + called with {platform:?}", + le.platform() + ))); + } + + self.build_launch_endorsement_corim(le)? + } + CorimTemplate::Architectural | CorimTemplate::Custom(_) => { + todo!("Architectural and Custom CoRIM templates not yet implemented") + } + }; + + self.extra_init_headers + .push(IgvmInitializationHeader::CorimDocument { + compatibility_mask, + document: corim_bytes, + }); + + Ok(()) + } + + /// Resolve a [`LaunchEndorsement`]'s populated measurements and CES + /// triples into the internal builder form, then build the CoRIM bytes. + #[cfg(feature = "corim")] + fn build_launch_endorsement_corim( + &self, + le: crate::corim::launch_endorsement::LaunchEndorsement, + ) -> Result, Error> { + use crate::corim::launch_endorsement::builder::ResolvedMeasurement; + use crate::corim::launch_endorsement::measurement_info; + + let platform = le.platform(); + // The measurement was computed eagerly during `IgvmSerializer::new` + // for every measurable platform header in the file. The caller of + // `add_corim` already validated `le.platform() == platform`, and + // `LaunchEndorsement::for_platform` only succeeds for measurable + // platforms — so the measurement must be present. + let cached = self.measurement_for(platform).ok_or_else(|| { + Error::CorimGeneration(format!("no platform header found for {platform:?}")) + })?; + + // Resolve the single populated measurement. + let kind = *le.measurement_kinds().iter().next().ok_or_else(|| { + Error::CorimGeneration( + "LaunchEndorsement requires exactly one populated measurement".into(), + ) + })?; + + let (mkey, digest_alg, _len) = + measurement_info(platform, kind).expect("kind validated by set_measurement"); + let resolved = ResolvedMeasurement { + mkey: mkey.to_string(), + digest_alg, + digest: cached.digest.clone(), + }; + + if le.triples().len() != 1 { + return Err(Error::CorimGeneration(format!( + "LaunchEndorsement requires exactly one CES triple, got {}", + le.triples().len() + ))); + } + + let svn = le.triples()[0].svn(); + + crate::corim::launch_endorsement::builder::build_corim_bytes(platform, &resolved, svn) + .map_err(|e| Error::CorimGeneration(e.to_string())) + } + + /// Serialize the IGVM file to binary format, including any CoRIM + /// documents that were added via [`add_corim`](Self::add_corim). + /// + /// This produces the same binary format as [`IgvmFile::serialize`], + /// but with additional initialization headers appended. + pub fn serialize(&self, output: &mut Vec) -> Result<(), Error> { + if self.extra_init_headers.is_empty() { + // Fast path: nothing added, delegate directly. + self.file.serialize(output) + } else { + // Clone the file and append the extra init headers so that + // the original IgvmFile::serialize handles all the work. + let mut file = self.file.clone(); + file.initializations_mut() + .extend(self.extra_init_headers.iter().cloned()); + file.serialize(output) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hv_defs::Vtl; + use crate::registers::X86Register; + use crate::Arch; + use crate::CorimTemplate; + use crate::IgvmInitializationHeader; + use crate::IgvmPlatformHeader; + use crate::IgvmRevision; + use igvm_defs::IgvmPageDataFlags; + use igvm_defs::IgvmPageDataType; + use igvm_defs::IgvmPlatformType; + use igvm_defs::IGVM_VHS_SUPPORTED_PLATFORM; + use igvm_defs::PAGE_SIZE_4K; + + fn new_platform(mask: u32, platform_type: IgvmPlatformType) -> IgvmPlatformHeader { + IgvmPlatformHeader::SupportedPlatform(IGVM_VHS_SUPPORTED_PLATFORM { + compatibility_mask: mask, + highest_vtl: 0, + platform_type, + platform_version: 1, + shared_gpa_boundary: 0, + }) + } + + fn new_page_data(page: u64, mask: u32, data: &[u8]) -> crate::IgvmDirectiveHeader { + crate::IgvmDirectiveHeader::PageData { + gpa: page * PAGE_SIZE_4K, + compatibility_mask: mask, + flags: IgvmPageDataFlags::new(), + data_type: IgvmPageDataType::NORMAL, + data: data.to_vec(), + } + } + + /// Build a minimal VBS IgvmFile with some page data and VP context. + fn make_vbs_file() -> IgvmFile { + IgvmFile::new( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![], + vec![ + new_page_data(0, 1, &[0xAA; PAGE_SIZE_4K as usize]), + new_page_data(1, 1, &[0xBB; PAGE_SIZE_4K as usize]), + crate::IgvmDirectiveHeader::X64VbsVpContext { + vtl: Vtl::Vtl0, + registers: vec![X86Register::Rip(0x1000)], + compatibility_mask: 0x1, + }, + ], + ) + .unwrap() + } + + /// Build a minimal SNP IgvmFile with a guest policy and page data. + fn make_snp_file() -> IgvmFile { + IgvmFile::new( + IgvmRevision::V1, + vec![new_platform(0x1, IgvmPlatformType::SEV_SNP)], + vec![IgvmInitializationHeader::GuestPolicy { + policy: 0x30000, + compatibility_mask: 0x1, + }], + vec![ + new_page_data(0, 1, &[0xCC; PAGE_SIZE_4K as usize]), + new_page_data(1, 1, &[0xDD; PAGE_SIZE_4K as usize]), + ], + ) + .unwrap() + } + + /// Build a minimal TDX IgvmFile with page data. + fn make_tdx_file() -> IgvmFile { + IgvmFile::new( + IgvmRevision::V1, + vec![new_platform(0x1, IgvmPlatformType::TDX)], + vec![], + vec![ + new_page_data(0, 1, &[0xEE; PAGE_SIZE_4K as usize]), + new_page_data(1, 1, &[0xFF; PAGE_SIZE_4K as usize]), + ], + ) + .unwrap() + } + + // ── Basic serializer tests ────────────────────────────────────── + + #[test] + fn serialize_without_corim_matches_file_serialize() { + let file = make_vbs_file(); + + // Serialize via IgvmFile::serialize + let mut direct = Vec::new(); + file.serialize(&mut direct).unwrap(); + + // Serialize via IgvmSerializer (no CoRIM added) + let serializer = IgvmSerializer::new(&file).unwrap(); + let mut via_builder = Vec::new(); + serializer.serialize(&mut via_builder).unwrap(); + + assert_eq!(direct, via_builder); + } + + #[test] + fn serialize_without_corim_roundtrips() { + let file = make_snp_file(); + + let serializer = IgvmSerializer::new(&file).unwrap(); + let mut output = Vec::new(); + serializer.serialize(&mut output).unwrap(); + + let deserialized = IgvmFile::new_from_binary(&output, None).unwrap(); + assert_eq!(file.platforms(), deserialized.platforms()); + assert_eq!(file.directives().len(), deserialized.directives().len()); + } + + // ── Measurement tests ─────────────────────────────────────────── + + #[test] + fn vbs_measurement_computed_eagerly() { + let file = make_vbs_file(); + let serializer = IgvmSerializer::new(&file).unwrap(); + + let m = serializer + .measurement_for(IgvmPlatformType::VSM_ISOLATION) + .expect("VBS measurement should be computed eagerly"); + assert_eq!(m.platform, IgvmPlatformType::VSM_ISOLATION); + assert_eq!(m.compatibility_mask, 0x1); + assert_eq!(m.digest.len(), 32); // SHA-256 + } + + #[test] + fn snp_measurement_computed_eagerly() { + let file = make_snp_file(); + let serializer = IgvmSerializer::new(&file).unwrap(); + + let m = serializer + .measurement_for(IgvmPlatformType::SEV_SNP) + .expect("SNP measurement should be computed eagerly"); + assert_eq!(m.platform, IgvmPlatformType::SEV_SNP); + assert_eq!(m.digest.len(), 48); // SHA-384 + } + + #[test] + fn tdx_measurement_computed_eagerly() { + let file = make_tdx_file(); + let serializer = IgvmSerializer::new(&file).unwrap(); + + let m = serializer + .measurement_for(IgvmPlatformType::TDX) + .expect("TDX measurement should be computed eagerly"); + assert_eq!(m.platform, IgvmPlatformType::TDX); + assert_eq!(m.digest.len(), 48); // SHA-384 + } + + #[test] + fn measurement_for_returns_none_for_absent_platform() { + // File has SNP only; querying TDX should return None. + let file = make_snp_file(); + let serializer = IgvmSerializer::new(&file).unwrap(); + assert!(serializer.measurement_for(IgvmPlatformType::TDX).is_none()); + } + + #[test] + fn unmeasurable_platform_skipped() { + // NATIVE has no measurement profile in this crate. A file containing + // only a NATIVE platform header should construct cleanly with an + // empty measurements list. + let file = IgvmFile::new( + IgvmRevision::V1, + vec![new_platform(0x1, IgvmPlatformType::NATIVE)], + vec![], + vec![new_page_data(0, 1, &[0xAA; PAGE_SIZE_4K as usize])], + ) + .unwrap(); + let serializer = IgvmSerializer::new(&file).unwrap(); + assert!(serializer.measurements().is_empty()); + assert!(serializer + .measurement_for(IgvmPlatformType::NATIVE) + .is_none()); + } + + #[test] + fn snp_missing_guest_policy_fails_construction() { + // SNP measurement requires a GuestPolicy initialization header. + // Build a file without one and verify `IgvmSerializer::new` fails. + let file = IgvmFile::new( + IgvmRevision::V1, + vec![new_platform(0x1, IgvmPlatformType::SEV_SNP)], + vec![], // no GuestPolicy + vec![new_page_data(0, 1, &[0xAA; PAGE_SIZE_4K as usize])], + ) + .unwrap(); + let err = IgvmSerializer::new(&file).unwrap_err(); + assert!(matches!(err, Error::MeasurementFailed(_)), "got: {err:?}"); + } + + // ── CoRIM integration tests ───────────────────────────────────── + + /// Helper: build a `CorimTemplate::LaunchEndorsement` with a single + /// CES triple binding the launch measurement → svn. + fn launch_endorsement_template(platform: IgvmPlatformType, svn: u64) -> CorimTemplate { + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + let mut le = LaunchEndorsement::for_platform(platform).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + le.endorse(svn) + .with(MeasurementKind::Launch) + .unwrap() + .finish() + .unwrap(); + le.build() + } + + #[test] + fn add_corim_produces_larger_output() { + let file = make_snp_file(); + + // Serialize without CoRIM + let mut without = Vec::new(); + file.serialize(&mut without).unwrap(); + + // Serialize with CoRIM + let mut serializer = IgvmSerializer::new(&file).unwrap(); + serializer + .add_corim( + IgvmPlatformType::SEV_SNP, + launch_endorsement_template(IgvmPlatformType::SEV_SNP, 1), + ) + .unwrap(); + let mut with = Vec::new(); + serializer.serialize(&mut with).unwrap(); + + // Output with CoRIM should be larger (has the CorimDocument init header) + assert!(with.len() > without.len()); + } + + #[test] + fn add_corim_uses_eager_measurement() { + let file = make_tdx_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + + // Measurement was computed eagerly during construction. + assert!(serializer.measurement_for(IgvmPlatformType::TDX).is_some()); + assert_eq!(serializer.measurements().len(), 1); + + // add_corim should reuse the cached measurement. + serializer + .add_corim( + IgvmPlatformType::TDX, + launch_endorsement_template(IgvmPlatformType::TDX, 5), + ) + .unwrap(); + + assert_eq!(serializer.measurements().len(), 1); + } + + #[test] + fn add_corim_output_roundtrips() { + let file = make_snp_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + serializer + .add_corim( + IgvmPlatformType::SEV_SNP, + launch_endorsement_template(IgvmPlatformType::SEV_SNP, 42), + ) + .unwrap(); + + let mut output = Vec::new(); + serializer.serialize(&mut output).unwrap(); + + // Should parse back successfully and contain a CorimDocument + let deserialized = IgvmFile::new_from_binary(&output, None).unwrap(); + let has_corim = deserialized + .initializations() + .iter() + .any(|h| matches!(h, IgvmInitializationHeader::CorimDocument { .. })); + assert!(has_corim); + } + + #[test] + fn file_not_mutated_after_add_corim() { + let file = make_snp_file(); + let init_count_before = file.initializations().len(); + + let mut serializer = IgvmSerializer::new(&file).unwrap(); + serializer + .add_corim( + IgvmPlatformType::SEV_SNP, + launch_endorsement_template(IgvmPlatformType::SEV_SNP, 1), + ) + .unwrap(); + + // The original file should not have been mutated + assert_eq!(file.initializations().len(), init_count_before); + } + + // ── Two-stage builder tests ───────────────────────────────────── + + #[test] + fn launch_endorsement_unsupported_platform() { + use crate::corim::launch_endorsement::Error as LeError; + use crate::corim::launch_endorsement::LaunchEndorsement; + let err = LaunchEndorsement::for_platform(IgvmPlatformType::NATIVE).unwrap_err(); + assert!(matches!(err, LeError::UnsupportedPlatform(_))); + } + + #[test] + fn launch_endorsement_select_unpopulated_rejected() { + use crate::corim::launch_endorsement::Error as LeError; + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + let err = le.endorse(1).with(MeasurementKind::Launch).unwrap_err(); + assert!(matches!(err, LeError::MeasurementNotPopulated { .. })); + } + + #[test] + fn launch_endorsement_duplicate_selection_rejected() { + use crate::corim::launch_endorsement::Error as LeError; + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + let err = le + .endorse(1) + .with(MeasurementKind::Launch) + .unwrap() + .with(MeasurementKind::Launch) + .unwrap_err(); + assert!(matches!(err, LeError::DuplicateSelection { .. })); + } + + #[test] + fn launch_endorsement_empty_selection_rejected() { + use crate::corim::launch_endorsement::Error as LeError; + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + let err = le.endorse(1).finish().unwrap_err(); + assert!(matches!(err, LeError::EmptySelection)); + } + + #[test] + fn add_corim_platform_mismatch_rejected() { + let file = make_snp_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + // LaunchEndorsement targets TDX but we call add_corim with SEV_SNP. + let template = launch_endorsement_template(IgvmPlatformType::TDX, 1); + let err = serializer + .add_corim(IgvmPlatformType::SEV_SNP, template) + .unwrap_err(); + assert!(err.to_string().contains("targets")); + } + + #[test] + fn add_corim_no_ces_triple_rejected() { + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + let file = make_snp_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + + // Populate a measurement but never call `endorse(...).finish()`. + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + + let err = serializer + .add_corim(IgvmPlatformType::SEV_SNP, le.build()) + .unwrap_err(); + assert!(err.to_string().contains("got 0"), "got: {err}"); + } + + #[test] + fn add_corim_multiple_ces_triples_rejected() { + use crate::corim::launch_endorsement::LaunchEndorsement; + use crate::corim::launch_endorsement::MeasurementKind; + let file = make_snp_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + + let mut le = LaunchEndorsement::for_platform(IgvmPlatformType::SEV_SNP).unwrap(); + le.set_measurement(MeasurementKind::Launch).unwrap(); + // Two CES triples in one endorsement. + le.endorse(1) + .with(MeasurementKind::Launch) + .unwrap() + .finish() + .unwrap(); + le.endorse(2) + .with(MeasurementKind::Launch) + .unwrap() + .finish() + .unwrap(); + + let err = serializer + .add_corim(IgvmPlatformType::SEV_SNP, le.build()) + .unwrap_err(); + assert!(err.to_string().contains("got 2"), "got: {err}"); + } + + #[test] + fn measurement_deterministic() { + let file = make_vbs_file(); + + let s1 = IgvmSerializer::new(&file).unwrap(); + let m1 = s1 + .measurement_for(IgvmPlatformType::VSM_ISOLATION) + .unwrap() + .digest + .clone(); + + let s2 = IgvmSerializer::new(&file).unwrap(); + let m2 = s2 + .measurement_for(IgvmPlatformType::VSM_ISOLATION) + .unwrap() + .digest + .clone(); + + assert_eq!(m1, m2); + } +}