1#![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 Dump {
91 #[clap(short, long = "filepath")]
93 file_path: PathBuf,
94 },
95 DumpCorim {
109 #[clap(short, long = "filepath")]
111 file_path: PathBuf,
112 #[clap(long, value_enum)]
115 header_type: Option<CorimHeaderType>,
116 #[clap(long, value_enum)]
120 platform: Option<Platform>,
121 #[clap(short, long)]
127 output: Option<PathBuf>,
128 },
129 Manifest {
137 #[clap(short, long = "manifest")]
139 manifest: PathBuf,
140 #[clap(short, long = "resources")]
143 resources: PathBuf,
144 #[clap(short = 'o', long)]
146 output: PathBuf,
147 #[clap(long)]
149 debug_validation: bool,
150 #[clap(long)]
152 disable_secure_avic: bool,
153 #[clap(long)]
161 confidential_debug: bool,
162 },
163 AddSnpIdBlock {
187 #[clap(short, long)]
189 input: PathBuf,
190 #[clap(short, long)]
192 output: PathBuf,
193 #[clap(long, conflicts_with_all = ["manifest", "id_block", "id_signature", "id_public_key"])]
197 guest_svn: Option<u32>,
198 #[clap(long, conflicts_with_all = ["guest_svn", "id_block", "id_signature", "id_public_key"])]
201 manifest: Option<PathBuf>,
202 #[clap(long, requires_all = ["id_signature", "id_public_key"], conflicts_with_all = ["guest_svn", "manifest"])]
206 id_block: Option<PathBuf>,
207 #[clap(long, requires = "id_block")]
210 id_signature: Option<PathBuf>,
211 #[clap(long, requires = "id_block")]
214 id_public_key: Option<PathBuf>,
215 },
216 PatchCorimSignature {
241 #[clap(short, long)]
243 input: PathBuf,
244 #[clap(short, long)]
246 output: PathBuf,
247 #[clap(
255 long,
256 conflicts_with = "corim_signature",
257 required_unless_present = "corim_signature"
258 )]
259 corim_bundle: Option<PathBuf>,
260 #[clap(long)]
267 corim_signature: Option<PathBuf>,
268 #[clap(long, value_enum)]
270 platform: Platform,
271 },
272}
273
274#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
279enum Platform {
280 Snp,
282 Tdx,
284 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#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
300enum CorimHeaderType {
301 Document,
303 Signature,
305}
306
307fn 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; 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 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 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 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
439struct PlatformMeta {
443 platform: IgvmPlatformType,
444 svn: u32,
445 debug_enabled: bool,
446 snp_identity: Option<snp_id_block::SnpImageIdentity>,
447}
448
449fn 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
467fn 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
482fn 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
515fn 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
535fn 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 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 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 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 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 let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?;
726
727 for meta in &platform_metas {
733 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 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_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 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 debug_validation {
799 debug_validate_igvm_file(&igvm_binary);
800 }
801
802 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 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
832fn write_igvm_file_atomic(
839 output: &std::path::Path,
840 data: &[u8],
841 log_message: &str,
842) -> anyhow::Result<()> {
843 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
869fn 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 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 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
948fn 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
972fn 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 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; println!("IGVM File: {}", file_path.display());
989 println!("Total file size: {} bytes", fixed_header.total_file_size);
990 println!();
991
992 let mut output_dir_created = false;
996
997 let platforms = igvm_file.platforms();
998
999 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 let platform_mask_filter = platform_filter
1017 .map(|p| platform_mask::lookup_compatibility_mask(platforms, IgvmPlatformType::from(p)))
1018 .transpose()?;
1019
1020 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
1096fn 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 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
1162fn 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 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 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
1265trait 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
1432fn 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 }
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 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}