1#![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 Dump {
83 #[clap(short, long = "filepath")]
85 file_path: PathBuf,
86 },
87 DumpCorim {
101 #[clap(short, long = "filepath")]
103 file_path: PathBuf,
104 #[clap(long, value_enum)]
107 header_type: Option<CorimHeaderType>,
108 #[clap(long, value_enum)]
112 platform: Option<Platform>,
113 #[clap(short, long)]
119 output: Option<PathBuf>,
120 },
121 Manifest {
129 #[clap(short, long = "manifest")]
131 manifest: PathBuf,
132 #[clap(short, long = "resources")]
135 resources: PathBuf,
136 #[clap(short = 'o', long)]
138 output: PathBuf,
139 #[clap(long)]
141 debug_validation: bool,
142 #[clap(long)]
144 disable_secure_avic: bool,
145 #[clap(long)]
153 confidential_debug: bool,
154 },
155 AddSnpIdBlock {
179 #[clap(short, long)]
181 input: PathBuf,
182 #[clap(short, long)]
184 output: PathBuf,
185 #[clap(long, conflicts_with_all = ["manifest", "id_block", "id_signature", "id_public_key"])]
189 guest_svn: Option<u32>,
190 #[clap(long, conflicts_with_all = ["guest_svn", "id_block", "id_signature", "id_public_key"])]
193 manifest: Option<PathBuf>,
194 #[clap(long, requires_all = ["id_signature", "id_public_key"], conflicts_with_all = ["guest_svn", "manifest"])]
198 id_block: Option<PathBuf>,
199 #[clap(long, requires = "id_block")]
202 id_signature: Option<PathBuf>,
203 #[clap(long, requires = "id_block")]
206 id_public_key: Option<PathBuf>,
207 },
208 PatchCorimSignature {
233 #[clap(short, long)]
235 input: PathBuf,
236 #[clap(short, long)]
238 output: PathBuf,
239 #[clap(
247 long,
248 conflicts_with = "corim_signature",
249 required_unless_present = "corim_signature"
250 )]
251 corim_bundle: Option<PathBuf>,
252 #[clap(long)]
259 corim_signature: Option<PathBuf>,
260 #[clap(long, value_enum)]
262 platform: Platform,
263 },
264}
265
266#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
271enum Platform {
272 Snp,
274 Tdx,
276 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#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
292enum CorimHeaderType {
293 Document,
295 Signature,
297}
298
299fn 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; 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 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 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 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
431struct PlatformMeta {
435 platform: IgvmPlatformType,
436 svn: u32,
437 debug_enabled: bool,
438 snp_identity: Option<snp_id_block::SnpImageIdentity>,
439}
440
441fn 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
459fn 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
474fn 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
507fn 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
527fn 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 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 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 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 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 let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?;
718
719 for meta in &platform_metas {
725 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 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_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 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 debug_validation {
791 debug_validate_igvm_file(&igvm_binary);
792 }
793
794 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 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
824fn write_igvm_file_atomic(
831 output: &std::path::Path,
832 data: &[u8],
833 log_message: &str,
834) -> anyhow::Result<()> {
835 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
861fn 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 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 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
940fn 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
964fn 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 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; println!("IGVM File: {}", file_path.display());
981 println!("Total file size: {} bytes", fixed_header.total_file_size);
982 println!();
983
984 let mut output_dir_created = false;
988
989 let platforms = igvm_file.platforms();
990
991 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 let platform_mask_filter = platform_filter
1009 .map(|p| platform_mask::lookup_compatibility_mask(platforms, IgvmPlatformType::from(p)))
1010 .transpose()?;
1011
1012 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
1088fn 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 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
1154fn 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 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 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
1257trait 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
1424fn 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 }
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 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}