Skip to main content

igvmfilegen/
main.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Host-side command-line utility for constructing IGVM files.
5//!
6//! `igvmfilegen` combines a configuration manifest with firmware, kernel,
7//! initrd, boot-shim, and sidecar resources; lays them out for the selected
8//! platforms; and emits launch measurements and endorsement data.
9//!
10//! The OpenHCL build normally invokes this tool through `cargo xflowey
11//! build-igvm`, which supplies a supported manifest and all resource paths.
12//! Direct commands are useful when developing image layouts.
13
14#![forbid(unsafe_code)]
15
16#[cfg(not(test))]
17crypto::ensure_single_backend!();
18
19mod corim_signature;
20mod file_loader;
21mod firmware_dll;
22mod identity_mapping;
23mod measurement_diag;
24mod platform_mask;
25mod snp_id_block;
26mod snp_linux_direct;
27mod vp_context_builder;
28
29use crate::corim_signature::detach_payload;
30use crate::file_loader::IgvmLoader;
31use crate::file_loader::LoaderIsolationType;
32use crate::identity_mapping::Measurement;
33use crate::identity_mapping::SnpMeasurement;
34use crate::identity_mapping::TdxMeasurement;
35use crate::identity_mapping::VbsMeasurement;
36use crate::measurement_diag::log_measurement_diagnostic;
37use anyhow::Context;
38use anyhow::bail;
39use clap::Parser;
40use clap::ValueEnum;
41use file_loader::IgvmLoaderRegister;
42use file_loader::IgvmVtlLoader;
43use igvm::IgvmFile;
44use igvm::IgvmInitializationHeader;
45use igvm::IgvmPlatformHeader;
46use igvm::IgvmSerializer;
47use igvm::corim::launch_measurement::LaunchMeasurement;
48use igvm::corim::launch_measurement::MeasurementKind;
49use igvm_defs::IGVM_FIXED_HEADER;
50use igvm_defs::IgvmPlatformType;
51use igvm_defs::SnpPolicy;
52use igvm_defs::TdxPolicy;
53use igvmfilegen_config::Config;
54use igvmfilegen_config::ConfigIsolationType;
55use igvmfilegen_config::Image;
56use igvmfilegen_config::LinuxImage;
57use igvmfilegen_config::ResourceType;
58use igvmfilegen_config::Resources;
59use igvmfilegen_config::SecureAvicType;
60use igvmfilegen_config::SnpInjectionType;
61use igvmfilegen_config::UefiConfigType;
62use loader::importer::Aarch64Register;
63use loader::importer::GuestArch;
64use loader::importer::GuestArchKind;
65use loader::importer::ImageLoad;
66use loader::importer::X86Register;
67use loader::linux::InitrdConfig;
68use loader::paravisor::CommandLineType;
69use loader::paravisor::Vtl0Config;
70use loader::paravisor::Vtl0Linux;
71use product_policy::ProductPolicy;
72use std::io::Seek;
73use std::io::Write;
74use std::path::PathBuf;
75use tracing_subscriber::EnvFilter;
76use tracing_subscriber::filter::LevelFilter;
77use underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME;
78use zerocopy::FromBytes;
79use zerocopy::IntoBytes;
80
81#[derive(Parser)]
82#[clap(name = "igvmfilegen", about = "Tool to generate IGVM files")]
83enum Options {
84    /// Dumps the contents of an IGVM file in a human-readable format.
85    ///
86    /// Accepts either a raw IGVM file or a `vmfirmwareigvm` resource DLL
87    /// (`vmfirmwareigvm.dll` / `vmfirmwarecvm.dll`); for a DLL, the embedded
88    /// IGVM is extracted automatically.
89    // TODO: Move into its own tool.
90    Dump {
91        /// Dump file path
92        #[clap(short, long = "filepath")]
93        file_path: PathBuf,
94    },
95    /// Dump CoRIM (Concise Reference Integrity Manifest) headers and payloads from an IGVM file.
96    ///
97    /// This command scans the IGVM variable headers for CoRIM-related entries and prints or
98    /// extracts their contents. By default, all supported CoRIM headers for all platforms found
99    /// in the file are dumped. Use `--header-type` and `--platform` to narrow the selection.
100    ///
101    /// The input may be either a raw IGVM file or a `vmfirmwareigvm` resource DLL
102    /// (`vmfirmwareigvm.dll` / `vmfirmwarecvm.dll`); for a DLL, the embedded IGVM is
103    /// extracted automatically, so a shipped firmware DLL can be checked for a CoRIM directly.
104    ///
105    /// A human-readable summary of the selected CoRIM headers is written to stdout. When
106    /// `--output <dir>` is provided, the CoRIM payloads are also extracted to files in
107    /// that directory and the file paths are reported.
108    DumpCorim {
109        /// Input IGVM file (or `vmfirmwareigvm` resource DLL) to read CoRIM headers and payloads from.
110        #[clap(short, long = "filepath")]
111        file_path: PathBuf,
112        /// Filter by CoRIM header type (e.g. document or signature). If not specified,
113        /// all supported CoRIM header types in the IGVM file are included.
114        #[clap(long, value_enum)]
115        header_type: Option<CorimHeaderType>,
116        /// Filter by platform type for which the CoRIM applies (see `Platform` enum).
117        /// If not specified, CoRIM entries for all platforms present in the IGVM file
118        /// are considered.
119        #[clap(long, value_enum)]
120        platform: Option<Platform>,
121        /// Output directory to extract CoRIM payload data. For each matching CoRIM header,
122        /// the payload is written to a file named `corim_{document,signature}_<platform>.<ext>`,
123        /// e.g. `corim_document_vbs.cbor` or `corim_signature_snp.cose`.
124        /// If omitted, payload contents are not written as files and are instead described
125        /// in the textual output on stdout.
126        #[clap(short, long)]
127        output: Option<PathBuf>,
128    },
129    /// Build an IGVM file according to a manifest.
130    ///
131    /// Also emits per-platform sibling files next to `--output`:
132    /// `<base>-{snp,tdx,vbs}.json` (legacy identity documents) and
133    /// `<base>-{snp,tdx,vbs}.cbor` (CoRIM launch endorsements) for every
134    /// measurable platform in the manifest, and `<base>-snp.idblock` (the
135    /// SEV-SNP ID block signing payload) for SNP.
136    Manifest {
137        /// Config manifest file path
138        #[clap(short, long = "manifest")]
139        manifest: PathBuf,
140        /// Resources file describing binary resources used to build the igvm
141        /// file.
142        #[clap(short, long = "resources")]
143        resources: PathBuf,
144        /// Output file path for the built igvm file
145        #[clap(short = 'o', long)]
146        output: PathBuf,
147        /// Additional debug validation when building IGVM files
148        #[clap(long)]
149        debug_validation: bool,
150        /// Override secure AVIC to disabled for debug SNP guest configs
151        #[clap(long)]
152        disable_secure_avic: bool,
153        /// Add the confidential debug flag to the measured OpenHCL command
154        /// line, enabling confidential diagnostics on CVM guest configs even
155        /// in release builds.
156        ///
157        /// WARNING: This is security-sensitive. OpenHCL uses this flag to decide
158        /// whether it can trust host-provided boot options for isolated guests.
159        /// Only enable this flag if you understand the security implications.
160        #[clap(long)]
161        confidential_debug: bool,
162    },
163    /// Add a SEV-SNP ID block to an existing IGVM file.
164    ///
165    /// The IGVM file must contain an SEV-SNP platform header and a matching
166    /// `GuestPolicy`. The SNP launch measurement is computed once and embedded
167    /// as the ID block's launch digest (`ld`); the ID block is not part of the
168    /// measured page set, so the digest stays valid. Its presence signals the
169    /// IGVM loader to set `id_block_en` at launch. Adding a second ID block is
170    /// refused.
171    ///
172    /// Two signing modes are supported:
173    ///
174    /// * Out-of-band (production): pass `--id-block <signing payload>` (the
175    ///   `<base>-snp.idblock` emitted by `manifest`, which is the raw ID block
176    ///   bytes) together with `--id-signature <sig.der>` (a DER-encoded ECDSA
177    ///   signature produced by a file-content signer over those exact bytes,
178    ///   e.g. `openssl dgst -sha384 -sign key.pem -out sig.der file.idblock`)
179    ///   and `--id-public-key <key.pem>` (the signer's X.509 certificate or
180    ///   SPKI public key, PEM or DER). No private key is held by this tool.
181    ///   The guest SVN comes from the signing payload.
182    /// * Temporary key (development/test): pass `--guest-svn <N>` to sign an
183    ///   OpenHCL identity, or `--manifest <config.json>` to source both the SVN
184    ///   and image identity from its SNP guest config. An ephemeral ECDSA P-384
185    ///   key signs the block in-process.
186    AddSnpIdBlock {
187        /// Input IGVM file path
188        #[clap(short, long)]
189        input: PathBuf,
190        /// Output IGVM file path (can be the same as input to modify in place)
191        #[clap(short, long)]
192        output: PathBuf,
193        /// Temporary-key mode: guest security version number to embed using
194        /// the OpenHCL image identity.
195        /// Mutually exclusive with the out-of-band inputs and with `--manifest`.
196        #[clap(long, conflicts_with_all = ["manifest", "id_block", "id_signature", "id_public_key"])]
197        guest_svn: Option<u32>,
198        /// Temporary-key mode: source the guest SVN and image identity from
199        /// the SNP guest config in this manifest instead of `--guest-svn`.
200        #[clap(long, conflicts_with_all = ["guest_svn", "id_block", "id_signature", "id_public_key"])]
201        manifest: Option<PathBuf>,
202        /// Out-of-band mode: path to the SNP ID block signing payload
203        /// (`<base>-snp.idblock` emitted by `manifest`). Requires
204        /// `--id-signature` and `--id-public-key`.
205        #[clap(long, requires_all = ["id_signature", "id_public_key"], conflicts_with_all = ["guest_svn", "manifest"])]
206        id_block: Option<PathBuf>,
207        /// Out-of-band mode: path to the DER-encoded ECDSA signature over the
208        /// signing payload bytes. Requires `--id-block`.
209        #[clap(long, requires = "id_block")]
210        id_signature: Option<PathBuf>,
211        /// Out-of-band mode: path to the signer's public key (X.509 cert or
212        /// SPKI public key, PEM or DER). Requires `--id-block`.
213        #[clap(long, requires = "id_block")]
214        id_public_key: Option<PathBuf>,
215    },
216    /// Patch a CoRIM signature into an existing IGVM file for a given platform.
217    ///
218    /// The CoRIM document is generated automatically by `manifest` for every
219    /// measurable platform, so this command only attaches the detached
220    /// signature. Provide either a single bundled/signed CoRIM via
221    /// `--corim-bundle` (the tool splits it and uses the detached signature;
222    /// the IGVM file must already contain a matching CoRIM document) or an
223    /// already-detached signature via `--corim-signature` (the document slot
224    /// must already be populated in the IGVM file).
225    ///
226    /// What is verified: this command checks that the supplied signature
227    /// is a well-formed COSE_Sign1 envelope using PS384 (COSE alg -38,
228    /// RSA-PSS with SHA-384) and that the signature math validates against
229    /// the IGVM-embedded CoRIM document, using the public key carried in
230    /// the envelope's `x5chain` / `x5bag` header (RFC 9360).
231    ///
232    /// What is NOT verified: certificate-chain trust. The signing
233    /// certificate is taken from the envelope at face value -- no
234    /// validation against a trust root, no revocation check, no policy /
235    /// EKU enforcement. The caller is responsible for ensuring the input
236    /// signature originated from a trusted signer (e.g. by sourcing it
237    /// only from a controlled signing pipeline). Verification here exists
238    /// to catch accidental corruption and algorithm mismatches, not to
239    /// establish trust.
240    PatchCorimSignature {
241        /// Input IGVM file path
242        #[clap(short, long)]
243        input: PathBuf,
244        /// Output IGVM file path (can be the same as input to modify in place)
245        #[clap(short, long)]
246        output: PathBuf,
247        /// Path to a bundled/signed CoRIM file (COSE_Sign1 with embedded payload).
248        /// The tool will internally split it and use the detached signature;
249        /// the IGVM file must already contain a matching CoRIM document.
250        /// Mutually exclusive with `--corim-signature`.
251        ///
252        /// Only PS384 (COSE alg -38, RSA-PSS with SHA-384) signatures are
253        /// accepted; other algorithms are rejected at verify time.
254        #[clap(
255            long,
256            conflicts_with = "corim_signature",
257            required_unless_present = "corim_signature"
258        )]
259        corim_bundle: Option<PathBuf>,
260        /// Path to the CoRIM signature (COSE_Sign1 with nil payload) file.
261        /// Requires that a corresponding document already exists in the
262        /// file for the same compatibility mask.
263        ///
264        /// Only PS384 (COSE alg -38, RSA-PSS with SHA-384) signatures are
265        /// accepted; other algorithms are rejected at verify time.
266        #[clap(long)]
267        corim_signature: Option<PathBuf>,
268        /// Platform type for the CoRIM headers
269        #[clap(long, value_enum)]
270        platform: Platform,
271    },
272}
273
274/// IGVM platform types for CLI selection.
275///
276/// This is a CLI-friendly adapter for [`IgvmPlatformType`], which is an
277/// `open_enum` and cannot derive clap's `ValueEnum` directly.
278#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
279enum Platform {
280    /// AMD SEV-SNP
281    Snp,
282    /// Intel TDX
283    Tdx,
284    /// VBS (Virtualization Based Security)
285    Vbs,
286}
287
288impl From<Platform> for IgvmPlatformType {
289    fn from(platform: Platform) -> Self {
290        match platform {
291            Platform::Snp => IgvmPlatformType::SEV_SNP,
292            Platform::Tdx => IgvmPlatformType::TDX,
293            Platform::Vbs => IgvmPlatformType::VSM_ISOLATION,
294        }
295    }
296}
297
298/// CoRIM header types for filtering
299#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
300enum CorimHeaderType {
301    /// CoRIM document header
302    Document,
303    /// CoRIM signature header
304    Signature,
305}
306
307// TODO: Potential CLI flags:
308//       --report: Dump additional data like what memory ranges were accepted with what values
309
310fn main() -> anyhow::Result<()> {
311    let opts = Options::parse();
312    let filter = if std::env::var(EnvFilter::DEFAULT_ENV).is_ok() {
313        EnvFilter::from_default_env()
314    } else {
315        EnvFilter::default().add_directive(LevelFilter::INFO.into())
316    };
317    tracing_subscriber::fmt()
318        .log_internal_errors(true)
319        .with_writer(std::io::stderr)
320        .with_env_filter(filter)
321        .init();
322
323    match opts {
324        Options::Dump { file_path } => {
325            let image = firmware_dll::read_igvm_image(&file_path)?;
326            let fixed_header = IGVM_FIXED_HEADER::read_from_prefix(image.as_bytes())
327                .expect("Invalid fixed header")
328                .0; // TODO: zerocopy: use-rest-of-range (https://github.com/microsoft/openvmm/issues/759)
329
330            let igvm_data = IgvmFile::new_from_binary(&image, None).expect("should be valid");
331            println!("Total file size: {} bytes\n", fixed_header.total_file_size);
332            println!("{:#X?}", fixed_header);
333            println!("{}", igvm_data);
334            Ok(())
335        }
336        Options::DumpCorim {
337            file_path,
338            header_type,
339            platform,
340            output,
341        } => dump_corim_headers(&file_path, header_type, platform, output),
342        Options::Manifest {
343            manifest,
344            resources,
345            output,
346            debug_validation,
347            disable_secure_avic,
348            confidential_debug,
349        } => {
350            // Read the config from the JSON manifest path.
351            let mut config: Config = serde_json::from_str(
352                &fs_err::read_to_string(manifest).context("reading manifest")?,
353            )
354            .context("parsing manifest")?;
355
356            if disable_secure_avic {
357                for guest_config in &mut config.guest_configs {
358                    if let ConfigIsolationType::Snp { secure_avic, .. } =
359                        &mut guest_config.isolation_type
360                    {
361                        *secure_avic = SecureAvicType::Disabled;
362                    }
363                }
364            }
365
366            if confidential_debug {
367                for guest_config in &mut config.guest_configs {
368                    if let Image::Openhcl { command_line, .. } = &mut guest_config.image {
369                        if !command_line.is_empty() {
370                            command_line.push(' ');
371                        }
372                        command_line.push_str(OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME);
373                        command_line.push_str("=1");
374                    }
375                }
376            }
377
378            // Read resources and validate that it covers the required resources
379            // from the config.
380            let resources: Resources = serde_json::from_str(
381                &fs_err::read_to_string(resources).context("reading resources")?,
382            )
383            .context("parsing resources")?;
384
385            let required_resources = config.required_resources();
386            resources
387                .check_required(&required_resources)
388                .context("required resources not specified")?;
389
390            tracing::info!(
391                ?config,
392                ?resources,
393                "Building igvm file with given config and resources"
394            );
395
396            // Enable debug validation if specified or if running a debug build.
397            match config.guest_arch {
398                igvmfilegen_config::GuestArch::X64 => create_igvm_file::<X86Register>(
399                    config,
400                    resources,
401                    debug_validation || cfg!(debug_assertions),
402                    output,
403                ),
404                igvmfilegen_config::GuestArch::Aarch64 => create_igvm_file::<Aarch64Register>(
405                    config,
406                    resources,
407                    debug_validation || cfg!(debug_assertions),
408                    output,
409                ),
410            }
411        }
412        Options::AddSnpIdBlock {
413            input,
414            output,
415            guest_svn,
416            manifest,
417            id_block,
418            id_signature,
419            id_public_key,
420        } => add_snp_id_block_command(
421            input,
422            output,
423            guest_svn,
424            manifest,
425            id_block,
426            id_signature,
427            id_public_key,
428        ),
429        Options::PatchCorimSignature {
430            input,
431            output,
432            corim_bundle,
433            corim_signature,
434            platform,
435        } => patch_corim_signature(input, output, corim_bundle, corim_signature, platform),
436    }
437}
438
439/// Per-config measurement metadata captured during the build loop and
440/// consumed after merging to emit JSON identity documents and CoRIM
441/// launch endorsements.
442struct PlatformMeta {
443    platform: IgvmPlatformType,
444    svn: u32,
445    debug_enabled: bool,
446    snp_identity: Option<snp_id_block::SnpImageIdentity>,
447}
448
449/// Build a sibling path of `output` named `<base>-<isolation><ext>`,
450/// where `base` is `output`'s stem, `<isolation>` is derived from
451/// `meta.platform`, and `ext` includes the leading dot
452/// (e.g. `".json"` or `".cbor"`).
453fn sibling_path(
454    base: &std::ffi::OsStr,
455    output: &std::path::Path,
456    meta: &PlatformMeta,
457    ext: &str,
458) -> PathBuf {
459    let isolation = platform_mask::isolation_label(meta.platform);
460    let mut name = base.to_os_string();
461    name.push("-");
462    name.push(isolation);
463    name.push(ext);
464    output.with_file_name(name)
465}
466
467/// Build a `LaunchMeasurement` template for the given platform's
468/// launch measurement at the configured guest SVN.
469fn build_endorsement_corim(meta: &PlatformMeta) -> anyhow::Result<LaunchMeasurement> {
470    let mut le = LaunchMeasurement::for_platform(meta.platform)
471        .context("starting CoRIM launch endorsement")?;
472    le.set_measurement(MeasurementKind::Launch)
473        .context("setting CoRIM launch measurement kind")?;
474    le.endorse(meta.svn as u64)
475        .with(MeasurementKind::Launch)
476        .context("selecting CoRIM measurement in CES triple")?
477        .finish()
478        .context("finalizing CoRIM CES triple")?;
479    Ok(le)
480}
481
482/// Build a JSON identity document for a platform measurement.
483///
484/// The digest is produced by `IgvmSerializer::measurement_for(platform)`
485/// which contractually returns 48 bytes for SNP/TDX and 32 bytes for
486/// VBS; a length mismatch is an in-tree invariant violation and panics.
487/// `platform` is restricted to the three measurable platforms by the
488/// only call site (`create_igvm_file`'s `platform_metas` loop); any
489/// other value is `unreachable!`.
490fn build_endorsement_json(
491    platform: IgvmPlatformType,
492    digest: &[u8],
493    svn: u32,
494    debug_enabled: bool,
495) -> Measurement {
496    match platform {
497        IgvmPlatformType::SEV_SNP => {
498            let ld: [u8; 48] = digest.try_into().expect("SNP launch digest is 48 bytes");
499            Measurement::Snp(SnpMeasurement::new(ld, svn, debug_enabled))
500        }
501        IgvmPlatformType::TDX => {
502            let mrtd: [u8; 48] = digest.try_into().expect("TDX MRTD is 48 bytes");
503            Measurement::Tdx(TdxMeasurement::new(mrtd, svn, debug_enabled))
504        }
505        IgvmPlatformType::VSM_ISOLATION => {
506            let boot_digest: [u8; 32] = digest.try_into().expect("VBS boot digest is 32 bytes");
507            Measurement::Vbs(VbsMeasurement::new(boot_digest, svn, debug_enabled))
508        }
509        other => {
510            unreachable!("build_endorsement_json called for non-measurable platform {other:?}")
511        }
512    }
513}
514
515/// Write a per-platform sibling file (a JSON identity document, an SNP ID
516/// block signing payload, or a CoRIM launch endorsement) next to `output`,
517/// named `<base>-<isolation><ext>`. `ext` must include the leading dot.
518fn write_platform_sibling(
519    base: &std::ffi::OsStr,
520    output: &std::path::Path,
521    meta: &PlatformMeta,
522    ext: &str,
523    bytes: &[u8],
524) -> anyhow::Result<()> {
525    let path = sibling_path(base, output, meta, ext);
526    tracing::info!(
527        path = %path.display(),
528        size = bytes.len(),
529        "Writing sibling file",
530    );
531    fs_err::write(&path, bytes).context("writing sibling file")?;
532    Ok(())
533}
534
535/// Create an IGVM file from the specified config
536fn create_igvm_file<R: IgvmfilegenRegister + GuestArch + 'static>(
537    igvm_config: Config,
538    resources: Resources,
539    debug_validation: bool,
540    output: PathBuf,
541) -> anyhow::Result<()> {
542    tracing::debug!(?igvm_config, "Creating IGVM file",);
543
544    let mut igvm_file: Option<IgvmFile> = None;
545    let mut map_files = Vec::new();
546    let mut platform_metas: Vec<PlatformMeta> = Vec::new();
547    let base_path = output.file_stem().unwrap();
548    let has_snp_linux_direct = igvm_config
549        .guest_configs
550        .iter()
551        .any(|config| matches!(&config.image, Image::SnpLinuxDirect { .. }));
552    if has_snp_linux_direct && igvm_config.guest_configs.len() != 1 {
553        bail!("snp_linux_direct must be the only guest config in an IGVM file");
554    }
555
556    for config in igvm_config.guest_configs {
557        // Max VTL must be 2 or 0.
558        if config.max_vtl != 2 && config.max_vtl != 0 {
559            bail!("max_vtl must be 2 or 0");
560        }
561
562        config.image.validate().context("invalid image config")?;
563
564        let loader_isolation_type = match &config.isolation_type {
565            ConfigIsolationType::None => LoaderIsolationType::None,
566            ConfigIsolationType::Vbs { enable_debug } => LoaderIsolationType::Vbs {
567                enable_debug: *enable_debug,
568            },
569            ConfigIsolationType::Snp {
570                shared_gpa_boundary_bits,
571                policy,
572                enable_debug,
573                injection_type,
574                secure_avic,
575            } => LoaderIsolationType::Snp {
576                shared_gpa_boundary_bits: *shared_gpa_boundary_bits,
577                policy: SnpPolicy::from(*policy).with_debug(*enable_debug as u8),
578                injection_type: match injection_type {
579                    SnpInjectionType::Normal => vp_context_builder::snp::InjectionType::Normal,
580                    SnpInjectionType::Restricted => {
581                        vp_context_builder::snp::InjectionType::Restricted
582                    }
583                },
584                secure_avic: match secure_avic {
585                    SecureAvicType::Enabled => vp_context_builder::snp::SecureAvic::Enabled,
586                    SecureAvicType::Disabled => vp_context_builder::snp::SecureAvic::Disabled,
587                },
588            },
589            ConfigIsolationType::Tdx {
590                enable_debug,
591                sept_ve_disable,
592            } => LoaderIsolationType::Tdx {
593                policy: TdxPolicy::new()
594                    .with_debug_allowed(*enable_debug as u8)
595                    .with_sept_ve_disable(*sept_ve_disable as u8),
596            },
597        };
598
599        // Track measurement metadata for measurable platforms so the
600        // post-merge step can look up the digest from `IgvmSerializer`
601        // and emit a JSON identity document + CoRIM launch endorsement.
602        //
603        // Each measurable platform type may appear at most once across
604        // all guest configs: the post-merge step keys both the
605        // `IgvmSerializer::measurement_for(platform)` lookup and the
606        // `<base>-<isolation>.{cbor,json}` sibling filenames purely by
607        // platform type, so a duplicate would silently overwrite the
608        // earlier artifacts and could pair the wrong svn/debug bit with
609        // the merged measurement. Fail fast here with a clear error.
610        let platform = match &loader_isolation_type {
611            LoaderIsolationType::Snp { .. } => Some(IgvmPlatformType::SEV_SNP),
612            LoaderIsolationType::Tdx { .. } => Some(IgvmPlatformType::TDX),
613            LoaderIsolationType::Vbs { .. } => Some(IgvmPlatformType::VSM_ISOLATION),
614            LoaderIsolationType::None => None,
615        };
616        if let Some(platform) = platform
617            && platform_metas.iter().any(|m| m.platform == platform)
618        {
619            bail!(
620                "manifest contains more than one guest config for measurable platform {platform:?}; \
621                 at most one is supported because endorsement artifacts and the post-merge \
622                 measurement lookup are keyed by platform type"
623            );
624        }
625        match &loader_isolation_type {
626            LoaderIsolationType::Snp { policy, .. } => {
627                platform_metas.push(PlatformMeta {
628                    platform: IgvmPlatformType::SEV_SNP,
629                    svn: config.guest_svn,
630                    debug_enabled: policy.debug() == 1,
631                    snp_identity: Some(if matches!(&config.image, Image::SnpLinuxDirect { .. }) {
632                        snp_id_block::SnpImageIdentity::LINUX_DIRECT
633                    } else {
634                        snp_id_block::SnpImageIdentity::OPENHCL
635                    }),
636                });
637            }
638            LoaderIsolationType::Tdx { policy } => {
639                platform_metas.push(PlatformMeta {
640                    platform: IgvmPlatformType::TDX,
641                    svn: config.guest_svn,
642                    debug_enabled: policy.debug_allowed() == 1,
643                    snp_identity: None,
644                });
645            }
646            LoaderIsolationType::Vbs { enable_debug } => {
647                platform_metas.push(PlatformMeta {
648                    platform: IgvmPlatformType::VSM_ISOLATION,
649                    svn: config.guest_svn,
650                    debug_enabled: *enable_debug,
651                    snp_identity: None,
652                });
653            }
654            LoaderIsolationType::None => {}
655        }
656
657        let igvm_output: file_loader::IgvmOutput = match &config.image {
658            Image::SnpLinuxDirect {
659                linux,
660                processor_count,
661                memory_page_count,
662                c_bit_position,
663            } => {
664                if config.max_vtl != 0 {
665                    bail!("snp_linux_direct requires max_vtl 0");
666                }
667                let ConfigIsolationType::Snp {
668                    shared_gpa_boundary_bits,
669                    policy,
670                    enable_debug,
671                    injection_type,
672                    secure_avic,
673                } = &config.isolation_type
674                else {
675                    bail!("snp_linux_direct requires SNP isolation");
676                };
677                if shared_gpa_boundary_bits.is_some() {
678                    bail!("snp_linux_direct does not support a shared GPA boundary");
679                }
680                if !matches!(secure_avic, SecureAvicType::Disabled) {
681                    bail!("snp_linux_direct requires secure AVIC to be disabled");
682                }
683
684                R::build_snp_linux_direct(snp_linux_direct::BuildParams {
685                    linux,
686                    processor_count: *processor_count,
687                    memory_page_count: *memory_page_count,
688                    c_bit_position: *c_bit_position,
689                    policy: SnpPolicy::from(*policy).with_debug(*enable_debug as u8),
690                    injection_type,
691                    resources: &resources,
692                })?
693            }
694            _ => {
695                // Max VTL of 2 implies paravisor.
696                let with_paravisor = config.max_vtl == 2;
697                let mut loader = IgvmLoader::<R>::new(with_paravisor, loader_isolation_type);
698                load_image(&mut loader.loader(), &config.image, &resources)?;
699                loader.finalize().context("finalizing loader")?
700            }
701        };
702
703        igvm_output.map.emit_tracing();
704
705        // Merge the loaded guest into the overall IGVM file.
706        match &mut igvm_file {
707            Some(file) => file
708                .merge_simple(igvm_output.guest)
709                .context("merging guest into overall igvm file")?,
710            None => igvm_file = Some(igvm_output.guest),
711        }
712
713        map_files.push(igvm_output.map);
714    }
715
716    let Some(igvm_file) = igvm_file else {
717        bail!("manifest contained no guest configs");
718    };
719
720    // Construct the serializer once on the merged IGVM file. This eagerly
721    // computes the launch measurement for every measurable platform and
722    // serves as the single source of truth for the digest used in the JSON
723    // identity documents, the CoRIM launch endorsements, and the SNP ID
724    // block signing payload.
725    let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?;
726
727    // For each measurable platform: log the diagnostic, attach a CoRIM
728    // launch endorsement, then write the sibling files (CoRIM document,
729    // JSON identity document, and for SNP the ID block signing payload).
730    // The CoRIM is attached before any sibling file is written so a failure
731    // in `add_corim` leaves no half-written artifact set on disk.
732    for meta in &platform_metas {
733        // Snapshot the digest so the immutable borrow on `serializer`
734        // ends before the `&mut serializer` call to `add_corim` below.
735        let (digest, compatibility_mask) = {
736            let m = serializer.measurement_for(meta.platform).with_context(|| {
737                format!("no measurement computed for platform {:?}", meta.platform)
738            })?;
739            (m.digest.clone(), m.compatibility_mask)
740        };
741
742        // Emit the platform-specific launch-measurement diagnostic
743        // structure (VBS signed data, SNP ID block, TDX MRTD) for human
744        // inspection. The digest itself does not depend on these inputs.
745        log_measurement_diagnostic(
746            meta.platform,
747            &digest,
748            meta.svn,
749            meta.debug_enabled,
750            serializer.file(),
751            compatibility_mask,
752        );
753
754        let corim = build_endorsement_corim(meta)?;
755        let corim_bytes = serializer
756            .add_corim(meta.platform, corim.build())
757            .context("adding CoRIM document to IGVM serializer")?
758            .to_vec();
759
760        // Write the CoRIM document first since it is the new artifact
761        // downstream signing tooling keys on; the JSON identity document
762        // is the legacy companion.
763        write_platform_sibling(base_path, &output, meta, ".cbor", &corim_bytes)?;
764
765        let json = build_endorsement_json(meta.platform, &digest, meta.svn, meta.debug_enabled);
766        let mut json_bytes =
767            serde_json::to_vec(&json).expect("serializing measurement JSON cannot fail");
768        json_bytes.push(b'\n');
769        write_platform_sibling(base_path, &output, meta, ".json", &json_bytes)?;
770
771        // For SEV-SNP, also emit the SNP ID block signing payload. An
772        // out-of-band signer signs it, and the result is applied
773        // later via `add-snp-id-block --id-block ... --id-signature ...`.
774        if meta.platform == IgvmPlatformType::SEV_SNP {
775            let policy = snp_id_block::guest_policy(serializer.file(), compatibility_mask)
776                .context("SNP GuestPolicy missing; cannot emit SNP ID block signing payload")?;
777            let identity = meta
778                .snp_identity
779                .expect("SNP platform metadata includes an image identity");
780            let signing_payload = if identity == snp_id_block::SnpImageIdentity::OPENHCL {
781                snp_id_block::id_block_signing_payload(&digest, meta.svn, policy)?
782            } else {
783                snp_id_block::id_block_signing_payload_with_identity(
784                    &digest, meta.svn, policy, identity,
785                )?
786            };
787            write_platform_sibling(base_path, &output, meta, ".idblock", &signing_payload)?;
788        }
789    }
790
791    let mut igvm_binary = Vec::new();
792    serializer
793        .serialize(&mut igvm_binary)
794        .context("serializing igvm")?;
795
796    // If enabled, perform additional validation by round-tripping the
797    // serialized binary through the parser and re-serializer.
798    if debug_validation {
799        debug_validate_igvm_file(&igvm_binary);
800    }
801
802    // Write the IGVM file to the specified file path in the config.
803    tracing::info!(
804        path = %output.display(),
805        "Writing output IGVM file",
806    );
807    fs_err::File::create(&output)
808        .context("creating igvm file")?
809        .write_all(&igvm_binary)
810        .context("writing igvm file")?;
811
812    // Write the map file display output to a file with the same name, but .map
813    // extension.
814    let map_path = {
815        let mut name = output.file_name().expect("has name").to_owned();
816        name.push(".map");
817        output.with_file_name(name)
818    };
819    tracing::info!(
820        path = %map_path.display(),
821        "Writing output map file",
822    );
823    let mut map_file = fs_err::File::create(map_path).context("creating map file")?;
824
825    for map in map_files {
826        writeln!(map_file, "{}", map).context("writing map file")?;
827    }
828
829    Ok(())
830}
831
832/// Atomically write `data` to `output`: write to a sibling temp file, then
833/// rename it into place. This prevents a crash or interruption during the
834/// write from corrupting the output (which may be the same file as the input
835/// for in-place edits). Same-volume renames are atomic on both POSIX and
836/// Windows (`std::fs::rename` uses `MoveFileExW` with
837/// `MOVEFILE_REPLACE_EXISTING`). `log_message` is emitted before the write.
838fn write_igvm_file_atomic(
839    output: &std::path::Path,
840    data: &[u8],
841    log_message: &str,
842) -> anyhow::Result<()> {
843    // Sibling temp file; not `with_extension`, which would replace any existing ext.
844    let temp_path = {
845        let mut s = output.as_os_str().to_owned();
846        s.push(".tmp");
847        PathBuf::from(s)
848    };
849
850    tracing::info!(
851        path = %output.display(),
852        size = data.len(),
853        "{log_message}",
854    );
855    fs_err::write(&temp_path, data)
856        .with_context(|| format!("writing temporary IGVM file at {}", temp_path.display()))?;
857
858    fs_err::rename(&temp_path, output).with_context(|| {
859        format!(
860            "renaming temporary file {} to {}",
861            temp_path.display(),
862            output.display()
863        )
864    })?;
865
866    Ok(())
867}
868
869/// Add a SEV-SNP ID block to an existing IGVM file, either from an out-of-band
870/// signature (production) or signed with an ephemeral key (development/test).
871fn add_snp_id_block_command(
872    input: PathBuf,
873    output: PathBuf,
874    guest_svn: Option<u32>,
875    manifest: Option<PathBuf>,
876    id_block: Option<PathBuf>,
877    id_signature: Option<PathBuf>,
878    id_public_key: Option<PathBuf>,
879) -> anyhow::Result<()> {
880    let igvm_data = fs_err::read(&input)
881        .with_context(|| format!("reading input IGVM file at {}", input.display()))?;
882
883    let new_igvm = if let Some(id_block_path) = id_block {
884        // Out-of-band (production) mode. clap guarantees `id_signature` and
885        // `id_public_key` are present whenever `id_block` is.
886        let sig_path =
887            id_signature.expect("clap ensures --id-signature is present with --id-block");
888        let key_path =
889            id_public_key.expect("clap ensures --id-public-key is present with --id-block");
890        let signing_payload = fs_err::read(&id_block_path).with_context(|| {
891            format!(
892                "reading SNP ID block signing payload at {}",
893                id_block_path.display()
894            )
895        })?;
896        let signature_der = fs_err::read(&sig_path)
897            .with_context(|| format!("reading SNP ID block signature at {}", sig_path.display()))?;
898        let public_key = fs_err::read(&key_path).with_context(|| {
899            format!("reading SNP ID block public key at {}", key_path.display())
900        })?;
901
902        tracing::info!(
903            input = %input.display(),
904            output = %output.display(),
905            id_block = %id_block_path.display(),
906            signature = %sig_path.display(),
907            public_key = %key_path.display(),
908            "Adding SNP ID block (out-of-band signature)"
909        );
910        snp_id_block::add_snp_id_block_signed(
911            &igvm_data,
912            &signing_payload,
913            &signature_der,
914            &public_key,
915        )?
916    } else {
917        // Temporary-key (development/test) mode. Raw `--guest-svn` preserves
918        // the original OpenHCL behavior; a manifest selects both fields.
919        let (svn, identity) = if let Some(svn) = guest_svn {
920            (svn, snp_id_block::SnpImageIdentity::OPENHCL)
921        } else if let Some(manifest_path) = manifest {
922            let config: Config = serde_json::from_str(
923                &fs_err::read_to_string(&manifest_path)
924                    .with_context(|| format!("reading manifest at {}", manifest_path.display()))?,
925            )
926            .with_context(|| format!("parsing manifest at {}", manifest_path.display()))?;
927            snp_temporary_signing_metadata_from_config(&config)?
928        } else {
929            bail!(
930                "provide either --guest-svn/--manifest (temporary-key signing) \
931                 or --id-block with --id-signature and --id-public-key (out-of-band signing)"
932            );
933        };
934
935        tracing::info!(
936            input = %input.display(),
937            output = %output.display(),
938            guest_svn = svn,
939            ?identity,
940            "Adding SNP ID block (temporary key)"
941        );
942        snp_id_block::add_snp_id_block_temp_key(&igvm_data, svn, identity)?
943    };
944
945    write_igvm_file_atomic(&output, &new_igvm, "Writing IGVM file with SNP ID block")
946}
947
948/// Extract temporary-signing metadata from the single SNP guest config.
949fn snp_temporary_signing_metadata_from_config(
950    config: &Config,
951) -> anyhow::Result<(u32, snp_id_block::SnpImageIdentity)> {
952    let mut snp_guests = config
953        .guest_configs
954        .iter()
955        .filter(|c| matches!(&c.isolation_type, ConfigIsolationType::Snp { .. }));
956    let guest = snp_guests.next().context(
957        "manifest has no SEV-SNP guest config to source temporary-signing metadata from",
958    )?;
959    anyhow::ensure!(
960        snp_guests.next().is_none(),
961        "manifest has more than one SEV-SNP guest config; cannot unambiguously \
962         source temporary-signing metadata"
963    );
964    let identity = if matches!(&guest.image, Image::SnpLinuxDirect { .. }) {
965        snp_id_block::SnpImageIdentity::LINUX_DIRECT
966    } else {
967        snp_id_block::SnpImageIdentity::OPENHCL
968    };
969    Ok((guest.guest_svn, identity))
970}
971
972/// Dump CoRIM headers from an IGVM file.
973fn dump_corim_headers(
974    file_path: &std::path::Path,
975    header_type_filter: Option<CorimHeaderType>,
976    platform_filter: Option<Platform>,
977    output_dir: Option<PathBuf>,
978) -> anyhow::Result<()> {
979    let image = firmware_dll::read_igvm_image(file_path)?;
980
981    // Parse the IGVM file using the igvm crate's structured API.
982    let igvm_file = IgvmFile::new_from_binary(&image, None).context("parsing IGVM file")?;
983
984    let fixed_header = IGVM_FIXED_HEADER::read_from_prefix(image.as_slice())
985        .map_err(|_| anyhow::anyhow!("Invalid IGVM file: cannot read fixed header"))?
986        .0; // TODO: zerocopy: use-rest-of-range (https://github.com/microsoft/openvmm/issues/759)
987
988    println!("IGVM File: {}", file_path.display());
989    println!("Total file size: {} bytes", fixed_header.total_file_size);
990    println!();
991
992    // The output directory is created lazily on the first matching header
993    // so a filter that matches nothing doesn't leave an empty directory
994    // behind.
995    let mut output_dir_created = false;
996
997    let platforms = igvm_file.platforms();
998
999    // Print the supported platform table
1000    if !platforms.is_empty() {
1001        println!("Supported Platforms:");
1002        for header in platforms {
1003            match header {
1004                IgvmPlatformHeader::SupportedPlatform(info) => {
1005                    println!(
1006                        "  {:?} -> compatibility_mask 0x{:X}",
1007                        info.platform_type, info.compatibility_mask
1008                    );
1009                }
1010            }
1011        }
1012        println!();
1013    }
1014
1015    // Convert platform filter to compatibility mask using the file's actual mapping
1016    let platform_mask_filter = platform_filter
1017        .map(|p| platform_mask::lookup_compatibility_mask(platforms, IgvmPlatformType::from(p)))
1018        .transpose()?;
1019
1020    // Iterate through initialization headers looking for CoRIM entries
1021    let mut document_count: usize = 0;
1022    let mut signature_count: usize = 0;
1023
1024    for header in igvm_file.initializations() {
1025        let (kind, label, extension, compatibility_mask, payload) = match header {
1026            IgvmInitializationHeader::CorimDocument {
1027                compatibility_mask,
1028                document,
1029            } => (
1030                CorimHeaderType::Document,
1031                "Document",
1032                "cbor",
1033                *compatibility_mask,
1034                document.as_slice(),
1035            ),
1036            IgvmInitializationHeader::CorimSignature {
1037                compatibility_mask,
1038                signature,
1039            } => (
1040                CorimHeaderType::Signature,
1041                "Signature",
1042                "cose",
1043                *compatibility_mask,
1044                signature.as_slice(),
1045            ),
1046            _ => continue,
1047        };
1048
1049        let show_type = header_type_filter.is_none_or(|t| t == kind);
1050        let show_platform = platform_mask_filter.is_none_or(|mask| compatibility_mask & mask != 0);
1051
1052        if !show_type || !show_platform {
1053            continue;
1054        }
1055
1056        match kind {
1057            CorimHeaderType::Document => document_count += 1,
1058            CorimHeaderType::Signature => signature_count += 1,
1059        }
1060
1061        let platform_name = platform_mask::platform_name_for_mask(platforms, compatibility_mask);
1062
1063        println!("CoRIM {label} ({platform_name}):");
1064        println!(
1065            "  Compatibility Mask: 0x{compatibility_mask:X} ({})",
1066            platform_mask::format_platform_mask(platforms, compatibility_mask)
1067        );
1068        println!("  Size: {} bytes", payload.len());
1069
1070        if let Some(ref dir) = output_dir {
1071            if !output_dir_created {
1072                fs_err::create_dir_all(dir).context("creating output directory")?;
1073                output_dir_created = true;
1074            }
1075            let file_prefix = label.to_lowercase();
1076            let output_file = dir.join(format!("corim_{file_prefix}_{platform_name}.{extension}"));
1077            fs_err::write(&output_file, payload)
1078                .with_context(|| format!("writing {label} payload to {}", output_file.display()))?;
1079            println!("  Output: {}", output_file.display());
1080        }
1081        println!();
1082    }
1083
1084    if document_count == 0 && signature_count == 0 {
1085        println!("No CoRIM headers found matching the specified filters.");
1086    } else {
1087        println!(
1088            "Summary: {} document header(s), {} signature header(s)",
1089            document_count, signature_count
1090        );
1091    }
1092
1093    Ok(())
1094}
1095
1096/// Patch a CoRIM signature into an existing IGVM file.
1097fn patch_corim_signature(
1098    input: PathBuf,
1099    output: PathBuf,
1100    corim_bundle: Option<PathBuf>,
1101    corim_signature: Option<PathBuf>,
1102    platform: Platform,
1103) -> anyhow::Result<()> {
1104    let igvm_data = fs_err::read(&input)
1105        .with_context(|| format!("reading input IGVM file at {}", input.display()))?;
1106
1107    let platform_type = IgvmPlatformType::from(platform);
1108
1109    // The CoRIM document is expected to already be embedded in the IGVM file
1110    // for the target platform (auto-generated at build time). `corim_signature::patch`
1111    // looks it up internally and verifies `signature_data` against it before
1112    // mutating the file. The issuer certificate is carried in the
1113    // signature's COSE protected header (x5chain / x5bag, RFC 9360); no
1114    // separate cert input needed.
1115    //
1116    // When the user supplies a `--corim-bundle`, that bundle's embedded
1117    // payload is the document the signature was actually produced over;
1118    // we forward it so `patch` can sanity-check it against the
1119    // IGVM-embedded document and surface a targeted mismatch error
1120    // instead of an opaque cryptographic verify failure.
1121    let (signature_data, bundle_document) = if let Some(bundle_path) = &corim_bundle {
1122        let bundle_data = fs_err::read(bundle_path)
1123            .with_context(|| format!("reading bundled CoRIM file at {}", bundle_path.display()))?;
1124
1125        let detached = detach_payload(&bundle_data).context("splitting bundled CoRIM")?;
1126        tracing::info!(
1127            path = %bundle_path.display(),
1128            bundle_size = bundle_data.len(),
1129            document_size = detached.document.len(),
1130            signature_size = detached.signature.len(),
1131            "Split bundled signed CoRIM into document and detached signature"
1132        );
1133        (detached.signature, Some(detached.document))
1134    } else {
1135        let path = corim_signature
1136            .as_ref()
1137            .context("one of --corim-bundle or --corim-signature must be provided")?;
1138        let sig = fs_err::read(path)
1139            .with_context(|| format!("reading CoRIM signature file at {}", path.display()))?;
1140        (sig, None)
1141    };
1142
1143    tracing::info!(
1144        input = %input.display(),
1145        output = %output.display(),
1146        bundle = ?corim_bundle,
1147        signature = ?corim_signature,
1148        platform = ?platform,
1149        "Patching CoRIM signature into IGVM file"
1150    );
1151
1152    let patched_igvm = corim_signature::patch(
1153        &igvm_data,
1154        &signature_data,
1155        platform_type,
1156        bundle_document.as_deref(),
1157    )?;
1158
1159    write_igvm_file_atomic(&output, &patched_igvm, "Writing patched IGVM file")
1160}
1161
1162/// Validate that the serialized IGVM file round-trips through the parser
1163/// and re-serializer producing identical structural headers.
1164// TODO: should live in the igvm crate
1165fn debug_validate_igvm_file(binary_file: &[u8]) {
1166    use igvm::IgvmDirectiveHeader;
1167    tracing::info!("Debug validation of serialized IGVM file.");
1168
1169    let igvm_file =
1170        IgvmFile::new_from_binary(binary_file, None).expect("first parse should succeed");
1171
1172    let mut reserialized = Vec::new();
1173    igvm_file
1174        .serialize(&mut reserialized)
1175        .expect("re-serialize should succeed");
1176
1177    let igvm_reserialized =
1178        IgvmFile::new_from_binary(&reserialized, None).expect("re-parse should succeed");
1179
1180    for (a, b) in igvm_file
1181        .platforms()
1182        .iter()
1183        .zip(igvm_reserialized.platforms().iter())
1184    {
1185        assert_eq!(a, b);
1186    }
1187
1188    for (a, b) in igvm_file
1189        .initializations()
1190        .iter()
1191        .zip(igvm_reserialized.initializations().iter())
1192    {
1193        assert_eq!(a, b);
1194    }
1195
1196    for (a, b) in igvm_file
1197        .directives()
1198        .iter()
1199        .zip(igvm_reserialized.directives().iter())
1200    {
1201        match (a, b) {
1202            (
1203                IgvmDirectiveHeader::PageData {
1204                    gpa: a_gpa,
1205                    flags: a_flags,
1206                    data_type: a_data_type,
1207                    data: a_data,
1208                    compatibility_mask: a_compmask,
1209                },
1210                IgvmDirectiveHeader::PageData {
1211                    gpa: b_gpa,
1212                    flags: b_flags,
1213                    data_type: b_data_type,
1214                    data: b_data,
1215                    compatibility_mask: b_compmask,
1216                },
1217            ) => {
1218                assert!(
1219                    a_gpa == b_gpa
1220                        && a_flags == b_flags
1221                        && a_data_type == b_data_type
1222                        && a_compmask == b_compmask
1223                );
1224
1225                // data might not be the same length, as it gets padded out.
1226                for i in 0..b_data.len() {
1227                    if i < a_data.len() {
1228                        assert_eq!(a_data[i], b_data[i]);
1229                    } else {
1230                        assert_eq!(0, b_data[i]);
1231                    }
1232                }
1233            }
1234            (
1235                IgvmDirectiveHeader::ParameterArea {
1236                    number_of_bytes: a_number_of_bytes,
1237                    parameter_area_index: a_parameter_area_index,
1238                    initial_data: a_initial_data,
1239                },
1240                IgvmDirectiveHeader::ParameterArea {
1241                    number_of_bytes: b_number_of_bytes,
1242                    parameter_area_index: b_parameter_area_index,
1243                    initial_data: b_initial_data,
1244                },
1245            ) => {
1246                assert!(
1247                    a_number_of_bytes == b_number_of_bytes
1248                        && a_parameter_area_index == b_parameter_area_index
1249                );
1250
1251                // initial_data might be padded out just like page data.
1252                for i in 0..b_initial_data.len() {
1253                    if i < a_initial_data.len() {
1254                        assert_eq!(a_initial_data[i], b_initial_data[i]);
1255                    } else {
1256                        assert_eq!(0, b_initial_data[i]);
1257                    }
1258                }
1259            }
1260            _ => assert_eq!(a, b),
1261        }
1262    }
1263}
1264
1265/// A trait to specialize behavior of the file builder based on different
1266/// register types for different architectures. Different methods may need to be
1267/// called depending on the register type that represents the given architecture.
1268trait IgvmfilegenRegister: IgvmLoaderRegister + 'static {
1269    fn build_snp_linux_direct(
1270        params: snp_linux_direct::BuildParams<'_>,
1271    ) -> anyhow::Result<file_loader::IgvmOutput>;
1272
1273    fn load_uefi(
1274        importer: &mut dyn ImageLoad<Self>,
1275        image: &[u8],
1276        config: loader::uefi::ConfigType,
1277    ) -> Result<loader::uefi::LoadInfo, loader::uefi::Error>;
1278
1279    fn load_linux_kernel_and_initrd<F>(
1280        importer: &mut impl ImageLoad<Self>,
1281        kernel_image: &mut F,
1282        kernel_minimum_start_address: u64,
1283        initrd: Option<InitrdConfig<'_>>,
1284        device_tree_blob: Option<&[u8]>,
1285    ) -> Result<loader::linux::LoadInfo, loader::linux::Error>
1286    where
1287        F: std::io::Read + Seek,
1288        Self: GuestArch;
1289
1290    fn load_openhcl<F>(
1291        importer: &mut dyn ImageLoad<Self>,
1292        kernel_image: &mut F,
1293        shim: &mut F,
1294        sidecar: Option<&mut F>,
1295        command_line: CommandLineType<'_>,
1296        initrd: Option<(&mut dyn loader::common::ReadSeek, u64)>,
1297        memory_page_base: Option<u64>,
1298        memory_page_count: u64,
1299        vtl0_config: Vtl0Config<'_>,
1300        product_policy: Option<&ProductPolicy>,
1301    ) -> Result<(), loader::paravisor::Error>
1302    where
1303        F: std::io::Read + Seek;
1304}
1305
1306impl IgvmfilegenRegister for X86Register {
1307    fn build_snp_linux_direct(
1308        params: snp_linux_direct::BuildParams<'_>,
1309    ) -> anyhow::Result<file_loader::IgvmOutput> {
1310        snp_linux_direct::build(params)
1311    }
1312
1313    fn load_uefi(
1314        importer: &mut dyn ImageLoad<Self>,
1315        image: &[u8],
1316        config: loader::uefi::ConfigType,
1317    ) -> Result<loader::uefi::LoadInfo, loader::uefi::Error> {
1318        loader::uefi::x86_64::load(importer, image, config, true)
1319    }
1320
1321    fn load_linux_kernel_and_initrd<F>(
1322        importer: &mut impl ImageLoad<Self>,
1323        kernel_image: &mut F,
1324        kernel_minimum_start_address: u64,
1325        initrd: Option<InitrdConfig<'_>>,
1326        _device_tree_blob: Option<&[u8]>,
1327    ) -> Result<loader::linux::LoadInfo, loader::linux::Error>
1328    where
1329        F: std::io::Read + Seek,
1330    {
1331        loader::linux::load_kernel_and_initrd_x64(
1332            importer,
1333            kernel_image,
1334            kernel_minimum_start_address,
1335            initrd,
1336        )
1337    }
1338
1339    fn load_openhcl<F>(
1340        importer: &mut dyn ImageLoad<Self>,
1341        kernel_image: &mut F,
1342        shim: &mut F,
1343        sidecar: Option<&mut F>,
1344        command_line: CommandLineType<'_>,
1345        initrd: Option<(&mut dyn loader::common::ReadSeek, u64)>,
1346        memory_page_base: Option<u64>,
1347        memory_page_count: u64,
1348        vtl0_config: Vtl0Config<'_>,
1349        product_policy: Option<&ProductPolicy>,
1350    ) -> Result<(), loader::paravisor::Error>
1351    where
1352        F: std::io::Read + Seek,
1353    {
1354        loader::paravisor::load_openhcl_x64(
1355            importer,
1356            kernel_image,
1357            shim,
1358            sidecar,
1359            command_line,
1360            initrd,
1361            memory_page_base,
1362            memory_page_count,
1363            vtl0_config,
1364            product_policy,
1365        )
1366    }
1367}
1368
1369impl IgvmfilegenRegister for Aarch64Register {
1370    fn build_snp_linux_direct(
1371        _params: snp_linux_direct::BuildParams<'_>,
1372    ) -> anyhow::Result<file_loader::IgvmOutput> {
1373        bail!("snp_linux_direct is only supported for x64")
1374    }
1375
1376    fn load_uefi(
1377        importer: &mut dyn ImageLoad<Self>,
1378        image: &[u8],
1379        config: loader::uefi::ConfigType,
1380    ) -> Result<loader::uefi::LoadInfo, loader::uefi::Error> {
1381        loader::uefi::aarch64::load(importer, image, config, true)
1382    }
1383
1384    fn load_linux_kernel_and_initrd<F>(
1385        importer: &mut impl ImageLoad<Self>,
1386        kernel_image: &mut F,
1387        kernel_minimum_start_address: u64,
1388        initrd: Option<InitrdConfig<'_>>,
1389        device_tree_blob: Option<&[u8]>,
1390    ) -> Result<loader::linux::LoadInfo, loader::linux::Error>
1391    where
1392        F: std::io::Read + Seek,
1393    {
1394        loader::linux::load_kernel_and_initrd_arm64(
1395            importer,
1396            kernel_image,
1397            kernel_minimum_start_address,
1398            initrd,
1399            device_tree_blob,
1400        )
1401    }
1402
1403    fn load_openhcl<F>(
1404        importer: &mut dyn ImageLoad<Self>,
1405        kernel_image: &mut F,
1406        shim: &mut F,
1407        _sidecar: Option<&mut F>,
1408        command_line: CommandLineType<'_>,
1409        initrd: Option<(&mut dyn loader::common::ReadSeek, u64)>,
1410        memory_page_base: Option<u64>,
1411        memory_page_count: u64,
1412        vtl0_config: Vtl0Config<'_>,
1413        product_policy: Option<&ProductPolicy>,
1414    ) -> Result<(), loader::paravisor::Error>
1415    where
1416        F: std::io::Read + Seek,
1417    {
1418        loader::paravisor::load_openhcl_arm64(
1419            importer,
1420            kernel_image,
1421            shim,
1422            command_line,
1423            initrd,
1424            memory_page_base,
1425            memory_page_count,
1426            vtl0_config,
1427            product_policy,
1428        )
1429    }
1430}
1431
1432/// Load an image.
1433fn load_image<'a, R: IgvmfilegenRegister + GuestArch + 'static>(
1434    loader: &mut IgvmVtlLoader<'_, R>,
1435    config: &'a Image,
1436    resources: &'a Resources,
1437) -> anyhow::Result<()> {
1438    tracing::debug!(?config, "loading into VTL0");
1439
1440    match *config {
1441        Image::None => {
1442            // Nothing is loaded.
1443        }
1444        Image::Uefi { config_type } => {
1445            load_uefi(loader, resources, config_type)?;
1446        }
1447        Image::Linux(ref linux) => {
1448            load_linux(loader, linux, resources)?;
1449        }
1450        Image::SnpLinuxDirect { .. } => {
1451            unreachable!("snp_linux_direct is built by its dedicated strategy")
1452        }
1453        Image::Openhcl {
1454            ref command_line,
1455            static_command_line,
1456            memory_page_base,
1457            memory_page_count,
1458            uefi,
1459            ref linux,
1460            ref product_policy,
1461        } => {
1462            if uefi && linux.is_some() {
1463                anyhow::bail!("cannot include both UEFI and Linux images in OpenHCL image");
1464            }
1465
1466            let kernel_path = resources
1467                .get(ResourceType::UnderhillKernel)
1468                .expect("validated present");
1469            let mut kernel = fs_err::File::open(kernel_path).context(format!(
1470                "reading underhill kernel image at {}",
1471                kernel_path.display()
1472            ))?;
1473
1474            let mut initrd = {
1475                let initrd_path = resources
1476                    .get(ResourceType::UnderhillInitrd)
1477                    .expect("validated present");
1478                Some(fs_err::File::open(initrd_path).context(format!(
1479                    "reading underhill initrd at {}",
1480                    initrd_path.display()
1481                ))?)
1482            };
1483
1484            let shim_path = resources
1485                .get(ResourceType::OpenhclBoot)
1486                .expect("validated present");
1487            let mut shim = fs_err::File::open(shim_path)
1488                .context(format!("reading underhill shim at {}", shim_path.display()))?;
1489
1490            let mut sidecar =
1491                if let Some(sidecar_path) = resources.get(ResourceType::UnderhillSidecar) {
1492                    Some(fs_err::File::open(sidecar_path).context("reading AP kernel")?)
1493                } else {
1494                    None
1495                };
1496
1497            let initrd_info = if let Some(ref mut f) = initrd {
1498                let size = f.seek(std::io::SeekFrom::End(0))?;
1499                f.rewind()?;
1500                Some((f as &mut dyn loader::common::ReadSeek, size))
1501            } else {
1502                None
1503            };
1504
1505            // TODO: While the paravisor supports multiple things that can be
1506            // loaded in VTL0, we don't yet have updated file builder config for
1507            // that.
1508            //
1509            // Since the host performs PCAT loading, each image that supports
1510            // UEFI also supports PCAT boot. A future file builder config change
1511            // will make this more explicit.
1512            let vtl0_load_config = if uefi {
1513                let mut inner_loader = loader.nested_loader();
1514                let load_info = load_uefi(&mut inner_loader, resources, UefiConfigType::None)?;
1515                let vp_context = inner_loader.take_vp_context();
1516                Vtl0Config {
1517                    supports_pcat: loader.loader().arch() == GuestArchKind::X86_64,
1518                    supports_uefi: Some((load_info, vp_context)),
1519                    supports_linux: None,
1520                }
1521            } else if let Some(linux) = linux {
1522                let load_info = load_linux(&mut loader.nested_loader(), linux, resources)?;
1523                Vtl0Config {
1524                    supports_pcat: false,
1525                    supports_uefi: None,
1526                    supports_linux: Some(Vtl0Linux {
1527                        command_line: &linux.command_line,
1528                        load_info,
1529                    }),
1530                }
1531            } else {
1532                Vtl0Config {
1533                    supports_pcat: false,
1534                    supports_uefi: None,
1535                    supports_linux: None,
1536                }
1537            };
1538
1539            let command_line = if static_command_line {
1540                CommandLineType::Static(command_line)
1541            } else {
1542                CommandLineType::HostAppendable(command_line)
1543            };
1544
1545            R::load_openhcl(
1546                loader,
1547                &mut kernel,
1548                &mut shim,
1549                sidecar.as_mut(),
1550                command_line,
1551                initrd_info,
1552                memory_page_base,
1553                memory_page_count,
1554                vtl0_load_config,
1555                product_policy.as_ref(),
1556            )
1557            .context("underhill kernel loader")?;
1558        }
1559    };
1560
1561    Ok(())
1562}
1563
1564fn load_uefi<R: IgvmfilegenRegister + GuestArch + 'static>(
1565    loader: &mut IgvmVtlLoader<'_, R>,
1566    resources: &Resources,
1567    config_type: UefiConfigType,
1568) -> Result<loader::uefi::LoadInfo, anyhow::Error> {
1569    let image_path = resources
1570        .get(ResourceType::Uefi)
1571        .expect("validated present");
1572    let image = fs_err::read(image_path)
1573        .context(format!("reading uefi image at {}", image_path.display()))?;
1574    let config = match config_type {
1575        UefiConfigType::None => loader::uefi::ConfigType::None,
1576        UefiConfigType::Igvm => loader::uefi::ConfigType::Igvm,
1577    };
1578    let load_info = R::load_uefi(loader, &image, config).context("uefi loader")?;
1579    Ok(load_info)
1580}
1581
1582fn load_linux<R: IgvmfilegenRegister + GuestArch + 'static>(
1583    loader: &mut IgvmVtlLoader<'_, R>,
1584    config: &LinuxImage,
1585    resources: &Resources,
1586) -> Result<loader::linux::LoadInfo, anyhow::Error> {
1587    let LinuxImage {
1588        use_initrd,
1589        command_line: _,
1590    } = *config;
1591    let kernel_path = resources
1592        .get(ResourceType::LinuxKernel)
1593        .expect("validated present");
1594    let mut kernel = fs_err::File::open(kernel_path).context(format!(
1595        "reading vtl0 kernel image at {}",
1596        kernel_path.display()
1597    ))?;
1598    let mut initrd_file = if use_initrd {
1599        let initrd_path = resources
1600            .get(ResourceType::LinuxInitrd)
1601            .expect("validated present");
1602        Some(
1603            fs_err::File::open(initrd_path)
1604                .context(format!("reading vtl0 initrd at {}", initrd_path.display()))?,
1605        )
1606    } else {
1607        None
1608    };
1609    let initrd = if let Some(ref mut f) = initrd_file {
1610        let size = f.seek(std::io::SeekFrom::End(0))?;
1611        f.rewind()?;
1612        Some(InitrdConfig {
1613            initrd_address: loader::linux::InitrdAddressType::AfterKernel,
1614            initrd: f,
1615            size,
1616        })
1617    } else {
1618        None
1619    };
1620    let load_info = R::load_linux_kernel_and_initrd(loader, &mut kernel, 0, initrd, None)
1621        .context("loading linux kernel and initrd")?;
1622    Ok(load_info)
1623}
1624
1625#[cfg(test)]
1626mod temporary_signing_tests {
1627    use super::*;
1628
1629    #[test]
1630    fn linux_direct_manifest_selects_linux_direct_identity() {
1631        let config: Config =
1632            serde_json::from_str(include_str!("../../manifests/snp-linux-direct.json")).unwrap();
1633
1634        let (svn, identity) = snp_temporary_signing_metadata_from_config(&config).unwrap();
1635        assert_eq!(svn, 1);
1636        assert_eq!(identity, snp_id_block::SnpImageIdentity::LINUX_DIRECT);
1637    }
1638
1639    #[test]
1640    fn openhcl_manifest_selects_openhcl_identity() {
1641        let config: Config =
1642            serde_json::from_str(include_str!("../../manifests/openhcl-x64-cvm-dev.json")).unwrap();
1643
1644        let (_, identity) = snp_temporary_signing_metadata_from_config(&config).unwrap();
1645        assert_eq!(identity, snp_id_block::SnpImageIdentity::OPENHCL);
1646    }
1647}