Skip to main content

igvmfilegen/
main.rs

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