Skip to main content

igvmfilegen/corim_signature/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Support for patching CoRIM (Concise Reference Integrity Manifest) headers
5//! into an existing IGVM file.
6//!
7//! CoRIM headers allow embedding signed endorsement for the IGVM file that
8//! can be verified by the attestation service.
9//!
10//! # Module structure
11//!
12//! - [`envelope`] -- operations on signed CoRIM envelopes (split a bundled
13//!   envelope into document + detached signature; cryptographically verify
14//!   a detached signature using the issuer cert from its `x5chain` /
15//!   `x5bag` header).
16//! - [`patch`] -- verify a CoRIM signature against the document
17//!   already embedded in an IGVM file, then patch (or replace) the
18//!   corresponding `CorimSignature` header.
19
20mod envelope;
21
22#[cfg(test)]
23mod test_helpers;
24
25// Re-export signed-CoRIM operations for use by main.rs and other consumers.
26pub use envelope::detach_payload;
27
28use anyhow::Context;
29use igvm::IgvmFile;
30use igvm::IgvmSerializer;
31use igvm_defs::IgvmPlatformType;
32
33/// Verify a CoRIM signature against the document already embedded in an
34/// IGVM file, then patch the corresponding `CorimSignature` header.
35///
36/// The CoRIM document is expected to already be present in the IGVM file
37/// for the target platform (auto-generated at build time). This function:
38///
39/// 1. Parses the IGVM file and locates the existing `CorimDocument` for
40///    the target platform.
41/// 2. If `expected_document` is provided, asserts that it matches the
42///    in-file document byte-for-byte. This catches the common UX trap
43///    where a user supplies a `--corim-bundle` whose embedded payload
44///    was signed against a different document than the one baked into
45///    the IGVM file: without this check the failure would surface as an
46///    opaque "cryptographic verification failed" error.
47/// 3. Cryptographically verifies `corim_signature` against the in-file
48///    document via [`envelope::verify_corim_signature`]. The issuer
49///    certificate is taken from the signature envelope's `x5chain` /
50///    `x5bag` header.
51/// 4. Stages the signature replacement via
52///    [`IgvmSerializer::set_corim_signature`], which suppresses any
53///    in-file document/signature pair for the platform and re-emits the
54///    existing document followed by the new signature in the required
55///    order, then serializes.
56///
57/// Verification runs before any serializer mutation, so a failed
58/// verification leaves no partially-modified output.
59///
60/// # Arguments
61/// * `igvm_data` - The original IGVM file contents
62/// * `corim_signature` - Detached COSE_Sign1 signature payload (nil payload)
63/// * `platform` - The target platform type
64/// * `expected_document` - Optional CoRIM document bytes that the caller
65///   asserts should match the document embedded in the IGVM file. Used
66///   when the signature was extracted from a bundled envelope; pass
67///   `None` when the caller doesn't have an independent copy.
68///
69/// # Returns
70/// The modified IGVM file contents with the CoRIM signature header
71/// inserted or updated.
72///
73/// # Errors
74/// Returns an error if the IGVM file has no `CorimDocument` header for
75/// the target platform -- the signature cannot be attached without a
76/// corresponding document -- or if `expected_document` is provided and
77/// does not match the in-file document, or if cryptographic verification
78/// of `corim_signature` against the in-file document fails.
79pub fn patch(
80    igvm_data: &[u8],
81    corim_signature: &[u8],
82    platform: IgvmPlatformType,
83    expected_document: Option<&[u8]>,
84) -> anyhow::Result<Vec<u8>> {
85    // Parse the IGVM file using the igvm crate's structured API.
86    let igvm_file =
87        IgvmFile::new_from_binary(igvm_data, None).context("parsing input IGVM file")?;
88
89    // Validate the target platform is present, so an unknown platform gives a
90    // clear "not found" error distinct from the "no document" case below.
91    crate::platform_mask::lookup_compatibility_mask(igvm_file.platforms(), platform)?;
92
93    // The serializer exposes the effective CoRIM document for a platform
94    // (`corim_for`), so we no longer scan initialization headers ourselves.
95    let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?;
96
97    // Verify the signature against the in-file document *before* any serializer
98    // mutation, so a failed verification leaves no partially-modified output.
99    // The immutable borrow of `existing_doc` is scoped to this block so the
100    // subsequent `&mut serializer` call to `set_corim_signature` is allowed.
101    {
102        let existing_doc = serializer.corim_for(platform).ok_or_else(|| {
103            anyhow::anyhow!(
104                "Cannot patch CoRIM signature for platform {platform:?}: no CoRIM \
105                 document present in the IGVM file. The document must be embedded \
106                 at IGVM generation time before a signature can be attached."
107            )
108        })?;
109
110        // If the caller supplied the document they think the signature was
111        // produced against (typically extracted from a bundled COSE_Sign1
112        // envelope), check that it matches the in-file document. This produces
113        // a targeted error before the cryptographic verify path would
114        // otherwise fail with an opaque "verification failed" message.
115        if let Some(expected) = expected_document
116            && expected != existing_doc
117        {
118            anyhow::bail!(
119                "CoRIM document mismatch for platform {platform:?}: the document \
120                 carried by the input bundle ({} bytes) does not byte-match the \
121                 document embedded in the IGVM file ({} bytes). The bundle was \
122                 signed against a different document; re-sign against the \
123                 IGVM-embedded document or supply only the detached signature \
124                 via `--corim-signature`.",
125                expected.len(),
126                existing_doc.len(),
127            );
128        }
129
130        // The issuer certificate is taken from the envelope's protected header
131        // (x5chain / x5bag).
132        envelope::verify_corim_signature(corim_signature, existing_doc)
133            .context("verifying CoRIM signature against the in-file document")?;
134    }
135
136    // Stage the signature replacement on the serializer. `set_corim_signature`
137    // suppresses the in-file document/signature pair for this platform and
138    // re-emits the existing document followed by the new signature, keeping
139    // the required document-before-signature ordering without us having to
140    // reconstruct the full IgvmFile.
141    serializer
142        .set_corim_signature(platform, corim_signature.to_vec())
143        .context("staging CoRIM signature replacement")?;
144
145    // Serialize back to binary. The igvm crate handles file offsets,
146    // alignment, and CRC32 checksum.
147    let mut output = Vec::new();
148    serializer
149        .serialize(&mut output)
150        .context("serializing patched IGVM file")?;
151
152    tracing::info!(
153        original_size = igvm_data.len(),
154        new_size = output.len(),
155        signature_size = corim_signature.len(),
156        platform = ?platform,
157        "Patched CoRIM signature into IGVM file",
158    );
159
160    Ok(output)
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::corim_signature::test_helpers::sign_envelope_for;
167    use igvm::IgvmDirectiveHeader;
168    use igvm::IgvmInitializationHeader;
169    use igvm::IgvmPlatformHeader;
170    use igvm::IgvmRevision;
171    use igvm::IgvmSerializer;
172    use igvm::corim::launch_measurement::LaunchMeasurement;
173    use igvm::corim::launch_measurement::MeasurementKind;
174    use igvm_defs::IGVM_FIXED_HEADER;
175    use igvm_defs::IGVM_VHS_SUPPORTED_PLATFORM;
176    use igvm_defs::IgvmPageDataFlags;
177    use igvm_defs::IgvmPageDataType;
178    use test_with_tracing::test;
179    use zerocopy::FromBytes;
180
181    fn new_platform(
182        compatibility_mask: u32,
183        platform_type: IgvmPlatformType,
184    ) -> IgvmPlatformHeader {
185        IgvmPlatformHeader::SupportedPlatform(IGVM_VHS_SUPPORTED_PLATFORM {
186            compatibility_mask,
187            highest_vtl: 0,
188            platform_type,
189            platform_version: 1,
190            shared_gpa_boundary: 0,
191        })
192    }
193
194    fn new_page_data(page: u64, compatibility_mask: u32, data: &[u8]) -> IgvmDirectiveHeader {
195        IgvmDirectiveHeader::PageData {
196            gpa: page * 4096,
197            compatibility_mask,
198            flags: IgvmPageDataFlags::new(),
199            data_type: IgvmPageDataType::NORMAL,
200            data: data.to_vec(),
201        }
202    }
203
204    /// Build `GuestPolicy` initialization headers for every SEV-SNP
205    /// platform present. `IgvmSerializer::new` eagerly measures all
206    /// measurable platforms, and SNP measurement requires a `GuestPolicy`
207    /// header -- real manifest-built files always carry one, so the test
208    /// helpers synthesize an equivalent here.
209    fn snp_guest_policies(platforms: &[IgvmPlatformHeader]) -> Vec<IgvmInitializationHeader> {
210        platforms
211            .iter()
212            .filter_map(|p| match p {
213                IgvmPlatformHeader::SupportedPlatform(info)
214                    if info.platform_type == IgvmPlatformType::SEV_SNP =>
215                {
216                    Some(IgvmInitializationHeader::GuestPolicy {
217                        policy: 0x30000,
218                        compatibility_mask: info.compatibility_mask,
219                    })
220                }
221                _ => None,
222            })
223            .collect()
224    }
225
226    /// Build a minimal IGVM binary from given headers (no CoRIM).
227    fn build_igvm(
228        platforms: Vec<IgvmPlatformHeader>,
229        directives: Vec<IgvmDirectiveHeader>,
230    ) -> Vec<u8> {
231        let initializations = snp_guest_policies(&platforms);
232        let igvm = IgvmFile::new(IgvmRevision::V1, platforms, initializations, directives)
233            .expect("valid IgvmFile");
234        let mut output = Vec::new();
235        igvm.serialize(&mut output).expect("serialize");
236        output
237    }
238
239    /// Build a minimal IGVM binary with pre-embedded CoRIM document(s).
240    /// `documents` is a list of `(compatibility_mask, document_bytes)`
241    /// pairs. The order of resulting `CorimDocument` initializations
242    /// matches the list order.
243    fn build_igvm_with_corim_docs(
244        platforms: Vec<IgvmPlatformHeader>,
245        directives: Vec<IgvmDirectiveHeader>,
246        documents: Vec<(u32, Vec<u8>)>,
247    ) -> Vec<u8> {
248        let mut initializations: Vec<IgvmInitializationHeader> = snp_guest_policies(&platforms);
249        initializations.extend(documents.into_iter().map(|(mask, doc)| {
250            IgvmInitializationHeader::CorimDocument {
251                compatibility_mask: mask,
252                document: doc,
253            }
254        }));
255        let igvm = IgvmFile::new(IgvmRevision::V1, platforms, initializations, directives)
256            .expect("valid IgvmFile");
257        let mut output = Vec::new();
258        igvm.serialize(&mut output).expect("serialize");
259        output
260    }
261
262    /// Extracted CoRIM header info for test assertions.
263    struct CorimHeaderInfo {
264        compatibility_mask: u32,
265        payload: Vec<u8>,
266    }
267
268    /// Parse an IGVM binary and extract CoRIM document and signature headers
269    /// using the structured `IgvmFile` API.
270    fn extract_corim_headers(data: &[u8]) -> (Vec<CorimHeaderInfo>, Vec<CorimHeaderInfo>) {
271        let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM file");
272        let mut documents = Vec::new();
273        let mut signatures = Vec::new();
274
275        for header in igvm.initializations() {
276            match header {
277                IgvmInitializationHeader::CorimDocument {
278                    compatibility_mask,
279                    document,
280                } => {
281                    documents.push(CorimHeaderInfo {
282                        compatibility_mask: *compatibility_mask,
283                        payload: document.clone(),
284                    });
285                }
286                IgvmInitializationHeader::CorimSignature {
287                    compatibility_mask,
288                    signature,
289                } => {
290                    signatures.push(CorimHeaderInfo {
291                        compatibility_mask: *compatibility_mask,
292                        payload: signature.clone(),
293                    });
294                }
295                _ => {}
296            }
297        }
298
299        (documents, signatures)
300    }
301
302    /// Count directive headers (excluding CoRIM) in the IGVM binary.
303    fn count_non_corim_directive_headers(data: &[u8]) -> usize {
304        let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM file");
305        igvm.directives().len()
306    }
307
308    /// Extract platform types and masks from the IGVM binary.
309    fn extract_platform_types(data: &[u8]) -> Vec<(IgvmPlatformType, u32)> {
310        let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM file");
311        igvm.platforms()
312            .iter()
313            .map(|p| match p {
314                IgvmPlatformHeader::SupportedPlatform(plat) => {
315                    (plat.platform_type, plat.compatibility_mask)
316                }
317            })
318            .collect()
319    }
320
321    #[test]
322    fn test_patch_corim_add_signature() {
323        let page_data = vec![0xCC; 4096];
324        let igvm_data = build_igvm_with_corim_docs(
325            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
326            vec![new_page_data(0, 0x1, &page_data)],
327            vec![(0x1, b"corim-payload".to_vec())],
328        );
329
330        let sig = sign_envelope_for(b"corim-payload", "test");
331        let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None)
332            .expect("patch should succeed");
333
334        let (docs, sigs) = extract_corim_headers(&patched);
335        assert_eq!(docs.len(), 1);
336        assert_eq!(sigs.len(), 1);
337        assert_eq!(docs[0].payload, b"corim-payload");
338        assert_eq!(sigs[0].payload, sig);
339        // Document and signature must share the same mask.
340        assert_eq!(docs[0].compatibility_mask, sigs[0].compatibility_mask);
341    }
342
343    #[test]
344    fn test_patch_corim_preserves_non_corim_directives() {
345        let data1 = vec![0x11; 4096];
346        let data2 = vec![0x22; 4096];
347        let igvm_data = build_igvm_with_corim_docs(
348            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
349            vec![new_page_data(0, 0x1, &data1), new_page_data(1, 0x1, &data2)],
350            vec![(0x1, b"doc".to_vec())],
351        );
352
353        let original_count = count_non_corim_directive_headers(&igvm_data);
354
355        let sig = sign_envelope_for(b"doc", "test");
356        let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None)
357            .expect("patch should succeed");
358
359        let patched_count = count_non_corim_directive_headers(&patched);
360        assert_eq!(original_count, patched_count);
361    }
362
363    #[test]
364    fn test_patch_corim_preserves_platform_headers() {
365        let data = vec![0x55; 4096];
366        let igvm_data = build_igvm_with_corim_docs(
367            vec![
368                new_platform(0x1, IgvmPlatformType::VSM_ISOLATION),
369                new_platform(0x2, IgvmPlatformType::SEV_SNP),
370            ],
371            vec![new_page_data(0, 0x1, &data), new_page_data(0, 0x2, &data)],
372            vec![(0x1, b"vbs-corim".to_vec())],
373        );
374
375        let original_platforms = extract_platform_types(&igvm_data);
376
377        let sig = sign_envelope_for(b"vbs-corim", "test");
378        let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None)
379            .expect("patch should succeed");
380
381        let patched_platforms = extract_platform_types(&patched);
382        assert_eq!(original_platforms, patched_platforms);
383    }
384
385    #[test]
386    fn test_patch_corim_uses_correct_mask() {
387        let data = vec![0x55; 4096];
388        let igvm_data = build_igvm_with_corim_docs(
389            vec![
390                new_platform(0x1, IgvmPlatformType::VSM_ISOLATION),
391                new_platform(0x2, IgvmPlatformType::SEV_SNP),
392            ],
393            vec![new_page_data(0, 0x1, &data), new_page_data(0, 0x2, &data)],
394            vec![(0x1, b"vbs-corim".to_vec()), (0x2, b"snp-corim".to_vec())],
395        );
396
397        // Patch signature for VBS only.
398        let sig = sign_envelope_for(b"vbs-corim", "test");
399        let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None)
400            .expect("patch should succeed");
401
402        let (docs, sigs) = extract_corim_headers(&patched);
403        assert_eq!(docs.len(), 2, "both platform docs preserved");
404        assert_eq!(sigs.len(), 1, "only VBS signature added");
405        assert_eq!(sigs[0].compatibility_mask, 0x1);
406    }
407
408    #[test]
409    fn test_patch_corim_error_platform_not_in_file() {
410        let igvm_data = build_igvm_with_corim_docs(
411            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
412            vec![new_page_data(0, 0x1, &vec![0; 4096])],
413            vec![(0x1, b"doc".to_vec())],
414        );
415
416        // Platform-lookup failure fires before signature verification,
417        // so the envelope contents are irrelevant here.
418        let sig = sign_envelope_for(b"doc", "test");
419        let result = patch(
420            &igvm_data,
421            &sig,
422            IgvmPlatformType::SEV_SNP, // Not in file
423            None,
424        );
425        assert!(result.is_err());
426        let msg = result.unwrap_err().to_string();
427        assert!(
428            msg.contains("not found"),
429            "expected 'not found' error, got: {msg}"
430        );
431    }
432
433    #[test]
434    fn test_patch_corim_error_missing_document() {
435        // Signature-only patching on a file without an existing document
436        // for the target platform must fail with a targeted error.
437        let igvm_data = build_igvm(
438            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
439            vec![new_page_data(0, 0x1, &vec![0; 4096])],
440        );
441
442        // Missing-document failure fires before signature verification.
443        let sig = sign_envelope_for(b"doc", "test");
444        let err = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None).unwrap_err();
445        let msg = format!("{err:#}");
446        assert!(
447            msg.contains("no CoRIM document"),
448            "expected 'no CoRIM document' error, got: {msg}"
449        );
450    }
451
452    #[test]
453    fn test_patch_corim_output_is_valid_igvm_header() {
454        let page_data = vec![0x77; 4096];
455        let igvm_data = build_igvm_with_corim_docs(
456            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
457            vec![new_page_data(0, 0x1, &page_data)],
458            vec![(0x1, b"round-trip-doc".to_vec())],
459        );
460
461        let sig = sign_envelope_for(b"round-trip-doc", "test");
462        let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None)
463            .expect("patch should succeed");
464
465        let fixed = IGVM_FIXED_HEADER::read_from_prefix(&patched)
466            .expect("valid fixed header")
467            .0;
468        assert_eq!(fixed.magic, igvm_defs::IGVM_MAGIC_VALUE);
469        assert_eq!(fixed.format_version, 1);
470        assert_eq!(fixed.total_file_size as usize, patched.len());
471
472        // `IgvmFile::new_from_binary` recomputes and verifies the CRC32
473        // over the variable header section. A successful re-parse here
474        // confirms the patched file's CRC32 was correctly recomputed.
475        IgvmFile::new_from_binary(&patched, None)
476            .expect("patched file must pass IGVM CRC32 validation");
477    }
478
479    #[test]
480    fn test_patch_corim_bundle_document_mismatch() {
481        // When the caller supplies a bundle whose payload differs from
482        // the document embedded in the IGVM file, `patch` must surface a
483        // targeted mismatch error before attempting cryptographic
484        // verification (which would otherwise fail with an opaque
485        // \"verification failed\" message).
486        let page_data = vec![0xAB; 4096];
487        let igvm_data = build_igvm_with_corim_docs(
488            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
489            vec![new_page_data(0, 0x1, &page_data)],
490            vec![(0x1, b"in-file-doc".to_vec())],
491        );
492
493        // Build a syntactically valid signature; the mismatch check must
494        // fire before envelope::verify_corim_signature is reached.
495        let sig = sign_envelope_for(b"in-file-doc", "test");
496
497        let err = patch(
498            &igvm_data,
499            &sig,
500            IgvmPlatformType::VSM_ISOLATION,
501            Some(b"different-bundled-doc"),
502        )
503        .unwrap_err();
504        let msg = format!("{err:#}");
505        assert!(
506            msg.contains("does not byte-match"),
507            "expected bundle/in-file mismatch error, got: {msg}"
508        );
509    }
510
511    #[test]
512    fn test_patch_corim_round_trip_reparse() {
513        // Verify that a patched file can be re-parsed and re-patched
514        // (round-trip through new_from_binary works after the igvm crate
515        // CoRIM parsing fix).
516        let page_data = vec![0xDD; 4096];
517        let igvm_data = build_igvm_with_corim_docs(
518            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
519            vec![new_page_data(0, 0x1, &page_data)],
520            vec![(0x1, b"first-doc".to_vec())],
521        );
522
523        let first_sig = sign_envelope_for(b"first-doc", "test");
524        let patched = patch(
525            &igvm_data,
526            &first_sig,
527            IgvmPlatformType::VSM_ISOLATION,
528            None,
529        )
530        .expect("first patch should succeed");
531
532        // Re-patching with a different signature must also succeed and
533        // must preserve the document.
534        let second_sig = sign_envelope_for(b"first-doc", "test-alt");
535        let repatched = patch(&patched, &second_sig, IgvmPlatformType::VSM_ISOLATION, None)
536            .expect("re-patching should succeed");
537
538        let (docs, sigs) = extract_corim_headers(&repatched);
539        assert_eq!(docs.len(), 1);
540        assert_eq!(sigs.len(), 1);
541        assert_eq!(docs[0].payload, b"first-doc");
542        assert_eq!(sigs[0].payload, second_sig);
543    }
544
545    #[test]
546    fn test_patch_corim_replace_signature_preserves_document() {
547        let page_data = vec![0xFF; 4096];
548        let igvm_data = build_igvm_with_corim_docs(
549            vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)],
550            vec![new_page_data(0, 0x1, &page_data)],
551            vec![(0x1, b"keep-this-doc".to_vec())],
552        );
553
554        // First: attach an initial signature.
555        let first_sig = sign_envelope_for(b"keep-this-doc", "test");
556        let with_sig = patch(
557            &igvm_data,
558            &first_sig,
559            IgvmPlatformType::VSM_ISOLATION,
560            None,
561        )
562        .expect("initial signature attach");
563
564        // Replace it with a different signature.
565        let second_sig = sign_envelope_for(b"keep-this-doc", "test-alt");
566        let updated = patch(
567            &with_sig,
568            &second_sig,
569            IgvmPlatformType::VSM_ISOLATION,
570            None,
571        )
572        .expect("signature replacement");
573
574        let (docs, sigs) = extract_corim_headers(&updated);
575        assert_eq!(docs.len(), 1);
576        assert_eq!(sigs.len(), 1);
577        assert_eq!(docs[0].payload, b"keep-this-doc");
578        assert_eq!(sigs[0].payload, second_sig);
579    }
580
581    /// Helper: build a multi-platform IGVM file with CoRIM documents AND
582    /// signatures already attached for both VBS (mask=0x1) and SNP
583    /// (mask=0x2). Returns the file along with the VBS and SNP signatures
584    /// so callers can use them in assertions.
585    fn build_multi_platform_with_corim() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
586        let data = vec![0x55; 4096];
587        let igvm_data = build_igvm_with_corim_docs(
588            vec![
589                new_platform(0x1, IgvmPlatformType::VSM_ISOLATION),
590                new_platform(0x2, IgvmPlatformType::SEV_SNP),
591            ],
592            vec![new_page_data(0, 0x1, &data), new_page_data(0, 0x2, &data)],
593            vec![(0x1, b"vbs-doc".to_vec()), (0x2, b"snp-doc".to_vec())],
594        );
595
596        // Attach signature to VBS.
597        let vbs_sig = sign_envelope_for(b"vbs-doc", "test");
598        let with_vbs = patch(&igvm_data, &vbs_sig, IgvmPlatformType::VSM_ISOLATION, None)
599            .expect("VBS signature attach");
600
601        // Attach signature to SNP.
602        let snp_sig = sign_envelope_for(b"snp-doc", "test");
603        let with_both = patch(&with_vbs, &snp_sig, IgvmPlatformType::SEV_SNP, None)
604            .expect("SNP signature attach");
605
606        (with_both, vbs_sig, snp_sig)
607    }
608
609    #[test]
610    fn test_multi_platform_corim_interleaved_ordering_is_valid() {
611        let (with_both, _vbs_sig, _snp_sig) = build_multi_platform_with_corim();
612
613        let (docs, sigs) = extract_corim_headers(&with_both);
614        assert_eq!(docs.len(), 2, "should have docs for both platforms");
615        assert_eq!(sigs.len(), 2, "should have sigs for both platforms");
616
617        let reparsed = IgvmFile::new_from_binary(&with_both, None)
618            .expect("interleaved CoRIM ordering should be parseable");
619
620        let corim_count = reparsed
621            .initializations()
622            .iter()
623            .filter(|h| {
624                matches!(
625                    h,
626                    IgvmInitializationHeader::CorimDocument { .. }
627                        | IgvmInitializationHeader::CorimSignature { .. }
628                )
629            })
630            .count();
631        assert_eq!(corim_count, 4, "should have 4 CoRIM init headers total");
632    }
633
634    #[test]
635    fn test_multi_platform_replace_signature_preserves_other_platform() {
636        // Replace SNP signature while VBS CoRIM is also present.
637        // VBS headers must be completely unchanged.
638        let (with_both, vbs_sig, _snp_sig) = build_multi_platform_with_corim();
639
640        let new_snp_sig = sign_envelope_for(b"snp-doc", "test-alt");
641        let updated = patch(&with_both, &new_snp_sig, IgvmPlatformType::SEV_SNP, None)
642            .expect("update SNP signature");
643
644        let (docs, sigs) = extract_corim_headers(&updated);
645        assert_eq!(docs.len(), 2);
646        assert_eq!(sigs.len(), 2);
647
648        let vbs_doc = docs.iter().find(|d| d.compatibility_mask == 0x1).unwrap();
649        let snp_doc = docs.iter().find(|d| d.compatibility_mask == 0x2).unwrap();
650        let vbs_sig_after = sigs.iter().find(|s| s.compatibility_mask == 0x1).unwrap();
651        let snp_sig_after = sigs.iter().find(|s| s.compatibility_mask == 0x2).unwrap();
652
653        assert_eq!(vbs_doc.payload, b"vbs-doc", "VBS doc must be unchanged");
654        assert_eq!(snp_doc.payload, b"snp-doc", "SNP doc preserved");
655        assert_eq!(vbs_sig_after.payload, vbs_sig, "VBS sig must be unchanged");
656        assert_eq!(
657            snp_sig_after.payload, new_snp_sig,
658            "SNP sig must be the new one"
659        );
660
661        IgvmFile::new_from_binary(&updated, None).expect("output should be valid IGVM");
662    }
663
664    #[test]
665    fn test_multi_platform_sequential_updates_both_platforms() {
666        // Update VBS first, then SNP. Verify both updates are reflected
667        // and the file remains valid after each step.
668        let (with_both, _vbs_sig, _snp_sig) = build_multi_platform_with_corim();
669
670        // Step 1: replace VBS signature.
671        let new_vbs_sig = sign_envelope_for(b"vbs-doc", "test-alt");
672        let after_vbs = patch(
673            &with_both,
674            &new_vbs_sig,
675            IgvmPlatformType::VSM_ISOLATION,
676            None,
677        )
678        .expect("VBS update");
679
680        IgvmFile::new_from_binary(&after_vbs, None).expect("valid after VBS update");
681
682        // Step 2: replace SNP signature.
683        let new_snp_sig = sign_envelope_for(b"snp-doc", "test-alt");
684        let after_snp =
685            patch(&after_vbs, &new_snp_sig, IgvmPlatformType::SEV_SNP, None).expect("SNP update");
686
687        let (docs, sigs) = extract_corim_headers(&after_snp);
688        assert_eq!(docs.len(), 2);
689        assert_eq!(sigs.len(), 2);
690
691        let vbs_doc = docs.iter().find(|d| d.compatibility_mask == 0x1).unwrap();
692        let snp_doc = docs.iter().find(|d| d.compatibility_mask == 0x2).unwrap();
693        let vbs_sig = sigs.iter().find(|s| s.compatibility_mask == 0x1).unwrap();
694        let snp_sig = sigs.iter().find(|s| s.compatibility_mask == 0x2).unwrap();
695
696        assert_eq!(vbs_doc.payload, b"vbs-doc", "VBS doc preserved");
697        assert_eq!(snp_doc.payload, b"snp-doc", "SNP doc preserved");
698        assert_eq!(vbs_sig.payload, new_vbs_sig, "VBS sig from step 1");
699        assert_eq!(snp_sig.payload, new_snp_sig, "SNP sig from step 2");
700
701        IgvmFile::new_from_binary(&after_snp, None).expect("valid after both updates");
702    }
703
704    /// End-to-end exercise of the production pipeline: build a real IGVM
705    /// file, attach a real CoRIM document via `IgvmSerializer::add_corim`
706    /// (the same call site `create_igvm_file` uses), then sign the
707    /// resulting CoRIM document with PS384 and patch the signature in via
708    /// `patch`. Verifies that the patched file still
709    /// parses, the original document survives unchanged, and the patched
710    /// signature matches what was produced from the real CoRIM bytes.
711    #[test]
712    fn test_e2e_real_corim_build_and_patch() {
713        let platform = IgvmPlatformType::VSM_ISOLATION;
714        let mask = 0x1;
715
716        // Build a minimal valid IGVM file with one platform and one page.
717        let page_data = vec![0xAA; 4096];
718        let base = build_igvm(
719            vec![new_platform(mask, platform)],
720            vec![new_page_data(0, mask, &page_data)],
721        );
722
723        // Attach a real CoRIM launch endorsement, exactly as the
724        // production `create_igvm_file` post-merge step does.
725        let parsed = IgvmFile::new_from_binary(&base, None).expect("parse base IGVM");
726        let mut serializer = IgvmSerializer::new(&parsed).expect("construct serializer");
727        let mut le = LaunchMeasurement::for_platform(platform).expect("launch endorsement");
728        le.set_measurement(MeasurementKind::Launch)
729            .expect("set measurement kind");
730        le.endorse(1)
731            .with(MeasurementKind::Launch)
732            .expect("CES with")
733            .finish()
734            .expect("CES finish");
735        let real_corim = serializer
736            .add_corim(platform, le.build())
737            .expect("add_corim")
738            .to_vec();
739
740        let mut with_doc = Vec::new();
741        serializer.serialize(&mut with_doc).expect("serialize");
742
743        // The serializer must have embedded exactly the CoRIM bytes it
744        // returned, and no signature should be present yet.
745        let (docs, sigs) = extract_corim_headers(&with_doc);
746        assert_eq!(docs.len(), 1, "one CoRIM document embedded");
747        assert!(sigs.is_empty(), "no signature before patch");
748        assert_eq!(
749            docs[0].payload, real_corim,
750            "embedded doc matches add_corim return"
751        );
752
753        // Sign the real CoRIM document with PS384 and patch the signature
754        // into the IGVM file.
755        let signature = sign_envelope_for(&real_corim, "e2e-test");
756        let patched = patch(&with_doc, &signature, platform, None).expect("patch signature");
757
758        // The patched file must still parse, preserve the real document,
759        // and now carry exactly the signature we produced.
760        IgvmFile::new_from_binary(&patched, None).expect("patched file parses");
761        let (docs, sigs) = extract_corim_headers(&patched);
762        assert_eq!(docs.len(), 1, "one CoRIM document after patch");
763        assert_eq!(sigs.len(), 1, "one CoRIM signature after patch");
764        assert_eq!(docs[0].compatibility_mask, mask);
765        assert_eq!(sigs[0].compatibility_mask, mask);
766        assert_eq!(docs[0].payload, real_corim, "real CoRIM doc preserved");
767        assert_eq!(sigs[0].payload, signature, "real signature attached");
768    }
769}