diff --git a/Cargo.lock b/Cargo.lock index 2cfa36f28f..b56286cf7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,9 +326,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfield-struct" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be5a46ba01b60005ae2c51a36a29cfe134bcacae2dd5cedcd4615fbaad1494b" +checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" dependencies = [ "proc-macro2", "quote", @@ -337,9 +337,9 @@ dependencies = [ [[package]] name = "bitfield-struct" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ca019570363e800b05ad4fd890734f28ac7b72f563ad8a35079efb793616f8" +checksum = "8769c4854c5ada2852ddf6fd09d15cf43d4c2aaeccb4de6432f5402f08a6003b" dependencies = [ "proc-macro2", "quote", @@ -369,6 +369,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.0" @@ -943,12 +952,43 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "corim" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18ae0dacf8af1ae85596c375df3bf848ff57712a8b07d5a9ba36863d901f78d7" +dependencies = [ + "corim-macros", + "serde", + "thiserror 2.0.16", +] + +[[package]] +name = "corim-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c16ee2c7db3dde66570487ea3a6329bd07ca138652b57a3a442c200c75df495" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "cpubits" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -1087,7 +1127,7 @@ dependencies = [ "pkcs8", "rsa", "sha1", - "sha2", + "sha2 0.11.0", "signature", "symcrypt", "thiserror 2.0.16", @@ -1112,6 +1152,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.1" @@ -1397,15 +1447,25 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + [[package]] name = "digest" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ - "block-buffer", + "block-buffer 0.12.0", "const-oid", - "crypto-common", + "crypto-common 0.2.1", ] [[package]] @@ -2804,6 +2864,16 @@ dependencies = [ "windows 0.58.0", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "get_helpers" version = "0.0.0" @@ -3705,28 +3775,29 @@ dependencies = [ [[package]] name = "igvm" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67578b05ebcdfa1aa0fe13f77a13bdd7d87036128898a327f1bf8e7356cf09cd" +source = "git+https://github.com/microsoft/igvm?rev=e6c3ff1#e6c3ff1c34c3570e91e6c29bf3375313f5fafc39" dependencies = [ - "bitfield-struct 0.10.1", + "bitfield-struct 0.12.1", + "corim", "crc32fast", "hex", "igvm_defs", "open-enum", "range_map_vec", + "sha2 0.10.9", "static_assertions", "thiserror 2.0.16", "tracing", + "uuid", "zerocopy", ] [[package]] name = "igvm_defs" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eedd8c64460676101062f9f2ecdeb52d8f43e622da6a6c5bf5158f4ef08b0906" +source = "git+https://github.com/microsoft/igvm?rev=e6c3ff1#e6c3ff1c34c3570e91e6c29bf3375313f5fafc39" dependencies = [ - "bitfield-struct 0.10.1", + "bitfield-struct 0.12.1", "open-enum", "static_assertions", "zerocopy", @@ -3737,7 +3808,10 @@ name = "igvmfilegen" version = "0.0.0" dependencies = [ "anyhow", + "bitfield-struct 0.11.0", "clap", + "corim", + "crypto", "fs-err", "hex", "hvdef", @@ -3750,11 +3824,10 @@ dependencies = [ "range_map_vec", "serde", "serde_json", - "sha2", - "thiserror 2.0.16", + "sha2 0.11.0", + "test_with_tracing", "tracing", "tracing-subscriber", - "vbs_defs", "x86defs", "zerocopy", ] @@ -5367,7 +5440,7 @@ dependencies = [ "minimal_rt_build", "page_table", "safe_intrinsics", - "sha2", + "sha2 0.11.0", "sidecar_defs", "static_assertions", "string_page_buf", @@ -6890,11 +6963,11 @@ dependencies = [ "const-oid", "crypto-bigint", "crypto-primes", - "digest", + "digest 0.11.2", "pkcs1", "pkcs8", "rand_core", - "sha2", + "sha2 0.11.0", "signature", "spki", "zeroize", @@ -7416,8 +7489,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -7427,8 +7517,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -7528,7 +7618,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest", + "digest 0.11.2", "rand_core", ] @@ -9073,29 +9163,33 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "sha1_smol", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vbs_defs" -version = "0.0.0" -dependencies = [ - "bitfield-struct 0.11.0", - "igvm_defs", - "open_enum", - "static_assertions", - "zerocopy", -] - [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "vfio-bindings" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 7648b2cb6b..61af082e7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -400,7 +400,6 @@ igvmfilegen_config = { path = "vm/loader/igvmfilegen_config" } loader_defs = { path = "vm/loader/loader_defs" } page_table = { path = "vm/loader/page_table" } page_pool_alloc = { path = "vm/page_pool_alloc" } -vbs_defs = { path = "vm/vbs_defs" } vmgs = { path = "vm/vmgs/vmgs" } vmgs_broker = { path = "vm/vmgs/vmgs_broker" } vmgs_format = { path = "vm/vmgs/vmgs_format" } @@ -499,6 +498,7 @@ target-lexicon = "0.13.2" base64 = "0.22.1" base64-serde = "0.8" cargo_toml = "0.22" +corim = "0.1.3" heck = "0.5" hex = "0.4" pbjson = "0.5" @@ -612,8 +612,8 @@ iced-x86 = { version = "1.17", default-features = false, features = [ "no_xop", "no_d3now", ] } -igvm = "0.4.0" -igvm_defs = { version = "0.4.0", default-features = false } +igvm = { git = "https://github.com/microsoft/igvm", rev = "e6c3ff1" } +igvm_defs = { git = "https://github.com/microsoft/igvm", rev = "e6c3ff1", default-features = false } kvm-bindings = "0.14.0" mshv-bindings = "0.6.8" mshv-ioctls = "0.6.8" diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_build_igvm.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_build_igvm.rs index f7eec2117e..3ba094cb4c 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/local_build_igvm.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_build_igvm.rs @@ -301,11 +301,23 @@ impl SimpleFlowNode for Node { igvm_tdx_json, igvm_snp_json, igvm_vbs_json, + igvm_tdx_corim, + igvm_snp_corim, + igvm_vbs_corim, }) = openhcl_igvm.endorsements() { fs_err::copy(igvm_tdx_json, output_dir.join("openhcl-tdx.json"))?; fs_err::copy(igvm_snp_json, output_dir.join("openhcl-snp.json"))?; fs_err::copy(igvm_vbs_json, output_dir.join("openhcl-vbs.json"))?; + if let Some(igvm_tdx_corim) = igvm_tdx_corim { + fs_err::copy(igvm_tdx_corim, output_dir.join("openhcl-tdx.cbor"))?; + } + if let Some(igvm_snp_corim) = igvm_snp_corim { + fs_err::copy(igvm_snp_corim, output_dir.join("openhcl-snp.cbor"))?; + } + if let Some(igvm_vbs_corim) = igvm_vbs_corim { + fs_err::copy(igvm_vbs_corim, output_dir.join("openhcl-vbs.cbor"))?; + } } for e in fs_err::read_dir(output_dir)? { let e = e?; diff --git a/flowey/flowey_lib_hvlite/src/build_igvmfilegen.rs b/flowey/flowey_lib_hvlite/src/build_igvmfilegen.rs index 3fd14e226b..61aa06bb19 100644 --- a/flowey/flowey_lib_hvlite/src/build_igvmfilegen.rs +++ b/flowey/flowey_lib_hvlite/src/build_igvmfilegen.rs @@ -47,6 +47,7 @@ impl FlowNode for Node { fn imports(ctx: &mut ImportCtx<'_>) { ctx.import::(); + ctx.import::(); } fn emit(requests: Vec, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> { @@ -62,6 +63,22 @@ impl FlowNode for Node { m }); + // igvmfilegen depends on the workspace `crypto` crate, which on + // `linux-gnu` targets pulls in openssl-sys and requires the OpenSSL + // development headers + pkg-config to be present on the build host. + let mut pre_build_deps = Vec::new(); + if matches!( + ctx.platform(), + FlowPlatform::Linux(FlowPlatformLinuxDistro::Ubuntu) + ) { + pre_build_deps.push(ctx.reqv(|v| { + flowey_lib_common::install_dist_pkg::Request::Install { + package_names: vec!["libssl-dev".into(), "pkg-config".into()], + done: v, + } + })); + } + for (IgvmfilegenBuildParams { target, profile }, outvars) in requests { let output = ctx.reqv(|v| crate::run_cargo_build::Request { crate_name: "igvmfilegen".into(), @@ -72,7 +89,7 @@ impl FlowNode for Node { target: target.as_triple(), no_split_dbg_info: false, extra_env: None, - pre_build_deps: Vec::new(), + pre_build_deps: pre_build_deps.clone(), output: v, }); diff --git a/flowey/flowey_lib_hvlite/src/build_openhcl_igvm_from_recipe.rs b/flowey/flowey_lib_hvlite/src/build_openhcl_igvm_from_recipe.rs index 04b58a92d0..88e9580bb9 100644 --- a/flowey/flowey_lib_hvlite/src/build_openhcl_igvm_from_recipe.rs +++ b/flowey/flowey_lib_hvlite/src/build_openhcl_igvm_from_recipe.rs @@ -85,6 +85,12 @@ pub enum OpenhclIgvmEndorsements { igvm_snp_json: PathBuf, #[serde(rename = "openhcl-vbs.json")] igvm_vbs_json: PathBuf, + #[serde(rename = "openhcl-tdx.cbor")] + igvm_tdx_corim: Option, + #[serde(rename = "openhcl-snp.cbor")] + igvm_snp_corim: Option, + #[serde(rename = "openhcl-vbs.cbor")] + igvm_vbs_corim: Option, }, } @@ -139,6 +145,9 @@ impl OpenhclIgvmOutput { igvm_tdx_json, igvm_snp_json, igvm_vbs_json, + igvm_tdx_corim, + igvm_snp_corim, + igvm_vbs_corim, } = igvm; let mut endorsements = match (igvm_tdx_json, igvm_snp_json, igvm_vbs_json) { (Some(igvm_tdx_json), Some(igvm_snp_json), Some(igvm_vbs_json)) => { @@ -146,6 +155,9 @@ impl OpenhclIgvmOutput { igvm_tdx_json, igvm_snp_json, igvm_vbs_json, + igvm_tdx_corim, + igvm_snp_corim, + igvm_vbs_corim, }) } (None, None, None) => None, diff --git a/flowey/flowey_lib_hvlite/src/run_igvmfilegen.rs b/flowey/flowey_lib_hvlite/src/run_igvmfilegen.rs index 65ad0185d6..f74bd7d00b 100644 --- a/flowey/flowey_lib_hvlite/src/run_igvmfilegen.rs +++ b/flowey/flowey_lib_hvlite/src/run_igvmfilegen.rs @@ -15,6 +15,9 @@ pub struct IgvmOutput { pub igvm_tdx_json: Option, pub igvm_snp_json: Option, pub igvm_vbs_json: Option, + pub igvm_tdx_corim: Option, + pub igvm_snp_corim: Option, + pub igvm_vbs_corim: Option, } flowey_request! { @@ -97,6 +100,18 @@ impl SimpleFlowNode for Node { let path = igvm_path.with_file_name(format!("{igvm_file_stem}-vbs.json")); path.exists().then_some(path) }; + let igvm_tdx_corim = { + let path = igvm_path.with_file_name(format!("{igvm_file_stem}-tdx.cbor")); + path.exists().then_some(path) + }; + let igvm_snp_corim = { + let path = igvm_path.with_file_name(format!("{igvm_file_stem}-snp.cbor")); + path.exists().then_some(path) + }; + let igvm_vbs_corim = { + let path = igvm_path.with_file_name(format!("{igvm_file_stem}-vbs.cbor")); + path.exists().then_some(path) + }; rt.write( igvm, @@ -106,6 +121,9 @@ impl SimpleFlowNode for Node { igvm_tdx_json, igvm_snp_json, igvm_vbs_json, + igvm_tdx_corim, + igvm_snp_corim, + igvm_vbs_corim, }, ); diff --git a/openvmm/openvmm_core/Cargo.toml b/openvmm/openvmm_core/Cargo.toml index 2ff5d0d7ca..6b738d5a0c 100644 --- a/openvmm/openvmm_core/Cargo.toml +++ b/openvmm/openvmm_core/Cargo.toml @@ -34,7 +34,7 @@ floppy_resources.workspace = true hvdef.workspace = true hyperv_dump.workspace = true ide_resources.workspace = true -igvm.workspace = true +igvm = { workspace = true, features = ["corim"] } igvm_defs.workspace = true loader.workspace = true virt.workspace = true diff --git a/openvmm/openvmm_core/src/worker/vm_loaders/igvm.rs b/openvmm/openvmm_core/src/worker/vm_loaders/igvm.rs index 28b9e9be53..8796e75159 100644 --- a/openvmm/openvmm_core/src/worker/vm_loaders/igvm.rs +++ b/openvmm/openvmm_core/src/worker/vm_loaders/igvm.rs @@ -855,6 +855,9 @@ fn load_igvm_x86( IgvmDirectiveHeader::X64NativeVpContext { .. } => { todo!("native igvm type not supported yet") } + IgvmDirectiveHeader::AArch64CcaVpContext { .. } => { + todo!("AArch64 CCA VP context not supported yet") + } } } else { panic!("no relocation region, cannot filter to VTL2"); @@ -1192,6 +1195,9 @@ fn load_igvm_x86( IgvmDirectiveHeader::X64NativeVpContext { .. } => { todo!("native vp context not supported") } + IgvmDirectiveHeader::AArch64CcaVpContext { .. } => { + todo!("AArch64 CCA VP context not supported") + } } } diff --git a/vm/loader/igvmfilegen/Cargo.toml b/vm/loader/igvmfilegen/Cargo.toml index bb0ae67943..4f81e5a36a 100644 --- a/vm/loader/igvmfilegen/Cargo.toml +++ b/vm/loader/igvmfilegen/Cargo.toml @@ -14,23 +14,28 @@ loader_defs.workspace = true hvdef.workspace = true memory_range.workspace = true -vbs_defs.workspace = true x86defs.workspace = true anyhow.workspace = true +bitfield-struct.workspace = true clap = { workspace = true, features = ["derive"] } +corim.workspace = true +crypto = { workspace = true, features = ["native", "vendored"] } fs-err.workspace = true hex = { workspace = true, features = ["serde"] } -igvm.workspace = true +igvm = { workspace = true, features = ["corim"] } igvm_defs.workspace = true range_map_vec.workspace = true serde = { workspace = true, features = ["std"] } serde_json = { workspace = true, features = ["std"] } sha2.workspace = true -thiserror.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } tracing.workspace = true zerocopy.workspace = true +[dev-dependencies] +crypto = { workspace = true, features = ["test_helpers"] } +test_with_tracing.workspace = true + [lints] workspace = true diff --git a/vm/loader/igvmfilegen/src/corim_signature/envelope.rs b/vm/loader/igvmfilegen/src/corim_signature/envelope.rs new file mode 100644 index 0000000000..69fac4c38a --- /dev/null +++ b/vm/loader/igvmfilegen/src/corim_signature/envelope.rs @@ -0,0 +1,500 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Operations on signed CoRIM envelopes (`#6.18(COSE_Sign1)` carrying a +//! `tagged-unsigned-corim-map` payload). +//! +//! Two public entry points: +//! +//! - [`detach_payload`] splits a bundled signed CoRIM into its CoRIM +//! document and a detached COSE_Sign1 (nil-payload) signature. +//! - [`verify_corim_signature`] cryptographically verifies a detached +//! signature against a document, using the issuer X.509 certificate +//! carried in the envelope's `x5chain` / `x5bag` protected header +//! (RFC 9360). +//! +//! # Design rationale +//! +//! Parsing and encoding both delegate to the `corim` crate's +//! [`decode_signed_corim`] / [`encode_signed_corim`] entry points -- the +//! same code path that the upstream `igvm` crate uses for its CoRIM +//! support. This keeps a single source of truth for signed-CoRIM +//! envelope handling in the workspace and ensures that any envelope we +//! accept also satisfies draft-ietf-rats-corim section 4.2 (protected header +//! must include `corim-meta` or `cwt-claims`). +//! +//! Cryptographic verification is performed via the workspace `crypto` +//! crate's RSA-PSS primitives; only PS384 is currently supported +//! (see [`verify_corim_signature`] for details). +//! +//! [`decode_signed_corim`]: corim::types::signed::decode_signed_corim +//! [`encode_signed_corim`]: corim::types::signed::encode_signed_corim + +use anyhow::Context; +use corim::types::signed::CORIM_CONTENT_TYPE; +use corim::types::signed::CoseAlgorithm; +use corim::types::signed::decode_signed_corim; +use corim::types::signed::encode_signed_corim; +use crypto::HashAlgorithm; +use crypto::x509::X509Certificate; + +/// Output of [`detach_payload`]: the CoRIM document plus a detached +/// COSE_Sign1 envelope (`payload` field set to nil). +#[derive(Debug)] +pub struct DetachedCorim { + /// CBOR-encoded CoRIM document extracted from the input envelope. + pub document: Vec, + /// CoRIM-spec `#6.18(COSE_Sign1)` envelope with the payload slot + /// nil, suitable for [`verify_corim_signature`] against `document`. + pub signature: Vec, +} + +/// Split a bundled (payload-embedded) COSE_Sign1 into its CoRIM document +/// payload and a detached COSE_Sign1 signature. +/// +/// A signed CoRIM is `Tag(18) [ protected, unprotected, payload, signature ]` +/// where `payload` is a `bstr` containing the CBOR-encoded CoRIM document. +/// +/// This function: +/// 1. Decodes the COSE_Sign1 with `corim::types::signed::decode_signed_corim` +/// 2. Extracts the raw payload bytes -> returned as the document +/// 3. Re-emits the envelope with the payload field set to nil +/// -> returned as the detached signature +/// +/// The protected-header bytes and signature bytes are preserved verbatim +/// across the round-trip: `decode_signed_corim` retains the original +/// `protected_header_bytes` as-is, and `encode_signed_corim` emits them +/// unmodified. This is required because the COSE signature is computed +/// over the exact protected-header bytes. +/// +/// # Errors +/// Returns an error if: +/// - the input is not a valid CoRIM-spec-compliant `#6.18(COSE_Sign1)`, or +/// - the input has a nil payload (i.e., is already detached) -- in that +/// case pass the bytes straight to [`verify_corim_signature`] instead +/// of splitting them. +pub fn detach_payload(data: &[u8]) -> anyhow::Result { + let mut signed = decode_signed_corim(data).context("Signed CoRIM: decode failed")?; + + let document = signed.payload.take().ok_or_else(|| { + anyhow::anyhow!( + "Signed CoRIM: payload is nil (already detached); pass the detached \ + signature directly instead of splitting it" + ) + })?; + + // `payload` is now `None` -> encode produces a detached envelope. + let signature = encode_signed_corim(&signed) + .context("Signed CoRIM: failed to encode detached signature")?; + + tracing::debug!( + input_size = data.len(), + document_size = document.len(), + detached_signature_size = signature.len(), + "Split signed CoRIM into document payload and detached COSE_Sign1 signature" + ); + + Ok(DetachedCorim { + document, + signature, + }) +} + +/// Cryptographically verify a detached COSE_Sign1 CoRIM signature against +/// the document it endorses. +/// +/// The issuer X.509 certificate is taken from the envelope's protected +/// header per RFC 9360: `x5chain` (key 33) is preferred, with `x5bag` +/// (key 32) as a fallback. For a chain or bag, the end-entity (leaf) +/// certificate is used. +/// +/// Enforces: +/// +/// 1. The envelope decodes as a CoRIM-spec-compliant `#6.18(COSE_Sign1)` +/// via [`decode_signed_corim`]. +/// 2. The payload is nil (detached form). +/// 3. The COSE signature bytes are non-empty. +/// 4. If the protected header carries a `content-type` (key 3), it equals +/// `"application/rim+cbor"`. +/// 5. The protected header carries an `x5chain` or `x5bag` entry. +/// 6. The protected-header algorithm is supported (see below). +/// 7. The end-entity certificate parses as DER X.509 and exposes an RSA +/// public key. +/// 8. The signature math verifies via `pss_verify` over the COSE +/// `Sig_structure1` TBS bytes built from the envelope's protected +/// header, the supplied `document`, and empty external AAD. +/// +/// # Supported algorithms +/// +/// Only **PS384** is currently accepted: RSA-PSS with SHA-384, +/// MGF1-SHA-384, and a salt length equal to the hash output size +/// (48 bytes), per RFC 8230 section 2 (COSE alg ID `-38`). +/// +/// All other algorithms (RSA PKCS#1 v1.5, ECDSA, EdDSA, other PSS +/// variants) are rejected with a targeted error -- adding support +/// would require extending the `crypto` crate with the corresponding +/// primitives or COSE alg-ID mappings here. +/// +/// # Arguments +/// * `signature` - Detached COSE_Sign1 CoRIM envelope (nil payload). +/// * `document` - The CoRIM document the signature should endorse. +pub fn verify_corim_signature(signature: &[u8], document: &[u8]) -> anyhow::Result<()> { + let signed = decode_signed_corim(signature).context("CoRIM signature: decode failed")?; + + if !signed.is_detached() { + anyhow::bail!( + "CoRIM signature: payload must be nil for a detached signature; \ + embedded payloads must be split first" + ); + } + + if signed.signature.is_empty() { + anyhow::bail!("CoRIM signature: COSE signature bytes must be non-empty"); + } + + if let Some(ct) = &signed.protected.content_type + && ct != CORIM_CONTENT_TYPE + { + anyhow::bail!( + "CoRIM signature: protected content-type is {ct:?}, expected {CORIM_CONTENT_TYPE:?}" + ); + } + + // Extract the issuer cert from x5chain (key 33) -- falling back to + // x5bag (key 32). Per RFC 9360 the end-entity is the first cert + // (chain) or the only cert (single bstr); CoseX509::end_entity() + // hides that distinction. + let issuer_cert_der: &[u8] = signed + .protected + .x5chain + .as_ref() + .or(signed.protected.x5bag.as_ref()) + .map(|x| x.end_entity()) + .ok_or_else(|| { + anyhow::anyhow!( + "CoRIM signature: protected header carries neither x5chain (key 33) \ + nor x5bag (key 32); cannot identify the issuer certificate" + ) + })?; + + // Only PS384 is supported: RSA-PSS with SHA-384, MGF1-SHA-384, and + // a salt length equal to the hash output (48 bytes) per RFC 8230 + // section 2. + let hash = match signed.protected.alg { + CoseAlgorithm::Ps384 => HashAlgorithm::Sha384, + other => anyhow::bail!( + "CoRIM signature: unsupported COSE algorithm {other} ({}). \ + Only PS384 (-38) is supported.", + other.to_i64(), + ), + }; + + let cert = X509Certificate::from_der(issuer_cert_der) + .context("CoRIM signature: failed to parse issuer certificate (expected DER)")?; + + let pubkey = cert + .public_key() + .context("CoRIM signature: failed to extract public key from issuer certificate")?; + + let tbs = signed + .to_be_signed_detached(document, &[]) + .context("CoRIM signature: failed to construct Sig_structure1 TBS bytes")?; + + let valid = pubkey + .pss_verify(&tbs, &signed.signature, hash) + .context("CoRIM signature: RSA-PSS verification primitive returned an error")?; + + if !valid { + anyhow::bail!( + "CoRIM signature: cryptographic verification failed; signature \ + does not match the supplied document under the issuer's public key" + ); + } + + tracing::debug!( + signature_size = signature.len(), + document_size = document.len(), + tbs_size = tbs.len(), + issuer_cert_size = issuer_cert_der.len(), + alg = %signed.protected.alg, + "CoRIM signature cryptographically verified against issuer certificate from x5chain/x5bag" + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use corim::cbor::value::Value; + use corim::types::signed::CwtClaims; + use corim::types::signed::SignedCorimBuilder; + use test_with_tracing::test; + + const TEST_PAYLOAD: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD]; + + /// Build a CoRIM-spec-compliant bundled `#6.18(COSE_Sign1)` envelope + /// with the given inner payload and signature. + fn make_bundled(payload: &[u8], signature: Vec) -> Vec { + SignedCorimBuilder::new(-7_i64, payload.to_vec()) + .set_cwt_claims(CwtClaims::new("test")) + .build_with_signature(signature) + .unwrap() + } + + /// Build a CoRIM-spec-compliant detached `#6.18(COSE_Sign1)` envelope + /// (payload field is nil). + fn make_detached(payload: &[u8], signature: Vec) -> Vec { + SignedCorimBuilder::new(-7_i64, payload.to_vec()) + .set_cwt_claims(CwtClaims::new("test")) + .build_detached_with_signature(signature) + .unwrap() + } + + #[test] + fn split_basic_round_trip() { + let signature = vec![0xDE; 32]; + let bundled = make_bundled(TEST_PAYLOAD, signature.clone()); + + let detached = detach_payload(&bundled).unwrap(); + assert_eq!(detached.document, TEST_PAYLOAD); + + // The detached envelope round-trips as a nil-payload COSE_Sign1 + // with the original signature bytes preserved verbatim. + let decoded = decode_signed_corim(&detached.signature).unwrap(); + assert_eq!(decoded.signature, signature); + assert!(decoded.payload.is_none()); + } + + #[test] + fn split_preserves_signed_bytes() { + // The detached envelope must keep the protected-header bytes + // and signature bytes verbatim -- otherwise external signature + // verification would fail. + let signature = vec![0x01; 64]; + let bundled = make_bundled(&[0xCA, 0xFE, 0xBA, 0xBE], signature.clone()); + + let original = decode_signed_corim(&bundled).unwrap(); + let detached = detach_payload(&bundled).unwrap(); + let after = decode_signed_corim(&detached.signature).unwrap(); + + assert_eq!( + after.protected_header_bytes, + original.protected_header_bytes + ); + assert_eq!(after.signature, original.signature); + assert!(after.payload.is_none()); + } + + #[test] + fn split_already_detached_errors() { + let detached = make_detached(TEST_PAYLOAD, vec![0xDE; 32]); + let err = detach_payload(&detached).unwrap_err(); + assert!( + err.to_string().contains("already detached"), + "Error should mention already detached: {err}" + ); + } + + #[test] + fn split_empty_errors() { + assert!(detach_payload(&[]).is_err()); + } + + #[test] + fn split_large_payload_round_trip() { + let payload: Vec = (0..256).map(|i| (i & 0xFF) as u8).collect(); + let signature = vec![0xAB; 64]; + let bundled = make_bundled(&payload, signature.clone()); + + let detached = detach_payload(&bundled).unwrap(); + assert_eq!(detached.document, payload); + + let decoded = decode_signed_corim(&detached.signature).unwrap(); + assert_eq!(decoded.signature, signature); + assert!(decoded.payload.is_none()); + } + + // ---------- verify_corim_signature ---------- + + use crate::corim_signature::test_helpers::SIGNER; + use crate::corim_signature::test_helpers::sign_envelope_for; + use crate::corim_signature::test_helpers::sign_envelope_no_cert; + use crate::corim_signature::test_helpers::sign_envelope_with; + use corim::types::signed::COSE_HEADER_ALG; + use corim::types::signed::COSE_HEADER_CONTENT_TYPE; + use corim::types::signed::COSE_HEADER_CWT_CLAIMS; + use corim::types::signed::COSE_HEADER_X5CHAIN; + use corim::types::signed::cwt::CWT_CLAIM_ISS; + use corim::types::tags::TAG_SIGNED_CORIM; + use crypto::rsa::RsaKeyPair; + + #[test] + fn verify_ps384_round_trip() { + let document = b"corim-document-bytes"; + let envelope = sign_envelope_for(document, "test"); + + verify_corim_signature(&envelope, document).expect("valid PS384 signature should verify"); + } + + #[test] + fn verify_rejects_tampered_document() { + let document = b"original-document"; + let tampered = b"tampered-document"; + let envelope = sign_envelope_for(document, "test"); + + let err = verify_corim_signature(&envelope, tampered).unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("cryptographic verification failed"), + "Error should report verification failure: {msg}" + ); + } + + #[test] + fn verify_rejects_wrong_issuer() { + // Sign with a fresh key but embed the shared SIGNER's cert in + // x5chain. The verifier extracts the (wrong) cert from the + // header and fails to verify the signature against it. + let document = b"corim-document"; + let signer_key = RsaKeyPair::generate(2048).expect("signer keygen"); + let envelope = sign_envelope_with(&signer_key, document, &SIGNER.cert_der, "test"); + + let err = verify_corim_signature(&envelope, document).unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("cryptographic verification failed"), + "Error should report verification failure: {msg}" + ); + } + + #[test] + fn verify_rejects_unsupported_algorithm() { + // ES256 is a modeled COSE alg but not accepted; only PS384 is. + let envelope = SignedCorimBuilder::new(CoseAlgorithm::Es256, b"corim-document".to_vec()) + .set_cwt_claims(CwtClaims::new("test")) + .add_protected(COSE_HEADER_X5CHAIN, Value::Bytes(b"dummy".to_vec())) + .build_detached_with_signature(vec![0xDE; 64]) + .unwrap(); + let err = verify_corim_signature(&envelope, b"corim-document").unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("unsupported COSE algorithm"), + "Error should report unsupported algorithm: {msg}" + ); + } + + #[test] + fn verify_rejects_malformed_cert() { + // Embed garbage bytes in x5chain. The signature math will run + // only after cert parsing, so the from_der failure fires first. + let document = b"corim-document"; + let envelope = sign_envelope_with( + &SIGNER.key, + document, + b"not-a-der-encoded-x509-cert", + "test", + ); + + let err = verify_corim_signature(&envelope, document).unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("issuer certificate"), + "Error should mention issuer certificate: {msg}" + ); + } + + #[test] + fn verify_rejects_missing_issuer_cert() { + // Envelope without x5chain or x5bag -> cert extraction fails + // before any signature math runs. + let envelope = sign_envelope_no_cert(&SIGNER.key, b"doc"); + let err = verify_corim_signature(&envelope, b"doc").unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("x5chain") && msg.contains("x5bag"), + "Error should mention x5chain/x5bag: {msg}" + ); + } + + #[test] + fn verify_rejects_attached_payload() { + let bundled = make_bundled(b"doc", vec![0xDE; 32]); + let err = verify_corim_signature(&bundled, b"doc").unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("nil") || msg.contains("embedded"), + "Error should mention nil or embedded: {msg}" + ); + } + + #[test] + fn verify_rejects_empty_signature() { + let sig = make_detached(b"doc", vec![]); + let err = verify_corim_signature(&sig, b"doc").unwrap_err(); + assert!( + err.to_string().contains("non-empty"), + "Error should mention non-empty: {err}" + ); + } + + #[test] + fn verify_rejects_empty_input() { + assert!(verify_corim_signature(&[], b"doc").is_err()); + } + + #[test] + fn verify_rejects_untagged_envelope() { + // CoRIM mandates `#6.18` wrapping; a raw 4-element array + // without Tag(18) must be rejected by decode_signed_corim. + let cose = Value::Array(vec![ + Value::Bytes(vec![]), + Value::Map(vec![]), + Value::Null, + Value::Bytes(vec![0xFF; 32]), + ]); + let buf = corim::cbor::encode(&cose).unwrap(); + assert!(verify_corim_signature(&buf, b"doc").is_err()); + } + + #[test] + fn verify_rejects_wrong_content_type() { + // Manually build a detached envelope whose protected header carries + // a non-CoRIM content-type. We do this via the raw CBOR codec + // because SignedCorimBuilder always emits "application/rim+cbor". + let protected_map = Value::Map(vec![ + ( + Value::Integer(COSE_HEADER_ALG.into()), + Value::Integer(CoseAlgorithm::Ps384.to_i64().into()), + ), + ( + Value::Integer(COSE_HEADER_CONTENT_TYPE.into()), + Value::Text("application/x-other".into()), + ), + ( + Value::Integer(COSE_HEADER_CWT_CLAIMS.into()), + Value::Map(vec![( + Value::Integer(CWT_CLAIM_ISS.into()), + Value::Text("test".into()), + )]), + ), + ]); + let protected_bytes = corim::cbor::encode(&protected_map).unwrap(); + let cose = Value::Tag( + TAG_SIGNED_CORIM, + Box::new(Value::Array(vec![ + Value::Bytes(protected_bytes), + Value::Map(vec![]), + Value::Null, + Value::Bytes(vec![0xAB; 16]), + ])), + ); + let buf = corim::cbor::encode(&cose).unwrap(); + let err = verify_corim_signature(&buf, b"doc").unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("content-type"), + "Error should mention content-type: {msg}" + ); + } +} diff --git a/vm/loader/igvmfilegen/src/corim_signature/mod.rs b/vm/loader/igvmfilegen/src/corim_signature/mod.rs new file mode 100644 index 0000000000..095d0e20b4 --- /dev/null +++ b/vm/loader/igvmfilegen/src/corim_signature/mod.rs @@ -0,0 +1,821 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Support for patching CoRIM (Concise Reference Integrity Manifest) headers +//! into an existing IGVM file. +//! +//! CoRIM headers allow embedding signed endorsement for the IGVM file that +//! can be verified by the attestation service. +//! +//! # Module structure +//! +//! - [`envelope`] -- operations on signed CoRIM envelopes (split a bundled +//! envelope into document + detached signature; cryptographically verify +//! a detached signature using the issuer cert from its `x5chain` / +//! `x5bag` header). +//! - [`patch`] -- verify a CoRIM signature against the document +//! already embedded in an IGVM file, then patch (or replace) the +//! corresponding `CorimSignature` header. + +mod envelope; + +#[cfg(test)] +mod test_helpers; + +// Re-export signed-CoRIM operations for use by main.rs and other consumers. +pub use envelope::detach_payload; + +use anyhow::Context; +use igvm::IgvmFile; +use igvm::IgvmInitializationHeader; +use igvm::IgvmRevision; +use igvm_defs::IGVM_FIXED_HEADER; +use igvm_defs::IgvmPlatformType; +use zerocopy::FromBytes; + +/// Determine the [`IgvmRevision`] from raw IGVM binary data by inspecting the +/// fixed header's `format_version` field. +/// +/// Currently only IGVM format version 1 is supported. V2 support would +/// require enabling the `unstable` feature on `igvm_defs`. +fn igvm_revision_from_binary(data: &[u8]) -> anyhow::Result { + let (header, _) = IGVM_FIXED_HEADER::read_from_prefix(data) + .map_err(|_| anyhow::anyhow!("Invalid IGVM file: cannot read fixed header"))?; + + // TODO: Support V2 when CoRIM is required for AArch64. + match header.format_version { + 1 => Ok(IgvmRevision::V1), + other => anyhow::bail!( + "Unsupported IGVM format version {other} (only V1 is supported for CoRIM patching)" + ), + } +} + +/// Verify a CoRIM signature against the document already embedded in an +/// IGVM file, then patch the corresponding `CorimSignature` header. +/// +/// The CoRIM document is expected to already be present in the IGVM file +/// for the target platform (auto-generated at build time). This function: +/// +/// 1. Parses the IGVM file and locates the existing `CorimDocument` for +/// the target platform. +/// 2. If `expected_document` is provided, asserts that it matches the +/// in-file document byte-for-byte. This catches the common UX trap +/// where a user supplies a `--corim-bundle` whose embedded payload +/// was signed against a different document than the one baked into +/// the IGVM file: without this check the failure would surface as an +/// opaque "cryptographic verification failed" error. +/// 3. Cryptographically verifies `corim_signature` against the in-file +/// document via [`envelope::verify_corim_signature`]. The issuer +/// certificate is taken from the signature envelope's `x5chain` / +/// `x5bag` header. +/// 4. Replaces (or adds) the corresponding `CorimSignature` header while +/// leaving the document and all other headers untouched. +/// +/// Verification runs before any structural mutation, so a failed +/// verification leaves no partially-modified output. +/// +/// # Arguments +/// * `igvm_data` - The original IGVM file contents +/// * `corim_signature` - Detached COSE_Sign1 signature payload (nil payload) +/// * `platform` - The target platform type +/// * `expected_document` - Optional CoRIM document bytes that the caller +/// asserts should match the document embedded in the IGVM file. Used +/// when the signature was extracted from a bundled envelope; pass +/// `None` when the caller doesn't have an independent copy. +/// +/// # Returns +/// The modified IGVM file contents with the CoRIM signature header +/// inserted or updated. +/// +/// # Errors +/// Returns an error if the IGVM file has no `CorimDocument` header for +/// the target platform -- the signature cannot be attached without a +/// corresponding document -- or if `expected_document` is provided and +/// does not match the in-file document, or if cryptographic verification +/// of `corim_signature` against the in-file document fails. +pub fn patch( + igvm_data: &[u8], + corim_signature: &[u8], + platform: IgvmPlatformType, + expected_document: Option<&[u8]>, +) -> anyhow::Result> { + // Determine the IGVM revision from the fixed header (needed to + // reconstruct the file after modifying directives). + let revision = igvm_revision_from_binary(igvm_data)?; + + // Parse the IGVM file using the igvm crate's structured API. + // No isolation filter -- we want all headers so we can selectively + // replace only the CoRIM signature for our target platform. + let igvm_file = + IgvmFile::new_from_binary(igvm_data, None).context("parsing input IGVM file")?; + + // Look up the compatibility mask for the requested platform from the + // file's actual platform headers. + let compatibility_mask = + crate::platform_mask::lookup_compatibility_mask(igvm_file.platforms(), platform)?; + + // Build new initialization headers: + // - Keep all non-CoRIM initialization headers unchanged + // - Keep CoRIM headers for other platforms unchanged + // - For our target platform: preserve the existing CoRIM document + // (required invariant -- built into the file at generation time), + // drop any existing CoRIM signature, and re-append the document + // followed by the new signature at the end. + // + // The igvm crate (see `IgvmFile::new()` validation) only enforces + // that `CorimDocument` appears before `CorimSignature` for the same + // compatibility mask. Absolute position relative to other init + // headers (`GuestPolicy`, `RelocatableRegion`, ...) is unconstrained, + // so re-anchoring the pair at the tail is semantically safe. + // Capacity = current count + 1. The output unconditionally appends a + // `CorimDocument` and a `CorimSignature` at the tail, but the loop + // below skips at least the matching `CorimDocument` (and also the + // matching `CorimSignature` if one was present), so the worst-case + // final length is `len + 1`. `Vec::push` only reallocates when + // `len == capacity` *before* the push, so filling to capacity does + // not trigger a realloc. + let mut new_initializations = Vec::with_capacity(igvm_file.initializations().len() + 1); + let mut existing_doc = None; + let mut replaced_existing_sig = false; + + for header in igvm_file.initializations() { + match header { + IgvmInitializationHeader::CorimDocument { + compatibility_mask: mask, + document, + } if *mask == compatibility_mask => { + existing_doc = Some(document.clone()); + } + IgvmInitializationHeader::CorimSignature { + compatibility_mask: mask, + .. + } if *mask == compatibility_mask => { + replaced_existing_sig = true; + } + other => { + new_initializations.push(other.clone()); + } + } + } + + let existing_doc = existing_doc.ok_or_else(|| { + anyhow::anyhow!( + "Cannot patch CoRIM signature for platform {platform:?} \ + (compatibility mask 0x{compatibility_mask:X}): no CoRIM document \ + present in the IGVM file. The document must be embedded at \ + IGVM generation time before a signature can be attached." + ) + })?; + + // If the caller supplied the document they think the signature was + // produced against (typically extracted from a bundled COSE_Sign1 + // envelope), check that it matches the document we just located in + // the IGVM file. This produces a targeted error before the + // cryptographic verify path would otherwise fail with an opaque + // "verification failed" message. + if let Some(expected) = expected_document + && expected != existing_doc.as_slice() + { + anyhow::bail!( + "CoRIM document mismatch for platform {platform:?} \ + (compatibility mask 0x{compatibility_mask:X}): the document \ + carried by the input bundle ({} bytes) does not byte-match \ + the document embedded in the IGVM file ({} bytes). The \ + bundle was signed against a different document; re-sign \ + against the IGVM-embedded document or supply only the \ + detached signature via `--corim-signature`.", + expected.len(), + existing_doc.len(), + ); + } + + // Verify the signature against the in-file document before mutating + // anything. The issuer certificate is taken from the envelope's + // protected header (x5chain / x5bag). + envelope::verify_corim_signature(corim_signature, &existing_doc) + .context("verifying CoRIM signature against the in-file document")?; + + // Re-append the existing document followed by the new signature so + // they sit adjacently in the required order. + new_initializations.push(IgvmInitializationHeader::CorimDocument { + compatibility_mask, + document: existing_doc, + }); + new_initializations.push(IgvmInitializationHeader::CorimSignature { + compatibility_mask, + signature: corim_signature.to_vec(), + }); + + // Reconstruct the IGVM file with modified initialization headers. + // IgvmFile::new() validates the header structure (e.g., at most one + // CoRIM document/signature per compatibility mask, document before + // signature). + let new_igvm = IgvmFile::new( + revision, + igvm_file.platforms().to_vec(), + new_initializations, + igvm_file.directives().to_vec(), + ) + .context("constructing IGVM file with new CoRIM headers")?; + + // Serialize back to binary. The igvm crate handles file offsets, + // alignment, and CRC32 checksum. + let mut output = Vec::new(); + new_igvm + .serialize(&mut output) + .context("serializing patched IGVM file")?; + + tracing::info!( + original_size = igvm_data.len(), + new_size = output.len(), + signature_size = corim_signature.len(), + platform = ?platform, + replaced_existing_signature = replaced_existing_sig, + "Patched CoRIM signature into IGVM file", + ); + + Ok(output) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::corim_signature::test_helpers::sign_envelope_for; + use igvm::IgvmDirectiveHeader; + use igvm::IgvmPlatformHeader; + use igvm::IgvmSerializer; + use igvm::corim::launch_measurement::LaunchMeasurement; + use igvm::corim::launch_measurement::MeasurementKind; + use igvm_defs::IGVM_FIXED_HEADER; + use igvm_defs::IGVM_VHS_SUPPORTED_PLATFORM; + use igvm_defs::IgvmPageDataFlags; + use igvm_defs::IgvmPageDataType; + use test_with_tracing::test; + + fn new_platform( + compatibility_mask: u32, + platform_type: IgvmPlatformType, + ) -> IgvmPlatformHeader { + IgvmPlatformHeader::SupportedPlatform(IGVM_VHS_SUPPORTED_PLATFORM { + compatibility_mask, + highest_vtl: 0, + platform_type, + platform_version: 1, + shared_gpa_boundary: 0, + }) + } + + fn new_page_data(page: u64, compatibility_mask: u32, data: &[u8]) -> IgvmDirectiveHeader { + IgvmDirectiveHeader::PageData { + gpa: page * 4096, + compatibility_mask, + flags: IgvmPageDataFlags::new(), + data_type: IgvmPageDataType::NORMAL, + data: data.to_vec(), + } + } + + /// Build a minimal IGVM binary from given headers (no CoRIM). + fn build_igvm( + platforms: Vec, + directives: Vec, + ) -> Vec { + let igvm = + IgvmFile::new(IgvmRevision::V1, platforms, vec![], directives).expect("valid IgvmFile"); + let mut output = Vec::new(); + igvm.serialize(&mut output).expect("serialize"); + output + } + + /// Build a minimal IGVM binary with pre-embedded CoRIM document(s). + /// `documents` is a list of `(compatibility_mask, document_bytes)` + /// pairs. The order of resulting `CorimDocument` initializations + /// matches the list order. + fn build_igvm_with_corim_docs( + platforms: Vec, + directives: Vec, + documents: Vec<(u32, Vec)>, + ) -> Vec { + let initializations: Vec = documents + .into_iter() + .map(|(mask, doc)| IgvmInitializationHeader::CorimDocument { + compatibility_mask: mask, + document: doc, + }) + .collect(); + let igvm = IgvmFile::new(IgvmRevision::V1, platforms, initializations, directives) + .expect("valid IgvmFile"); + let mut output = Vec::new(); + igvm.serialize(&mut output).expect("serialize"); + output + } + + /// Extracted CoRIM header info for test assertions. + struct CorimHeaderInfo { + compatibility_mask: u32, + payload: Vec, + } + + /// Parse an IGVM binary and extract CoRIM document and signature headers + /// using the structured `IgvmFile` API. + fn extract_corim_headers(data: &[u8]) -> (Vec, Vec) { + let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM file"); + let mut documents = Vec::new(); + let mut signatures = Vec::new(); + + for header in igvm.initializations() { + match header { + IgvmInitializationHeader::CorimDocument { + compatibility_mask, + document, + } => { + documents.push(CorimHeaderInfo { + compatibility_mask: *compatibility_mask, + payload: document.clone(), + }); + } + IgvmInitializationHeader::CorimSignature { + compatibility_mask, + signature, + } => { + signatures.push(CorimHeaderInfo { + compatibility_mask: *compatibility_mask, + payload: signature.clone(), + }); + } + _ => {} + } + } + + (documents, signatures) + } + + /// Count directive headers (excluding CoRIM) in the IGVM binary. + fn count_non_corim_directive_headers(data: &[u8]) -> usize { + let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM file"); + igvm.directives().len() + } + + /// Extract platform types and masks from the IGVM binary. + fn extract_platform_types(data: &[u8]) -> Vec<(IgvmPlatformType, u32)> { + let igvm = IgvmFile::new_from_binary(data, None).expect("valid IGVM file"); + igvm.platforms() + .iter() + .map(|p| match p { + IgvmPlatformHeader::SupportedPlatform(plat) => { + (plat.platform_type, plat.compatibility_mask) + } + }) + .collect() + } + + #[test] + fn test_patch_corim_add_signature() { + let page_data = vec![0xCC; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &page_data)], + vec![(0x1, b"corim-payload".to_vec())], + ); + + let sig = sign_envelope_for(b"corim-payload", "test"); + let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("patch should succeed"); + + let (docs, sigs) = extract_corim_headers(&patched); + assert_eq!(docs.len(), 1); + assert_eq!(sigs.len(), 1); + assert_eq!(docs[0].payload, b"corim-payload"); + assert_eq!(sigs[0].payload, sig); + // Document and signature must share the same mask. + assert_eq!(docs[0].compatibility_mask, sigs[0].compatibility_mask); + } + + #[test] + fn test_patch_corim_preserves_non_corim_directives() { + let data1 = vec![0x11; 4096]; + let data2 = vec![0x22; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &data1), new_page_data(1, 0x1, &data2)], + vec![(0x1, b"doc".to_vec())], + ); + + let original_count = count_non_corim_directive_headers(&igvm_data); + + let sig = sign_envelope_for(b"doc", "test"); + let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("patch should succeed"); + + let patched_count = count_non_corim_directive_headers(&patched); + assert_eq!(original_count, patched_count); + } + + #[test] + fn test_patch_corim_preserves_platform_headers() { + let data = vec![0x55; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![ + new_platform(0x1, IgvmPlatformType::VSM_ISOLATION), + new_platform(0x2, IgvmPlatformType::SEV_SNP), + ], + vec![new_page_data(0, 0x1, &data), new_page_data(0, 0x2, &data)], + vec![(0x1, b"vbs-corim".to_vec())], + ); + + let original_platforms = extract_platform_types(&igvm_data); + + let sig = sign_envelope_for(b"vbs-corim", "test"); + let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("patch should succeed"); + + let patched_platforms = extract_platform_types(&patched); + assert_eq!(original_platforms, patched_platforms); + } + + #[test] + fn test_patch_corim_uses_correct_mask() { + let data = vec![0x55; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![ + new_platform(0x1, IgvmPlatformType::VSM_ISOLATION), + new_platform(0x2, IgvmPlatformType::SEV_SNP), + ], + vec![new_page_data(0, 0x1, &data), new_page_data(0, 0x2, &data)], + vec![(0x1, b"vbs-corim".to_vec()), (0x2, b"snp-corim".to_vec())], + ); + + // Patch signature for VBS only. + let sig = sign_envelope_for(b"vbs-corim", "test"); + let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("patch should succeed"); + + let (docs, sigs) = extract_corim_headers(&patched); + assert_eq!(docs.len(), 2, "both platform docs preserved"); + assert_eq!(sigs.len(), 1, "only VBS signature added"); + assert_eq!(sigs[0].compatibility_mask, 0x1); + } + + #[test] + fn test_patch_corim_error_platform_not_in_file() { + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &vec![0; 4096])], + vec![(0x1, b"doc".to_vec())], + ); + + // Platform-lookup failure fires before signature verification, + // so the envelope contents are irrelevant here. + let sig = sign_envelope_for(b"doc", "test"); + let result = patch( + &igvm_data, + &sig, + IgvmPlatformType::SEV_SNP, // Not in file + None, + ); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!( + msg.contains("not found"), + "expected 'not found' error, got: {msg}" + ); + } + + #[test] + fn test_patch_corim_error_missing_document() { + // Signature-only patching on a file without an existing document + // for the target platform must fail with a targeted error. + let igvm_data = build_igvm( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &vec![0; 4096])], + ); + + // Missing-document failure fires before signature verification. + let sig = sign_envelope_for(b"doc", "test"); + let err = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None).unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("no CoRIM document"), + "expected 'no CoRIM document' error, got: {msg}" + ); + } + + #[test] + fn test_patch_corim_output_is_valid_igvm_header() { + let page_data = vec![0x77; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &page_data)], + vec![(0x1, b"round-trip-doc".to_vec())], + ); + + let sig = sign_envelope_for(b"round-trip-doc", "test"); + let patched = patch(&igvm_data, &sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("patch should succeed"); + + let fixed = IGVM_FIXED_HEADER::read_from_prefix(&patched) + .expect("valid fixed header") + .0; + assert_eq!(fixed.magic, igvm_defs::IGVM_MAGIC_VALUE); + assert_eq!(fixed.format_version, 1); + assert_eq!(fixed.total_file_size as usize, patched.len()); + + // `IgvmFile::new_from_binary` recomputes and verifies the CRC32 + // over the variable header section. A successful re-parse here + // confirms the patched file's CRC32 was correctly recomputed. + IgvmFile::new_from_binary(&patched, None) + .expect("patched file must pass IGVM CRC32 validation"); + } + + #[test] + fn test_patch_corim_bundle_document_mismatch() { + // When the caller supplies a bundle whose payload differs from + // the document embedded in the IGVM file, `patch` must surface a + // targeted mismatch error before attempting cryptographic + // verification (which would otherwise fail with an opaque + // \"verification failed\" message). + let page_data = vec![0xAB; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &page_data)], + vec![(0x1, b"in-file-doc".to_vec())], + ); + + // Build a syntactically valid signature; the mismatch check must + // fire before envelope::verify_corim_signature is reached. + let sig = sign_envelope_for(b"in-file-doc", "test"); + + let err = patch( + &igvm_data, + &sig, + IgvmPlatformType::VSM_ISOLATION, + Some(b"different-bundled-doc"), + ) + .unwrap_err(); + let msg = format!("{err:#}"); + assert!( + msg.contains("does not byte-match"), + "expected bundle/in-file mismatch error, got: {msg}" + ); + } + + #[test] + fn test_patch_corim_round_trip_reparse() { + // Verify that a patched file can be re-parsed and re-patched + // (round-trip through new_from_binary works after the igvm crate + // CoRIM parsing fix). + let page_data = vec![0xDD; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &page_data)], + vec![(0x1, b"first-doc".to_vec())], + ); + + let first_sig = sign_envelope_for(b"first-doc", "test"); + let patched = patch( + &igvm_data, + &first_sig, + IgvmPlatformType::VSM_ISOLATION, + None, + ) + .expect("first patch should succeed"); + + // Re-patching with a different signature must also succeed and + // must preserve the document. + let second_sig = sign_envelope_for(b"first-doc", "test-alt"); + let repatched = patch(&patched, &second_sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("re-patching should succeed"); + + let (docs, sigs) = extract_corim_headers(&repatched); + assert_eq!(docs.len(), 1); + assert_eq!(sigs.len(), 1); + assert_eq!(docs[0].payload, b"first-doc"); + assert_eq!(sigs[0].payload, second_sig); + } + + #[test] + fn test_patch_corim_replace_signature_preserves_document() { + let page_data = vec![0xFF; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![new_page_data(0, 0x1, &page_data)], + vec![(0x1, b"keep-this-doc".to_vec())], + ); + + // First: attach an initial signature. + let first_sig = sign_envelope_for(b"keep-this-doc", "test"); + let with_sig = patch( + &igvm_data, + &first_sig, + IgvmPlatformType::VSM_ISOLATION, + None, + ) + .expect("initial signature attach"); + + // Replace it with a different signature. + let second_sig = sign_envelope_for(b"keep-this-doc", "test-alt"); + let updated = patch( + &with_sig, + &second_sig, + IgvmPlatformType::VSM_ISOLATION, + None, + ) + .expect("signature replacement"); + + let (docs, sigs) = extract_corim_headers(&updated); + assert_eq!(docs.len(), 1); + assert_eq!(sigs.len(), 1); + assert_eq!(docs[0].payload, b"keep-this-doc"); + assert_eq!(sigs[0].payload, second_sig); + } + + /// Helper: build a multi-platform IGVM file with CoRIM documents AND + /// signatures already attached for both VBS (mask=0x1) and SNP + /// (mask=0x2). Returns the file along with the VBS and SNP signatures + /// so callers can use them in assertions. + fn build_multi_platform_with_corim() -> (Vec, Vec, Vec) { + let data = vec![0x55; 4096]; + let igvm_data = build_igvm_with_corim_docs( + vec![ + new_platform(0x1, IgvmPlatformType::VSM_ISOLATION), + new_platform(0x2, IgvmPlatformType::SEV_SNP), + ], + vec![new_page_data(0, 0x1, &data), new_page_data(0, 0x2, &data)], + vec![(0x1, b"vbs-doc".to_vec()), (0x2, b"snp-doc".to_vec())], + ); + + // Attach signature to VBS. + let vbs_sig = sign_envelope_for(b"vbs-doc", "test"); + let with_vbs = patch(&igvm_data, &vbs_sig, IgvmPlatformType::VSM_ISOLATION, None) + .expect("VBS signature attach"); + + // Attach signature to SNP. + let snp_sig = sign_envelope_for(b"snp-doc", "test"); + let with_both = patch(&with_vbs, &snp_sig, IgvmPlatformType::SEV_SNP, None) + .expect("SNP signature attach"); + + (with_both, vbs_sig, snp_sig) + } + + #[test] + fn test_multi_platform_corim_interleaved_ordering_is_valid() { + let (with_both, _vbs_sig, _snp_sig) = build_multi_platform_with_corim(); + + let (docs, sigs) = extract_corim_headers(&with_both); + assert_eq!(docs.len(), 2, "should have docs for both platforms"); + assert_eq!(sigs.len(), 2, "should have sigs for both platforms"); + + let reparsed = IgvmFile::new_from_binary(&with_both, None) + .expect("interleaved CoRIM ordering should be parseable"); + + let corim_count = reparsed + .initializations() + .iter() + .filter(|h| { + matches!( + h, + IgvmInitializationHeader::CorimDocument { .. } + | IgvmInitializationHeader::CorimSignature { .. } + ) + }) + .count(); + assert_eq!(corim_count, 4, "should have 4 CoRIM init headers total"); + } + + #[test] + fn test_multi_platform_replace_signature_preserves_other_platform() { + // Replace SNP signature while VBS CoRIM is also present. + // VBS headers must be completely unchanged. + let (with_both, vbs_sig, _snp_sig) = build_multi_platform_with_corim(); + + let new_snp_sig = sign_envelope_for(b"snp-doc", "test-alt"); + let updated = patch(&with_both, &new_snp_sig, IgvmPlatformType::SEV_SNP, None) + .expect("update SNP signature"); + + let (docs, sigs) = extract_corim_headers(&updated); + assert_eq!(docs.len(), 2); + assert_eq!(sigs.len(), 2); + + let vbs_doc = docs.iter().find(|d| d.compatibility_mask == 0x1).unwrap(); + let snp_doc = docs.iter().find(|d| d.compatibility_mask == 0x2).unwrap(); + let vbs_sig_after = sigs.iter().find(|s| s.compatibility_mask == 0x1).unwrap(); + let snp_sig_after = sigs.iter().find(|s| s.compatibility_mask == 0x2).unwrap(); + + assert_eq!(vbs_doc.payload, b"vbs-doc", "VBS doc must be unchanged"); + assert_eq!(snp_doc.payload, b"snp-doc", "SNP doc preserved"); + assert_eq!(vbs_sig_after.payload, vbs_sig, "VBS sig must be unchanged"); + assert_eq!( + snp_sig_after.payload, new_snp_sig, + "SNP sig must be the new one" + ); + + IgvmFile::new_from_binary(&updated, None).expect("output should be valid IGVM"); + } + + #[test] + fn test_multi_platform_sequential_updates_both_platforms() { + // Update VBS first, then SNP. Verify both updates are reflected + // and the file remains valid after each step. + let (with_both, _vbs_sig, _snp_sig) = build_multi_platform_with_corim(); + + // Step 1: replace VBS signature. + let new_vbs_sig = sign_envelope_for(b"vbs-doc", "test-alt"); + let after_vbs = patch( + &with_both, + &new_vbs_sig, + IgvmPlatformType::VSM_ISOLATION, + None, + ) + .expect("VBS update"); + + IgvmFile::new_from_binary(&after_vbs, None).expect("valid after VBS update"); + + // Step 2: replace SNP signature. + let new_snp_sig = sign_envelope_for(b"snp-doc", "test-alt"); + let after_snp = + patch(&after_vbs, &new_snp_sig, IgvmPlatformType::SEV_SNP, None).expect("SNP update"); + + let (docs, sigs) = extract_corim_headers(&after_snp); + assert_eq!(docs.len(), 2); + assert_eq!(sigs.len(), 2); + + let vbs_doc = docs.iter().find(|d| d.compatibility_mask == 0x1).unwrap(); + let snp_doc = docs.iter().find(|d| d.compatibility_mask == 0x2).unwrap(); + let vbs_sig = sigs.iter().find(|s| s.compatibility_mask == 0x1).unwrap(); + let snp_sig = sigs.iter().find(|s| s.compatibility_mask == 0x2).unwrap(); + + assert_eq!(vbs_doc.payload, b"vbs-doc", "VBS doc preserved"); + assert_eq!(snp_doc.payload, b"snp-doc", "SNP doc preserved"); + assert_eq!(vbs_sig.payload, new_vbs_sig, "VBS sig from step 1"); + assert_eq!(snp_sig.payload, new_snp_sig, "SNP sig from step 2"); + + IgvmFile::new_from_binary(&after_snp, None).expect("valid after both updates"); + } + + /// End-to-end exercise of the production pipeline: build a real IGVM + /// file, attach a real CoRIM document via `IgvmSerializer::add_corim` + /// (the same call site `create_igvm_file` uses), then sign the + /// resulting CoRIM document with PS384 and patch the signature in via + /// `patch`. Verifies that the patched file still + /// parses, the original document survives unchanged, and the patched + /// signature matches what was produced from the real CoRIM bytes. + #[test] + fn test_e2e_real_corim_build_and_patch() { + let platform = IgvmPlatformType::VSM_ISOLATION; + let mask = 0x1; + + // Build a minimal valid IGVM file with one platform and one page. + let page_data = vec![0xAA; 4096]; + let base = build_igvm( + vec![new_platform(mask, platform)], + vec![new_page_data(0, mask, &page_data)], + ); + + // Attach a real CoRIM launch endorsement, exactly as the + // production `create_igvm_file` post-merge step does. + let parsed = IgvmFile::new_from_binary(&base, None).expect("parse base IGVM"); + let mut serializer = IgvmSerializer::new(&parsed).expect("construct serializer"); + let mut le = LaunchMeasurement::for_platform(platform).expect("launch endorsement"); + le.set_measurement(MeasurementKind::Launch) + .expect("set measurement kind"); + le.endorse(1) + .with(MeasurementKind::Launch) + .expect("CES with") + .finish() + .expect("CES finish"); + let real_corim = serializer + .add_corim(platform, le.build()) + .expect("add_corim") + .to_vec(); + + let mut with_doc = Vec::new(); + serializer.serialize(&mut with_doc).expect("serialize"); + + // The serializer must have embedded exactly the CoRIM bytes it + // returned, and no signature should be present yet. + let (docs, sigs) = extract_corim_headers(&with_doc); + assert_eq!(docs.len(), 1, "one CoRIM document embedded"); + assert!(sigs.is_empty(), "no signature before patch"); + assert_eq!( + docs[0].payload, real_corim, + "embedded doc matches add_corim return" + ); + + // Sign the real CoRIM document with PS384 and patch the signature + // into the IGVM file. + let signature = sign_envelope_for(&real_corim, "e2e-test"); + let patched = patch(&with_doc, &signature, platform, None).expect("patch signature"); + + // The patched file must still parse, preserve the real document, + // and now carry exactly the signature we produced. + IgvmFile::new_from_binary(&patched, None).expect("patched file parses"); + let (docs, sigs) = extract_corim_headers(&patched); + assert_eq!(docs.len(), 1, "one CoRIM document after patch"); + assert_eq!(sigs.len(), 1, "one CoRIM signature after patch"); + assert_eq!(docs[0].compatibility_mask, mask); + assert_eq!(sigs[0].compatibility_mask, mask); + assert_eq!(docs[0].payload, real_corim, "real CoRIM doc preserved"); + assert_eq!(sigs[0].payload, signature, "real signature attached"); + } +} diff --git a/vm/loader/igvmfilegen/src/corim_signature/test_helpers.rs b/vm/loader/igvmfilegen/src/corim_signature/test_helpers.rs new file mode 100644 index 0000000000..98d42532be --- /dev/null +++ b/vm/loader/igvmfilegen/src/corim_signature/test_helpers.rs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Shared CoRIM test helpers. +//! +//! Generating an RSA-2048 keypair is the slow part of any PS384 signing +//! test (often hundreds of milliseconds). Centralizing the keygen behind +//! a single `LazyLock` makes the whole CoRIM test suite share one key +//! and one self-signed cert, which keeps the suite under a second. + +use corim::cbor::value::Value; +use corim::types::signed::COSE_HEADER_X5CHAIN; +use corim::types::signed::CoseAlgorithm; +use corim::types::signed::CwtClaims; +use corim::types::signed::SignedCorimBuilder; +use crypto::HashAlgorithm; +use crypto::rsa::RsaKeyPair; +use crypto::x509::X509Certificate; +use std::sync::LazyLock; + +/// Shared 2048-bit RSA key + self-signed cert reused across every test +/// that needs to produce a real PS384-signed envelope. +pub(crate) struct TestSigner { + pub(crate) key: RsaKeyPair, + pub(crate) cert_der: Vec, +} + +pub(crate) static SIGNER: LazyLock = LazyLock::new(|| { + let key = RsaKeyPair::generate(2048).expect("RSA keygen"); + let cert_der = X509Certificate::build_self_signed(&key, "US", "WA", "Redmond", "Test", "test") + .expect("self-signed cert") + .to_der() + .expect("cert to_der"); + TestSigner { key, cert_der } +}); + +/// Build a PS384-signed detached CoRIM envelope over `document` using +/// the shared test key, embedding the shared self-signed cert in the +/// `x5chain` protected header so verification accepts it. +/// +/// `iss` is included in the CWT claims so two callers can produce +/// envelopes with distinct protected-header bytes (and therefore +/// distinct signatures) over the same document. +pub(crate) fn sign_envelope_for(document: &[u8], iss: &str) -> Vec { + sign_envelope_with(&SIGNER.key, document, &SIGNER.cert_der, iss) +} + +/// Like [`sign_envelope_for`], but lets the caller supply a specific +/// key and cert. Used by tests that exercise the wrong-issuer or +/// malformed-cert verification paths. +pub(crate) fn sign_envelope_with( + key: &RsaKeyPair, + document: &[u8], + cert_for_x5chain: &[u8], + iss: &str, +) -> Vec { + let mut builder = SignedCorimBuilder::new(CoseAlgorithm::Ps384, document.to_vec()) + .set_cwt_claims(CwtClaims::new(iss)) + .add_protected(COSE_HEADER_X5CHAIN, Value::Bytes(cert_for_x5chain.to_vec())); + let tbs = builder.to_be_signed(&[]).expect("TBS bytes"); + let sig = key + .pss_sign(&tbs, HashAlgorithm::Sha384) + .expect("RSA-PSS sign"); + builder + .build_detached_with_signature(sig) + .expect("envelope builds") +} + +/// Build a PS384-signed detached envelope without any `x5chain`/`x5bag` +/// header (used to exercise the missing-cert rejection path). +pub(crate) fn sign_envelope_no_cert(key: &RsaKeyPair, document: &[u8]) -> Vec { + let mut builder = SignedCorimBuilder::new(CoseAlgorithm::Ps384, document.to_vec()) + .set_cwt_claims(CwtClaims::new("test")); + let tbs = builder.to_be_signed(&[]).expect("TBS bytes"); + let sig = key + .pss_sign(&tbs, HashAlgorithm::Sha384) + .expect("RSA-PSS sign"); + builder + .build_detached_with_signature(sig) + .expect("envelope builds") +} diff --git a/vm/loader/igvmfilegen/src/file_loader.rs b/vm/loader/igvmfilegen/src/file_loader.rs index ebb10d1354..fa1ff274a2 100644 --- a/vm/loader/igvmfilegen/src/file_loader.rs +++ b/vm/loader/igvmfilegen/src/file_loader.rs @@ -3,13 +3,6 @@ //! Implements a loader that serializes the loaded state into the IGVM binary format. -use crate::identity_mapping::Measurement; -use crate::identity_mapping::SnpMeasurement; -use crate::identity_mapping::TdxMeasurement; -use crate::identity_mapping::VbsMeasurement; -use crate::signed_measurement::generate_snp_measurement; -use crate::signed_measurement::generate_tdx_measurement; -use crate::signed_measurement::generate_vbs_measurement; use crate::vp_context_builder::VpContextBuilder; use crate::vp_context_builder::VpContextPageState; use crate::vp_context_builder::VpContextState; @@ -172,15 +165,6 @@ pub trait IgvmLoaderRegister: VbsRegister { Box>, ); - /// Generate a measurement based on isolation type. - fn generate_measurement( - isolation: LoaderIsolationType, - initialization_headers: &[IgvmInitializationHeader], - directive_headers: &[IgvmDirectiveHeader], - svn: u32, - debug_enabled: bool, - ) -> anyhow::Result>; - /// The IGVM file revision to use for the built igvm file. fn igvm_revision() -> IgvmRevision; } @@ -262,46 +246,6 @@ impl IgvmLoaderRegister for X86Register { } } - fn generate_measurement( - isolation: LoaderIsolationType, - initialization_headers: &[IgvmInitializationHeader], - directive_headers: &[IgvmDirectiveHeader], - svn: u32, - debug_enabled: bool, - ) -> anyhow::Result> { - let measurement = match isolation { - LoaderIsolationType::Snp { .. } => { - let ld = generate_snp_measurement(initialization_headers, directive_headers, svn) - .context("generating snp measurement failed")?; - Some(Measurement::Snp(SnpMeasurement::new( - ld, - svn, - debug_enabled, - ))) - } - LoaderIsolationType::Tdx { .. } => { - let mrtd = generate_tdx_measurement(directive_headers) - .context("generating tdx measurement failed")?; - Some(Measurement::Tdx(TdxMeasurement::new( - mrtd, - svn, - debug_enabled, - ))) - } - LoaderIsolationType::Vbs { enable_debug } => { - let boot_digest = generate_vbs_measurement(directive_headers, enable_debug, svn) - .context("generating vbs measurement failed")?; - Some(Measurement::Vbs(VbsMeasurement::new( - boot_digest, - svn, - debug_enabled, - ))) - } - _ => None, - }; - Ok(measurement) - } - fn igvm_revision() -> IgvmRevision { // For now, x86 built files always uses V1 of the IGVM format. This is // to maintain compatibility with older OS repo loaders that do not @@ -323,16 +267,6 @@ impl IgvmLoaderRegister for Aarch64Register { unreachable!("should never be called") } - fn generate_measurement( - _isolation: LoaderIsolationType, - _initialization_headers: &[IgvmInitializationHeader], - _directive_headers: &[IgvmDirectiveHeader], - _svn: u32, - _debug_enabled: bool, - ) -> anyhow::Result> { - Ok(None) - } - fn igvm_revision() -> IgvmRevision { // AArch64 IGVM files are always V2. IgvmRevision::V2 { @@ -490,7 +424,6 @@ impl Display for MapFile { pub struct IgvmOutput { pub guest: IgvmFile, pub map: MapFile, - pub doc: Option, } impl IgvmLoader { @@ -582,7 +515,7 @@ impl IgvmLoader { } /// Finalize the loader state, returning an IGVM file. - pub fn finalize(mut self, guest_svn: u32) -> anyhow::Result { + pub fn finalize(mut self) -> anyhow::Result { // Finalize any VP state. let mut state = Vec::new(); self.vp_context.take().unwrap().finalize(&mut state); @@ -659,20 +592,12 @@ impl IgvmLoader { )); } - // Merge the page_data_directives into the others directives. This must be done before - // generating the launch measurement. + // Merge the page_data_directives into the others directives. This + // must be done before constructing the IGVM file so that subsequent + // measurement computation (in `IgvmSerializer`) sees the full set + // of directives. self.directives.append(&mut self.page_data_directives); - // Generate the launch measurement for the isolation type being used. - // The measurement is output for external signing. - let doc = R::generate_measurement( - self.isolation_type, - &self.initialization_headers, - &self.directives, - guest_svn, - self.confidential_debug(), - )?; - // Display a report about the build igvm file's layout. let map_file = MapFile { isolation: self.isolation_type, @@ -715,7 +640,6 @@ impl IgvmLoader { let output = IgvmOutput { guest: igvm_file, map: map_file, - doc, }; Ok(output) } @@ -773,17 +697,6 @@ impl IgvmLoader { R::arch() } - /// Returns true if this is an isolated guest with debug enabled, false - /// otherwise. - pub fn confidential_debug(&self) -> bool { - match self.isolation_type { - LoaderIsolationType::Vbs { enable_debug } => enable_debug, - LoaderIsolationType::Snp { policy, .. } => policy.debug() == 1, - LoaderIsolationType::Tdx { policy } => policy.debug_allowed() == 1, - _ => false, - } - } - pub fn loader(&mut self) -> IgvmVtlLoader<'_, R> { IgvmVtlLoader { vtl: self.max_vtl, @@ -830,9 +743,12 @@ impl IgvmLoader { } } - // Data size must match SNP VMSA size. - if data.len() != size_of::() { - anyhow::bail!("data len {:x} does not match VMSA size", data.len()); + // Data must not exceed the SNP VMSA size. The VP context builder + // produces the architectural VMSA (`x86defs::snp::SevVmsa`, 1648 + // bytes); the igvm crate's `SevVmsa` is padded out to a full 4K + // page, so zero-pad the input to match before reading. + if data.len() > size_of::() { + anyhow::bail!("data len {:x} exceeds VMSA size", data.len()); } // Page count must be 1. @@ -840,11 +756,16 @@ impl IgvmLoader { anyhow::bail!("page count {page_count:x} for snp vmsa is not 1"); } + let mut padded = vec![0u8; size_of::()]; + padded[..data.len()].copy_from_slice(data); + self.directives.push(IgvmDirectiveHeader::SnpVpContext { gpa: page_base * PAGE_SIZE_4K, compatibility_mask: DEFAULT_COMPATIBILITY_MASK, vp_index: 0, - vmsa: Box::new(SevVmsa::read_from_bytes(data).expect("should be correct size")), // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759) + vmsa: Box::new( + SevVmsa::read_from_bytes(padded.as_slice()).expect("should be correct size"), + ), // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759) }); } else { for page in page_base..page_base + page_count { @@ -1251,7 +1172,7 @@ impl ImageLoad for IgvmVtlLoader mod tests { use super::IgvmLoader; use super::*; - use crate::identity_mapping::Measurement; + use igvm::IgvmSerializer; use loader::importer::BootPageAcceptance; use loader::importer::ImageLoad; use loader_defs::paravisor::ImportedRegionDescriptor; @@ -1288,12 +1209,12 @@ mod tests { .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data) .unwrap(); - let igvm_output = loader.finalize(1).unwrap(); - let doc = igvm_output.doc.expect("doc"); - let Measurement::Snp(snp_measurement) = doc else { - panic!("known to be snp") - }; - assert_eq!(ref_ld, snp_measurement.series[0].reference.snp_ld); + let igvm_output = loader.finalize().unwrap(); + let serializer = IgvmSerializer::new(&igvm_output.guest).unwrap(); + let measurement = serializer + .measurement_for(IgvmPlatformType::SEV_SNP) + .expect("snp measurement"); + assert_eq!(ref_ld.as_slice(), measurement.digest.as_slice()); } #[test] @@ -1326,12 +1247,12 @@ mod tests { .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data) .unwrap(); - let igvm_output = loader.finalize(1).unwrap(); - let doc = igvm_output.doc.expect("doc"); - let Measurement::Tdx(tdx_measurement) = doc else { - panic!("known to be tdx") - }; - assert_eq!(ref_mrtd, tdx_measurement.series[0].reference.tdx_mrtd); + let igvm_output = loader.finalize().unwrap(); + let serializer = IgvmSerializer::new(&igvm_output.guest).unwrap(); + let measurement = serializer + .measurement_for(IgvmPlatformType::TDX) + .expect("tdx measurement"); + assert_eq!(ref_mrtd.as_slice(), measurement.digest.as_slice()); } #[test] @@ -1365,15 +1286,12 @@ mod tests { .unwrap(); } - let igvm_output = loader.finalize(1).unwrap(); - let doc = igvm_output.doc.expect("doc"); - let Measurement::Vbs(vbs_measurement) = doc else { - panic!("known to be vbs") - }; - assert_eq!( - ref_digest, - vbs_measurement.series[0].reference.vbs_boot_digest - ); + let igvm_output = loader.finalize().unwrap(); + let serializer = IgvmSerializer::new(&igvm_output.guest).unwrap(); + let measurement = serializer + .measurement_for(IgvmPlatformType::VSM_ISOLATION) + .expect("vbs measurement"); + assert_eq!(ref_digest.as_slice(), measurement.digest.as_slice()); } #[test] diff --git a/vm/loader/igvmfilegen/src/main.rs b/vm/loader/igvmfilegen/src/main.rs index 334097ce23..53bc4b615f 100644 --- a/vm/loader/igvmfilegen/src/main.rs +++ b/vm/loader/igvmfilegen/src/main.rs @@ -5,20 +5,38 @@ #![forbid(unsafe_code)] +#[cfg(not(test))] +crypto::ensure_single_backend!(); + +mod corim_signature; mod file_loader; mod identity_mapping; -mod signed_measurement; +mod measurement_diag; +mod platform_mask; mod vp_context_builder; +use crate::corim_signature::detach_payload; use crate::file_loader::IgvmLoader; use crate::file_loader::LoaderIsolationType; +use crate::identity_mapping::Measurement; +use crate::identity_mapping::SnpMeasurement; +use crate::identity_mapping::TdxMeasurement; +use crate::identity_mapping::VbsMeasurement; +use crate::measurement_diag::log_measurement_diagnostic; use anyhow::Context; use anyhow::bail; use clap::Parser; +use clap::ValueEnum; use file_loader::IgvmLoaderRegister; use file_loader::IgvmVtlLoader; use igvm::IgvmFile; +use igvm::IgvmInitializationHeader; +use igvm::IgvmPlatformHeader; +use igvm::IgvmSerializer; +use igvm::corim::launch_measurement::LaunchMeasurement; +use igvm::corim::launch_measurement::MeasurementKind; use igvm_defs::IGVM_FIXED_HEADER; +use igvm_defs::IgvmPlatformType; use igvm_defs::SnpPolicy; use igvm_defs::TdxPolicy; use igvmfilegen_config::Config; @@ -57,7 +75,42 @@ enum Options { #[clap(short, long = "filepath")] file_path: PathBuf, }, - /// Build an IGVM file according to a manifest + /// Dump CoRIM (Concise Reference Integrity Manifest) headers and payloads from an IGVM file. + /// + /// This command scans the IGVM variable headers for CoRIM-related entries and prints or + /// extracts their contents. By default, all supported CoRIM headers for all platforms found + /// in the file are dumped. Use `--header-type` and `--platform` to narrow the selection. + /// + /// A human-readable summary of the selected CoRIM headers is written to stdout. When + /// `--output ` is provided, the CoRIM payloads are also extracted to files in + /// that directory and the file paths are reported. + DumpCorim { + /// Input IGVM file path to read CoRIM headers and payloads from. + #[clap(short, long = "filepath")] + file_path: PathBuf, + /// Filter by CoRIM header type (e.g. document or signature). If not specified, + /// all supported CoRIM header types in the IGVM file are included. + #[clap(long, value_enum)] + header_type: Option, + /// Filter by platform type for which the CoRIM applies (see `Platform` enum). + /// If not specified, CoRIM entries for all platforms present in the IGVM file + /// are considered. + #[clap(long, value_enum)] + platform: Option, + /// Output directory to extract CoRIM payload data. For each matching CoRIM header, + /// the payload is written to a file named `corim_{document,signature}_.`, + /// e.g. `corim_document_vbs.cbor` or `corim_signature_snp.cose`. + /// If omitted, payload contents are not written as files and are instead described + /// in the textual output on stdout. + #[clap(short, long)] + output: Option, + }, + /// Build an IGVM file according to a manifest. + /// + /// Also emits per-platform sibling files next to `--output`: + /// `-{snp,tdx,vbs}.json` (legacy identity documents) and + /// `-{snp,tdx,vbs}.cbor` (CoRIM launch endorsements) for every + /// measurable platform in the manifest. Manifest { /// Config manifest file path #[clap(short, long = "manifest")] @@ -76,6 +129,95 @@ enum Options { #[clap(long)] disable_secure_avic: bool, }, + /// Patch a CoRIM signature into an existing IGVM file for a given platform. + /// + /// The CoRIM document is generated automatically by `manifest` for every + /// measurable platform, so this command only attaches the detached + /// signature. Provide either a single bundled/signed CoRIM via + /// `--corim-bundle` (the tool splits it and uses the detached signature; + /// the IGVM file must already contain a matching CoRIM document) or an + /// already-detached signature via `--corim-signature` (the document slot + /// must already be populated in the IGVM file). + /// + /// What is verified: this command checks that the supplied signature + /// is a well-formed COSE_Sign1 envelope using PS384 (COSE alg -38, + /// RSA-PSS with SHA-384) and that the signature math validates against + /// the IGVM-embedded CoRIM document, using the public key carried in + /// the envelope's `x5chain` / `x5bag` header (RFC 9360). + /// + /// What is NOT verified: certificate-chain trust. The signing + /// certificate is taken from the envelope at face value -- no + /// validation against a trust root, no revocation check, no policy / + /// EKU enforcement. The caller is responsible for ensuring the input + /// signature originated from a trusted signer (e.g. by sourcing it + /// only from a controlled signing pipeline). Verification here exists + /// to catch accidental corruption and algorithm mismatches, not to + /// establish trust. + PatchCorimSignature { + /// Input IGVM file path + #[clap(short, long)] + input: PathBuf, + /// Output IGVM file path (can be the same as input to modify in place) + #[clap(short, long)] + output: PathBuf, + /// Path to a bundled/signed CoRIM file (COSE_Sign1 with embedded payload). + /// The tool will internally split it and use the detached signature; + /// the IGVM file must already contain a matching CoRIM document. + /// Mutually exclusive with `--corim-signature`. + /// + /// Only PS384 (COSE alg -38, RSA-PSS with SHA-384) signatures are + /// accepted; other algorithms are rejected at verify time. + #[clap( + long, + conflicts_with = "corim_signature", + required_unless_present = "corim_signature" + )] + corim_bundle: Option, + /// Path to the CoRIM signature (COSE_Sign1 with nil payload) file. + /// Requires that a corresponding document already exists in the + /// file for the same compatibility mask. + /// + /// Only PS384 (COSE alg -38, RSA-PSS with SHA-384) signatures are + /// accepted; other algorithms are rejected at verify time. + #[clap(long)] + corim_signature: Option, + /// Platform type for the CoRIM headers + #[clap(long, value_enum)] + platform: Platform, + }, +} + +/// IGVM platform types for CLI selection. +/// +/// This is a CLI-friendly adapter for [`IgvmPlatformType`], which is an +/// `open_enum` and cannot derive clap's `ValueEnum` directly. +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +enum Platform { + /// AMD SEV-SNP + Snp, + /// Intel TDX + Tdx, + /// VBS (Virtualization Based Security) + Vbs, +} + +impl From for IgvmPlatformType { + fn from(platform: Platform) -> Self { + match platform { + Platform::Snp => IgvmPlatformType::SEV_SNP, + Platform::Tdx => IgvmPlatformType::TDX, + Platform::Vbs => IgvmPlatformType::VSM_ISOLATION, + } + } +} + +/// CoRIM header types for filtering +#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] +enum CorimHeaderType { + /// CoRIM document header + Document, + /// CoRIM signature header + Signature, } // TODO: Potential CLI flags: @@ -107,6 +249,12 @@ fn main() -> anyhow::Result<()> { println!("{}", igvm_data); Ok(()) } + Options::DumpCorim { + file_path, + header_type, + platform, + output, + } => dump_corim_headers(&file_path, header_type, platform, output), Options::Manifest { manifest, resources, @@ -164,9 +312,111 @@ fn main() -> anyhow::Result<()> { ), } } + Options::PatchCorimSignature { + input, + output, + corim_bundle, + corim_signature, + platform, + } => patch_corim_signature(input, output, corim_bundle, corim_signature, platform), + } +} + +/// Per-config measurement metadata captured during the build loop and +/// consumed after merging to emit JSON identity documents and CoRIM +/// launch endorsements. +struct PlatformMeta { + platform: IgvmPlatformType, + svn: u32, + debug_enabled: bool, +} + +/// Build a sibling path of `output` named `-`, +/// where `base` is `output`'s stem, `` is derived from +/// `meta.platform`, and `ext` includes the leading dot +/// (e.g. `".json"` or `".cbor"`). +fn sibling_path( + base: &std::ffi::OsStr, + output: &std::path::Path, + meta: &PlatformMeta, + ext: &str, +) -> PathBuf { + let isolation = platform_mask::isolation_label(meta.platform); + let mut name = base.to_os_string(); + name.push("-"); + name.push(isolation); + name.push(ext); + output.with_file_name(name) +} + +/// Build a `LaunchMeasurement` template for the given platform's +/// launch measurement at the configured guest SVN. +fn build_endorsement_corim(meta: &PlatformMeta) -> anyhow::Result { + let mut le = LaunchMeasurement::for_platform(meta.platform) + .context("starting CoRIM launch endorsement")?; + le.set_measurement(MeasurementKind::Launch) + .context("setting CoRIM launch measurement kind")?; + le.endorse(meta.svn as u64) + .with(MeasurementKind::Launch) + .context("selecting CoRIM measurement in CES triple")? + .finish() + .context("finalizing CoRIM CES triple")?; + Ok(le) +} + +/// Build a JSON identity document for a platform measurement. +/// +/// The digest is produced by `IgvmSerializer::measurement_for(platform)` +/// which contractually returns 48 bytes for SNP/TDX and 32 bytes for +/// VBS; a length mismatch is an in-tree invariant violation and panics. +/// `platform` is restricted to the three measurable platforms by the +/// only call site (`create_igvm_file`'s `platform_metas` loop); any +/// other value is `unreachable!`. +fn build_endorsement_json( + platform: IgvmPlatformType, + digest: &[u8], + svn: u32, + debug_enabled: bool, +) -> Measurement { + match platform { + IgvmPlatformType::SEV_SNP => { + let ld: [u8; 48] = digest.try_into().expect("SNP launch digest is 48 bytes"); + Measurement::Snp(SnpMeasurement::new(ld, svn, debug_enabled)) + } + IgvmPlatformType::TDX => { + let mrtd: [u8; 48] = digest.try_into().expect("TDX MRTD is 48 bytes"); + Measurement::Tdx(TdxMeasurement::new(mrtd, svn, debug_enabled)) + } + IgvmPlatformType::VSM_ISOLATION => { + let boot_digest: [u8; 32] = digest.try_into().expect("VBS boot digest is 32 bytes"); + Measurement::Vbs(VbsMeasurement::new(boot_digest, svn, debug_enabled)) + } + other => { + unreachable!("build_endorsement_json called for non-measurable platform {other:?}") + } } } +/// Write a per-platform endorsement artifact (JSON identity document or +/// CoRIM document) to a sibling of `output` whose name is +/// `-`. `ext` must include the leading dot. +fn write_endorsement( + base: &std::ffi::OsStr, + output: &std::path::Path, + meta: &PlatformMeta, + ext: &str, + bytes: &[u8], +) -> anyhow::Result<()> { + let path = sibling_path(base, output, meta, ext); + tracing::info!( + path = %path.display(), + size = bytes.len(), + "Writing endorsement file", + ); + fs_err::write(&path, bytes).context("writing endorsement file")?; + Ok(()) +} + /// Create an IGVM file from the specified config fn create_igvm_file( igvm_config: Config, @@ -178,6 +428,7 @@ fn create_igvm_file( let mut igvm_file: Option = None; let mut map_files = Vec::new(); + let mut platform_metas: Vec = Vec::new(); let base_path = output.file_stem().unwrap(); for config in igvm_config.guest_configs { // Max VTL must be 2 or 0. @@ -185,12 +436,6 @@ fn create_igvm_file( bail!("max_vtl must be 2 or 0"); } - let isolation_string = match config.isolation_type { - ConfigIsolationType::None => "none", - ConfigIsolationType::Vbs { .. } => "vbs", - ConfigIsolationType::Snp { .. } => "snp", - ConfigIsolationType::Tdx { .. } => "tdx", - }; let loader_isolation_type = match config.isolation_type { ConfigIsolationType::None => LoaderIsolationType::None, ConfigIsolationType::Vbs { enable_debug } => LoaderIsolationType::Vbs { enable_debug }, @@ -224,6 +469,57 @@ fn create_igvm_file( }, }; + // Track measurement metadata for measurable platforms so the + // post-merge step can look up the digest from `IgvmSerializer` + // and emit a JSON identity document + CoRIM launch endorsement. + // + // Each measurable platform type may appear at most once across + // all guest configs: the post-merge step keys both the + // `IgvmSerializer::measurement_for(platform)` lookup and the + // `-.{cbor,json}` sibling filenames purely by + // platform type, so a duplicate would silently overwrite the + // earlier artifacts and could pair the wrong svn/debug bit with + // the merged measurement. Fail fast here with a clear error. + let platform = match &loader_isolation_type { + LoaderIsolationType::Snp { .. } => Some(IgvmPlatformType::SEV_SNP), + LoaderIsolationType::Tdx { .. } => Some(IgvmPlatformType::TDX), + LoaderIsolationType::Vbs { .. } => Some(IgvmPlatformType::VSM_ISOLATION), + LoaderIsolationType::None => None, + }; + if let Some(platform) = platform + && platform_metas.iter().any(|m| m.platform == platform) + { + bail!( + "manifest contains more than one guest config for measurable platform {platform:?}; \ + at most one is supported because endorsement artifacts and the post-merge \ + measurement lookup are keyed by platform type" + ); + } + match &loader_isolation_type { + LoaderIsolationType::Snp { policy, .. } => { + platform_metas.push(PlatformMeta { + platform: IgvmPlatformType::SEV_SNP, + svn: config.guest_svn, + debug_enabled: policy.debug() == 1, + }); + } + LoaderIsolationType::Tdx { policy } => { + platform_metas.push(PlatformMeta { + platform: IgvmPlatformType::TDX, + svn: config.guest_svn, + debug_enabled: policy.debug_allowed() == 1, + }); + } + LoaderIsolationType::Vbs { enable_debug } => { + platform_metas.push(PlatformMeta { + platform: IgvmPlatformType::VSM_ISOLATION, + svn: config.guest_svn, + debug_enabled: *enable_debug, + }); + } + LoaderIsolationType::None => {} + } + // Max VTL of 2 implies paravisor. let with_paravisor = config.max_vtl == 2; @@ -231,9 +527,7 @@ fn create_igvm_file( load_image(&mut loader.loader(), &config.image, &resources)?; - let igvm_output = loader - .finalize(config.guest_svn) - .context("finalizing loader")?; + let igvm_output = loader.finalize().context("finalizing loader")?; // Merge the loaded guest into the overall IGVM file. match &mut igvm_file { @@ -244,45 +538,71 @@ fn create_igvm_file( } map_files.push(igvm_output.map); + } - if let Some(doc) = igvm_output.doc { - // Write the measurement document to a file with the same name, - // but with -[isolation].json extension. - let doc_path = { - let mut name = base_path.to_os_string(); - name.push("-"); - name.push(isolation_string); - name.push(".json"); - output.with_file_name(name) - }; - tracing::info!( - path = %doc_path.display(), - "Writing document json file", - ); - let mut doc_file = fs_err::OpenOptions::new() - .create(true) - .write(true) - .open(doc_path) - .context("creating doc file")?; - - writeln!( - doc_file, - "{}", - serde_json::to_string(&doc).expect("json string") - ) - .context("writing doc file")?; - } + let Some(igvm_file) = igvm_file else { + bail!("manifest contained no guest configs"); + }; + + // Construct the serializer once on the merged IGVM file. This eagerly + // computes the launch measurement for every measurable platform and + // serves as the single source of truth for the digest used in both + // the JSON identity documents and the generated CoRIM documents. + let mut serializer = IgvmSerializer::new(&igvm_file).context("constructing IGVM serializer")?; + + // For each measurable platform: log the diagnostic, attach a CoRIM + // launch endorsement, then write both sibling files. The CoRIM is + // attached before any sibling file is written so a failure in + // `add_corim` leaves no half-written artifact set on disk. + for meta in &platform_metas { + // Snapshot the digest so the immutable borrow on `serializer` + // ends before the `&mut serializer` call to `add_corim` below. + let (digest, compatibility_mask) = { + let m = serializer.measurement_for(meta.platform).with_context(|| { + format!("no measurement computed for platform {:?}", meta.platform) + })?; + (m.digest.clone(), m.compatibility_mask) + }; + + // Emit the platform-specific launch-measurement diagnostic + // structure (VBS signed data, SNP ID block, TDX MRTD) for human + // inspection. The digest itself does not depend on these inputs. + log_measurement_diagnostic( + meta.platform, + &digest, + meta.svn, + meta.debug_enabled, + serializer.file(), + compatibility_mask, + ); + + let corim = build_endorsement_corim(meta)?; + let corim_bytes = serializer + .add_corim(meta.platform, corim.build()) + .context("adding CoRIM document to IGVM serializer")? + .to_vec(); + + // Write the CoRIM document first since it is the new artifact + // downstream signing tooling keys on; the JSON identity document + // is the legacy companion. + write_endorsement(base_path, &output, meta, ".cbor", &corim_bytes)?; + + let json = build_endorsement_json(meta.platform, &digest, meta.svn, meta.debug_enabled); + let mut json_bytes = + serde_json::to_vec(&json).expect("serializing measurement JSON cannot fail"); + json_bytes.push(b'\n'); + write_endorsement(base_path, &output, meta, ".json", &json_bytes)?; } let mut igvm_binary = Vec::new(); - let igvm_file = igvm_file.expect("should have an igvm file"); - igvm_file + serializer .serialize(&mut igvm_binary) .context("serializing igvm")?; - // If enabled, perform additional validation. + // If enabled, perform additional validation by round-tripping the + // serialized binary through the parser and re-serializer. if debug_validation { - debug_validate_igvm_file(&igvm_file, &igvm_binary); + debug_validate_igvm_file(&igvm_binary); } // Write the IGVM file to the specified file path in the config. @@ -306,11 +626,7 @@ fn create_igvm_file( path = %map_path.display(), "Writing output map file", ); - let mut map_file = fs_err::OpenOptions::new() - .create(true) - .write(true) - .open(map_path) - .context("creating map file")?; + let mut map_file = fs_err::File::create(map_path).context("creating map file")?; for map in map_files { writeln!(map_file, "{}", map).context("writing map file")?; @@ -319,13 +635,242 @@ fn create_igvm_file( Ok(()) } -/// Validate an in-memory IGVM file and the binary repr are equivalent. +/// Dump CoRIM headers from an IGVM file. +fn dump_corim_headers( + file_path: &std::path::Path, + header_type_filter: Option, + platform_filter: Option, + output_dir: Option, +) -> anyhow::Result<()> { + let image = fs_err::read(file_path).context("reading input file")?; + + // Parse the IGVM file using the igvm crate's structured API. + let igvm_file = IgvmFile::new_from_binary(&image, None).context("parsing IGVM file")?; + + let fixed_header = IGVM_FIXED_HEADER::read_from_prefix(image.as_slice()) + .map_err(|_| anyhow::anyhow!("Invalid IGVM file: cannot read fixed header"))? + .0; // TODO: zerocopy: use-rest-of-range (https://github.com/microsoft/openvmm/issues/759) + + println!("IGVM File: {}", file_path.display()); + println!("Total file size: {} bytes", fixed_header.total_file_size); + println!(); + + // The output directory is created lazily on the first matching header + // so a filter that matches nothing doesn't leave an empty directory + // behind. + let mut output_dir_created = false; + + let platforms = igvm_file.platforms(); + + // Print the supported platform table + if !platforms.is_empty() { + println!("Supported Platforms:"); + for header in platforms { + match header { + IgvmPlatformHeader::SupportedPlatform(info) => { + println!( + " {:?} -> compatibility_mask 0x{:X}", + info.platform_type, info.compatibility_mask + ); + } + } + } + println!(); + } + + // Convert platform filter to compatibility mask using the file's actual mapping + let platform_mask_filter = platform_filter + .map(|p| platform_mask::lookup_compatibility_mask(platforms, IgvmPlatformType::from(p))) + .transpose()?; + + // Iterate through initialization headers looking for CoRIM entries + let mut document_count: usize = 0; + let mut signature_count: usize = 0; + + for header in igvm_file.initializations() { + let (kind, label, extension, compatibility_mask, payload) = match header { + IgvmInitializationHeader::CorimDocument { + compatibility_mask, + document, + } => ( + CorimHeaderType::Document, + "Document", + "cbor", + *compatibility_mask, + document.as_slice(), + ), + IgvmInitializationHeader::CorimSignature { + compatibility_mask, + signature, + } => ( + CorimHeaderType::Signature, + "Signature", + "cose", + *compatibility_mask, + signature.as_slice(), + ), + _ => continue, + }; + + let show_type = header_type_filter.is_none_or(|t| t == kind); + let show_platform = platform_mask_filter.is_none_or(|mask| compatibility_mask & mask != 0); + + if !show_type || !show_platform { + continue; + } + + match kind { + CorimHeaderType::Document => document_count += 1, + CorimHeaderType::Signature => signature_count += 1, + } + + let platform_name = platform_mask::platform_name_for_mask(platforms, compatibility_mask); + + println!("CoRIM {label} ({platform_name}):"); + println!( + " Compatibility Mask: 0x{compatibility_mask:X} ({})", + platform_mask::format_platform_mask(platforms, compatibility_mask) + ); + println!(" Size: {} bytes", payload.len()); + + if let Some(ref dir) = output_dir { + if !output_dir_created { + fs_err::create_dir_all(dir).context("creating output directory")?; + output_dir_created = true; + } + let file_prefix = label.to_lowercase(); + let output_file = dir.join(format!("corim_{file_prefix}_{platform_name}.{extension}")); + fs_err::write(&output_file, payload) + .with_context(|| format!("writing {label} payload to {}", output_file.display()))?; + println!(" Output: {}", output_file.display()); + } + println!(); + } + + if document_count == 0 && signature_count == 0 { + println!("No CoRIM headers found matching the specified filters."); + } else { + println!( + "Summary: {} document header(s), {} signature header(s)", + document_count, signature_count + ); + } + + Ok(()) +} + +/// Patch a CoRIM signature into an existing IGVM file. +fn patch_corim_signature( + input: PathBuf, + output: PathBuf, + corim_bundle: Option, + corim_signature: Option, + platform: Platform, +) -> anyhow::Result<()> { + let igvm_data = fs_err::read(&input) + .with_context(|| format!("reading input IGVM file at {}", input.display()))?; + + let platform_type = IgvmPlatformType::from(platform); + + // The CoRIM document is expected to already be embedded in the IGVM file + // for the target platform (auto-generated at build time). `corim_signature::patch` + // looks it up internally and verifies `signature_data` against it before + // mutating the file. The issuer certificate is carried in the + // signature's COSE protected header (x5chain / x5bag, RFC 9360); no + // separate cert input needed. + // + // When the user supplies a `--corim-bundle`, that bundle's embedded + // payload is the document the signature was actually produced over; + // we forward it so `patch` can sanity-check it against the + // IGVM-embedded document and surface a targeted mismatch error + // instead of an opaque cryptographic verify failure. + let (signature_data, bundle_document) = if let Some(bundle_path) = &corim_bundle { + let bundle_data = fs_err::read(bundle_path) + .with_context(|| format!("reading bundled CoRIM file at {}", bundle_path.display()))?; + + let detached = detach_payload(&bundle_data).context("splitting bundled CoRIM")?; + tracing::info!( + path = %bundle_path.display(), + bundle_size = bundle_data.len(), + document_size = detached.document.len(), + signature_size = detached.signature.len(), + "Split bundled signed CoRIM into document and detached signature" + ); + (detached.signature, Some(detached.document)) + } else { + let path = corim_signature.as_ref().expect( + "caller ensures at least one of --corim-bundle or --corim-signature is provided", + ); + let sig = fs_err::read(path) + .with_context(|| format!("reading CoRIM signature file at {}", path.display()))?; + (sig, None) + }; + + tracing::info!( + input = %input.display(), + output = %output.display(), + bundle = ?corim_bundle, + signature = ?corim_signature, + platform = ?platform, + "Patching CoRIM signature into IGVM file" + ); + + let patched_igvm = corim_signature::patch( + &igvm_data, + &signature_data, + platform_type, + bundle_document.as_deref(), + )?; + + // Write output file atomically: write to a temporary file in the same + // directory, then rename into place. This prevents a crash or + // interruption during the write from corrupting the output (which may + // be the same file as the input for in-place edits). Same-volume + // renames are atomic on both POSIX and Windows (`std::fs::rename` + // uses `MoveFileExW` with `MOVEFILE_REPLACE_EXISTING`). + let temp_path = { + // Sibling temp file; not `with_extension`, which would replace any existing ext. + let mut s = output.as_os_str().to_owned(); + s.push(".tmp"); + PathBuf::from(s) + }; + + tracing::info!( + path = %output.display(), + size = patched_igvm.len(), + "Writing patched IGVM file" + ); + fs_err::write(&temp_path, &patched_igvm) + .with_context(|| format!("writing temporary IGVM file at {}", temp_path.display()))?; + + fs_err::rename(&temp_path, &output).with_context(|| { + format!( + "renaming temporary file {} to {}", + temp_path.display(), + output.display() + ) + })?; + + Ok(()) +} + +/// Validate that the serialized IGVM file round-trips through the parser +/// and re-serializer producing identical structural headers. // TODO: should live in the igvm crate -fn debug_validate_igvm_file(igvm_file: &IgvmFile, binary_file: &[u8]) { +fn debug_validate_igvm_file(binary_file: &[u8]) { use igvm::IgvmDirectiveHeader; tracing::info!("Debug validation of serialized IGVM file."); - let igvm_reserialized = IgvmFile::new_from_binary(binary_file, None).expect("should be valid"); + let igvm_file = + IgvmFile::new_from_binary(binary_file, None).expect("first parse should succeed"); + + let mut reserialized = Vec::new(); + igvm_file + .serialize(&mut reserialized) + .expect("re-serialize should succeed"); + + let igvm_reserialized = + IgvmFile::new_from_binary(&reserialized, None).expect("re-parse should succeed"); for (a, b) in igvm_file .platforms() diff --git a/vm/loader/igvmfilegen/src/measurement_diag.rs b/vm/loader/igvmfilegen/src/measurement_diag.rs new file mode 100644 index 0000000000..8d7182e41f --- /dev/null +++ b/vm/loader/igvmfilegen/src/measurement_diag.rs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Per-platform launch measurement diagnostics. +//! +//! Builds the human-readable launch-measurement structures that downstream +//! signing/attestation tooling expects (`VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA` +//! for VBS, `SnpPspIdBlock` for SEV-SNP, MRTD for TDX) and emits them via +//! `tracing` so they are visible in `igvmfilegen` output. The `igvm` crate +//! itself only computes the raw digest; the diagnostic dressing (svn, +//! debug bit, SNP family/image identifiers, ...) is OpenHCL-specific and +//! lives here. + +use bitfield_struct::bitfield; +use igvm::IgvmFile; +use igvm::IgvmInitializationHeader; +use igvm_defs::IgvmPlatformType; +use igvm_defs::VbsDigestAlgorithm; +use igvm_defs::VbsSigningAlgorithm; +use x86defs::snp::SnpPspIdBlock; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +/// Hard-coded SNP family identifier historically used by `igvmfilegen`. +const SNP_FAMILY_ID: [u8; 16] = *b"msft\0\0\0\0\0\0\0\0\0\0\0\0"; +/// Hard-coded SNP image identifier historically used by `igvmfilegen`. +const SNP_IMAGE_ID: [u8; 16] = *b"underhill\0\0\0\0\0\0\0"; + +// Name follows the Windows VBS C struct convention; `#[repr(C)]` already +// silences the `non_camel_case_types` lint so no explicit allow is needed. +#[repr(C)] +#[derive(IntoBytes, Immutable, KnownLayout, Debug)] +struct VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA { + version: u32, + product_id: u32, + module_id: u32, + security_version: u32, + security_policy: VBS_POLICY_FLAGS, + boot_digest_algo: u32, + signing_algo: u32, + boot_measurement_digest: [u8; 32], +} + +/// Flags defining the security policy for the guest. +#[bitfield(u32)] +#[derive(IntoBytes, Immutable, KnownLayout)] +#[expect(non_camel_case_types)] +struct VBS_POLICY_FLAGS { + /// Guest supports debugging + #[bits(1)] + debug: bool, + #[bits(31)] + reserved: u32, +} + +/// Emit a `tracing` log of the platform-specific launch-measurement +/// diagnostic structure for human inspection. +pub fn log_measurement_diagnostic( + platform: IgvmPlatformType, + digest: &[u8], + svn: u32, + enable_debug: bool, + file: &IgvmFile, + compatibility_mask: u32, +) { + match platform { + IgvmPlatformType::VSM_ISOLATION => log_vbs(digest, svn, enable_debug), + IgvmPlatformType::SEV_SNP => log_snp(digest, svn, file, compatibility_mask), + IgvmPlatformType::TDX => log_tdx(digest), + _ => {} + } +} + +fn log_vbs(digest: &[u8], svn: u32, enable_debug: bool) { + const MSFT_PRODUCT_ID: u32 = u32::from_le_bytes(*b"msft"); + const VBS_MODULE_ID: u32 = u32::from_le_bytes(*b"vbs\0"); + const VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT: u32 = 0x1; + + // The digest comes from `IgvmSerializer::measurement_for(VSM_ISOLATION)` + // which contractually returns a 32-byte SHA-256. A length mismatch + // would indicate a broken in-tree invariant. + let boot_measurement_digest = + <[u8; 32]>::try_from(digest).expect("VBS launch digest is 32 bytes"); + + let boot_measurement = VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA { + version: VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT, + product_id: MSFT_PRODUCT_ID, + module_id: VBS_MODULE_ID, + security_version: svn, + security_policy: VBS_POLICY_FLAGS::new().with_debug(enable_debug), + boot_digest_algo: VbsDigestAlgorithm::SHA256.0, + signing_algo: VbsSigningAlgorithm::ECDSA_P384.0, + boot_measurement_digest, + }; + tracing::info!("Boot Measurement {:x?}", boot_measurement); +} + +fn log_snp(digest: &[u8], svn: u32, file: &IgvmFile, compatibility_mask: u32) { + // The digest comes from `IgvmSerializer::measurement_for(SEV_SNP)` + // which contractually returns a 48-byte SHA-384. A length mismatch + // would indicate a broken in-tree invariant. + let ld = <[u8; 48]>::try_from(digest).expect("SNP launch digest is 48 bytes"); + + let policy = file + .initializations() + .iter() + .find_map(|h| match h { + IgvmInitializationHeader::GuestPolicy { + policy, + compatibility_mask: mask, + } if mask & compatibility_mask == compatibility_mask => Some(*policy), + _ => None, + }) + .unwrap_or_else(|| { + tracing::error!( + compatibility_mask = format_args!("0x{compatibility_mask:X}"), + "Missing SNP GuestPolicy initialization header; reporting policy as 0" + ); + 0 + }); + + let psp_id_block = SnpPspIdBlock { + ld, + family_id: SNP_FAMILY_ID, + image_id: SNP_IMAGE_ID, + version: 0x1, + guest_svn: svn, + policy, + }; + tracing::info!("SNP ID Block {:x?}", psp_id_block); +} + +fn log_tdx(digest: &[u8]) { + tracing::info!("MRTD: {}", hex::encode_upper(digest)); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Pin the SNP ID block constants to their exact byte values. These + /// values are baked into externally-issued SNP ID block envelopes, + /// so any change here would silently break attestation flows; this + /// test forces such a change to be a deliberate, reviewed edit. + #[test] + fn snp_id_block_constants_byte_identity() { + assert_eq!(SNP_FAMILY_ID, *b"msft\0\0\0\0\0\0\0\0\0\0\0\0"); + assert_eq!(SNP_IMAGE_ID, *b"underhill\0\0\0\0\0\0\0"); + assert_eq!(SNP_FAMILY_ID.len(), 16); + assert_eq!(SNP_IMAGE_ID.len(), 16); + } +} diff --git a/vm/loader/igvmfilegen/src/platform_mask.rs b/vm/loader/igvmfilegen/src/platform_mask.rs new file mode 100644 index 0000000000..f71dd21979 --- /dev/null +++ b/vm/loader/igvmfilegen/src/platform_mask.rs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Helpers for resolving `IgvmPlatformType` <-> compatibility-mask +//! mappings declared in an IGVM file's platform headers, and for +//! deriving human-readable / file-name-safe short names for each +//! supported platform. + +use igvm::IgvmPlatformHeader; +use igvm_defs::IgvmPlatformType; + +/// Short lowercase name for a measurable platform, suitable for file +/// names and CLI suffixes (e.g. `"snp"`, `"tdx"`, `"vbs"`). +/// +/// Returns `None` for platform variants that don't have a canonical +/// short name in this tool (e.g. `Native`). +pub fn isolation_short_name(platform: IgvmPlatformType) -> Option<&'static str> { + match platform { + IgvmPlatformType::SEV_SNP => Some("snp"), + IgvmPlatformType::TDX => Some("tdx"), + IgvmPlatformType::VSM_ISOLATION => Some("vbs"), + _ => None, + } +} + +/// Always-safe label for a platform, used to generate sibling file +/// names and human-readable platform tags. Falls back to +/// `platform_` for variants without a canonical short name, +/// ensuring two distinct platforms never collide on the same label. +pub fn isolation_label(platform: IgvmPlatformType) -> String { + match isolation_short_name(platform) { + Some(name) => name.to_string(), + None => format!("platform_{platform:?}"), + } +} + +/// Look up the compatibility mask for a given platform type by reading the +/// platform headers from the IGVM file. +/// +/// Each IGVM file declares its own platform-to-mask mapping via +/// `IGVM_VHS_SUPPORTED_PLATFORM` headers. +/// +/// Returns an error if the requested platform type is not present in the +/// file's platform headers. +pub fn lookup_compatibility_mask( + platforms: &[IgvmPlatformHeader], + platform: IgvmPlatformType, +) -> anyhow::Result { + for header in platforms { + match header { + IgvmPlatformHeader::SupportedPlatform(info) => { + if info.platform_type == platform { + return Ok(info.compatibility_mask); + } + } + } + } + + anyhow::bail!( + "Platform type {platform:?} not found in IGVM file platform headers. \ + Available platforms: {}", + platforms + .iter() + .map(|h| match h { + IgvmPlatformHeader::SupportedPlatform(info) => { + format!( + "{:?} (mask=0x{:X})", + info.platform_type, info.compatibility_mask + ) + } + }) + .collect::>() + .join(", ") + ) +} + +/// Format a compatibility mask as a human-readable platform list using +/// the platform headers from the IGVM file. +pub fn format_platform_mask(platforms: &[IgvmPlatformHeader], mask: u32) -> String { + let mut names = Vec::new(); + for header in platforms { + match header { + IgvmPlatformHeader::SupportedPlatform(info) => { + if mask & info.compatibility_mask != 0 { + names.push(format!("{:?}", info.platform_type)); + } + } + } + } + if names.is_empty() { + "Unknown".to_string() + } else { + names.join(", ") + } +} + +/// Map a compatibility mask to a short lowercase platform name suitable for +/// use in file names. Returns e.g. `"vbs"`, `"snp"`, `"tdx"`, or +/// `"mask_0x3"` if the mask doesn't match a known platform. +pub fn platform_name_for_mask(platforms: &[IgvmPlatformHeader], mask: u32) -> String { + for header in platforms { + match header { + IgvmPlatformHeader::SupportedPlatform(info) => { + if info.compatibility_mask == mask { + return isolation_label(info.platform_type); + } + } + } + } + format!("mask_0x{mask:X}") +} diff --git a/vm/loader/igvmfilegen/src/signed_measurement/mod.rs b/vm/loader/igvmfilegen/src/signed_measurement/mod.rs deleted file mode 100644 index 987044f1fa..0000000000 --- a/vm/loader/igvmfilegen/src/signed_measurement/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Creates a digest for supported isolation types which can be signed externally. - -pub mod snp; -pub mod tdx; -pub mod vbs; - -pub use snp::generate_snp_measurement; -pub use tdx::generate_tdx_measurement; -pub use vbs::generate_vbs_measurement; - -const SHA_256_OUTPUT_SIZE_BYTES: usize = 32; -const SHA_384_OUTPUT_SIZE_BYTES: usize = 48; diff --git a/vm/loader/igvmfilegen/src/signed_measurement/snp.rs b/vm/loader/igvmfilegen/src/signed_measurement/snp.rs deleted file mode 100644 index cbaeb716bc..0000000000 --- a/vm/loader/igvmfilegen/src/signed_measurement/snp.rs +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Support for creating SNP ID blocks - -use super::SHA_384_OUTPUT_SIZE_BYTES; -use crate::file_loader::DEFAULT_COMPATIBILITY_MASK; -use igvm::IgvmDirectiveHeader; -use igvm::IgvmInitializationHeader; -use igvm_defs::IgvmPageDataType; -use igvm_defs::PAGE_SIZE_4K; -use sha2::Digest; -use sha2::Sha384; -use std::collections::HashMap; -use thiserror::Error; -use x86defs::snp::SnpPageInfo; -use x86defs::snp::SnpPageType; -use x86defs::snp::SnpPspIdBlock; -use zerocopy::IntoBytes; - -#[derive(Debug, Error)] -pub enum Error { - #[error("invalid parameter area index")] - InvalidParameterAreaIndex, -} - -/// Iterate through all headers, creating a launch digest which is then signed, -/// returning an [`IgvmDirectiveHeader::SnpIdBlock`] -pub fn generate_snp_measurement( - initialization_headers: &[IgvmInitializationHeader], - directive_headers: &[IgvmDirectiveHeader], - svn: u32, -) -> Result<[u8; SHA_384_OUTPUT_SIZE_BYTES], Error> { - let mut parameter_area_table = HashMap::new(); - const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize; - let snp_compatibility_mask = DEFAULT_COMPATIBILITY_MASK; - - let mut launch_digest: [u8; SHA_384_OUTPUT_SIZE_BYTES] = [0; SHA_384_OUTPUT_SIZE_BYTES]; - let zero_page: [u8; PAGE_SIZE_4K as usize] = [0; PAGE_SIZE_4K as usize]; - let mut hasher = Sha384::new(); - - // Hash the contents of empty 4K page, used when file does not carry data - hasher.update(zero_page.as_bytes()); - let zero_digest = hasher.finalize(); - - // Reuse the same vec for padding out data to 4k. - let mut padding_vec = vec![0; PAGE_SIZE_4K_USIZE]; - - let mut measure_page = |page_type: SnpPageType, gpa: u64, page_data: Option<&[u8]>| { - let mut hash = Sha384::new(); - let hash_contents = match page_data { - Some(data) => { - match data.len() { - 0 => zero_digest, - _ if data.len() < PAGE_SIZE_4K_USIZE => { - padding_vec.fill(0); - padding_vec[..data.len()].copy_from_slice(data); - hash.update(&padding_vec); - hash.finalize() - } - PAGE_SIZE_4K_USIZE => { - hash.update(data); - hash.finalize() - } - _ => { - // TODO SNP: Need to check the PSP spec how to measure 2MB - // pages. Fail for now, as they shouldn't exist. - todo!( - "unable to measure greater than 4k pages, len: {}", - data.len() - ) - } - } - } - None => [0; SHA_384_OUTPUT_SIZE_BYTES].into(), - }; - - let info = SnpPageInfo { - digest_current: launch_digest, - contents: hash_contents.into(), - length: size_of::() as u16, - page_type, - imi_page_bit: 0, - lower_vmpl_permissions: 0, - gpa, - }; - - let mut hash = Sha384::new(); - hash.update(info.as_bytes()); - launch_digest = hash.finalize().into(); - }; - - let mut policy: u64 = 0; - - for header in initialization_headers { - if let IgvmInitializationHeader::GuestPolicy { - policy: snp_policy, - compatibility_mask, - } = header - { - assert_eq!( - compatibility_mask & snp_compatibility_mask, - snp_compatibility_mask - ); - policy = *snp_policy; - } - } - assert_ne!(policy, 0); - - // Loop over all the page data to build the digest - for header in directive_headers { - // Skip headers that have compatibility masks that do not match snp. - if header - .compatibility_mask() - .map(|mask| mask & snp_compatibility_mask != snp_compatibility_mask) - .unwrap_or(false) - { - continue; - } - - match header { - IgvmDirectiveHeader::ErrorRange { .. } => todo!("error range not implemented"), - IgvmDirectiveHeader::ParameterArea { - number_of_bytes, - parameter_area_index, - initial_data: _, - } => { - assert_eq!( - parameter_area_table.contains_key(¶meter_area_index), - false - ); - assert_eq!(number_of_bytes % PAGE_SIZE_4K, 0); - parameter_area_table.insert(parameter_area_index, number_of_bytes); - } - IgvmDirectiveHeader::PageData { - gpa, - compatibility_mask, - flags, - data_type, - data, - } => { - assert_eq!( - compatibility_mask & snp_compatibility_mask, - snp_compatibility_mask - ); - - // Skip shared pages. - if flags.shared() { - continue; - } - - let (page_type, data) = match *data_type { - IgvmPageDataType::SECRETS => (SnpPageType::SECRETS, None), - IgvmPageDataType::CPUID_DATA | IgvmPageDataType::CPUID_XF => { - (SnpPageType::CPUID, None) - } - _ => { - if flags.unmeasured() { - (SnpPageType::UNMEASURED, None) - } else { - (SnpPageType::NORMAL, Some(data.as_bytes())) - } - } - }; - - measure_page(page_type, *gpa, data); - } - IgvmDirectiveHeader::ParameterInsert(param) => { - assert_eq!( - param.compatibility_mask & snp_compatibility_mask, - snp_compatibility_mask - ); - - let parameter_area_size = parameter_area_table - .get(¶m.parameter_area_index) - .ok_or(Error::InvalidParameterAreaIndex)?; - - for gpa in (param.gpa..param.gpa + *parameter_area_size).step_by(PAGE_SIZE_4K_USIZE) - { - measure_page(SnpPageType::UNMEASURED, gpa, None) - } - } - IgvmDirectiveHeader::SnpVpContext { - gpa, - compatibility_mask, - vp_index: _, - vmsa, - } => { - assert_eq!( - compatibility_mask & snp_compatibility_mask, - snp_compatibility_mask - ); - - let vmsa_bytes = vmsa.as_ref().as_bytes(); - measure_page(SnpPageType::VMSA, *gpa, Some(vmsa_bytes)); - } - _ => {} - } - } - - let family_id = *b"msft\0\0\0\0\0\0\0\0\0\0\0\0"; - let image_id = *b"underhill\0\0\0\0\0\0\0"; - - // Generate the PSP ID block format, hash with SHA-384 - let psp_id_block = SnpPspIdBlock { - ld: launch_digest, - version: 0x1, - guest_svn: svn, - policy, - family_id, - image_id, - }; - // Print the ID block for reference, not currently used. - tracing::info!("SNP ID Block {:x?}", psp_id_block); - Ok(psp_id_block.ld) -} diff --git a/vm/loader/igvmfilegen/src/signed_measurement/tdx.rs b/vm/loader/igvmfilegen/src/signed_measurement/tdx.rs deleted file mode 100644 index 5702d1c6b1..0000000000 --- a/vm/loader/igvmfilegen/src/signed_measurement/tdx.rs +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Support for creating TDX MRTD - -use super::SHA_384_OUTPUT_SIZE_BYTES; -use crate::file_loader::DEFAULT_COMPATIBILITY_MASK; -use igvm::IgvmDirectiveHeader; -use igvm_defs::PAGE_SIZE_4K; -use sha2::Digest; -use sha2::Sha384; -use std::collections::HashMap; -use thiserror::Error; -use zerocopy::FromBytes; -use zerocopy::Immutable; -use zerocopy::IntoBytes; -use zerocopy::KnownLayout; - -#[derive(Debug, Error)] -pub enum Error { - #[error("invalid parameter area index")] - InvalidParameterAreaIndex, -} - -/// Measure adding a page to TD. -#[repr(C)] -#[derive(Debug, Clone, Copy, IntoBytes, Immutable, KnownLayout, FromBytes)] -pub struct TdxPageAdd { - /// MEM.PAGE.ADD - pub operation: [u8; 16], - /// Must be aligned to a page size boundary. - pub gpa: u64, - /// Reserved mbz. - pub mbz: [u8; 104], -} - -const TDX_EXTEND_CHUNK_SIZE: usize = 256; - -/// Measure adding a chunk of data to TD. -#[repr(C)] -#[derive(Debug, Clone, Copy, IntoBytes, Immutable, KnownLayout, FromBytes)] -pub struct TdxMrExtend { - /// MR.EXTEND - pub operation: [u8; 16], - /// Aligned to a 256B boundary. - pub gpa: u64, - /// Reserved mbz. - pub mbz: [u8; 104], - /// Data to measure. - pub data: [u8; TDX_EXTEND_CHUNK_SIZE], -} - -/// Iterate through all headers to create the MRTD. -pub fn generate_tdx_measurement( - directive_headers: &[IgvmDirectiveHeader], -) -> Result<[u8; SHA_384_OUTPUT_SIZE_BYTES], Error> { - let mut parameter_area_table = HashMap::new(); - const PAGE_SIZE_4K_USIZE: usize = PAGE_SIZE_4K as usize; - let tdx_compatibility_mask = DEFAULT_COMPATIBILITY_MASK; - // Reuse the same vec for padding out data to 4k. - let mut padding_vec = vec![0; PAGE_SIZE_4K_USIZE]; - let mut hasher = Sha384::new(); - - let mut measure_page = |gpa: u64, page_data: Option<&[u8]>| { - // Measure the page being added. - let page_add = TdxPageAdd { - operation: *b"MEM.PAGE.ADD\0\0\0\0", - gpa, - mbz: [0; 104], - }; - hasher.update(page_add.as_bytes()); - - // Possibly measure the page contents in chunks. - if let Some(data) = page_data { - let data = match data.len() { - 0 => None, - PAGE_SIZE_4K_USIZE => Some(data), - _ if data.len() < PAGE_SIZE_4K_USIZE => { - padding_vec.fill(0); - padding_vec[..data.len()].copy_from_slice(data); - Some(padding_vec.as_slice()) - } - _ => { - panic!("Unexpected data size"); - } - }; - - // Hash the contents of the 4K page, 256 bytes at a time. - for offset in (0..PAGE_SIZE_4K).step_by(TDX_EXTEND_CHUNK_SIZE) { - let mut mr_extend = TdxMrExtend { - operation: *b"MR.EXTEND\0\0\0\0\0\0\0", - gpa: gpa + offset, - mbz: [0; 104], - data: [0; TDX_EXTEND_CHUNK_SIZE], - }; - - // Copy in data for chunk if it exists. - if let Some(data) = data { - mr_extend.data.copy_from_slice( - &data[offset as usize..offset as usize + TDX_EXTEND_CHUNK_SIZE], - ); - } - hasher.update(mr_extend.as_bytes()); - } - }; - }; - - // Loop over all the page data to build the digest - for header in directive_headers { - // Skip headers that have compatibility masks that do not match TDX. - if header - .compatibility_mask() - .map(|mask| mask & tdx_compatibility_mask != tdx_compatibility_mask) - .unwrap_or(false) - { - continue; - } - - match header { - IgvmDirectiveHeader::ParameterArea { - number_of_bytes, - parameter_area_index, - initial_data: _, - } => { - assert_eq!( - parameter_area_table.contains_key(¶meter_area_index), - false - ); - assert_eq!(number_of_bytes % PAGE_SIZE_4K, 0); - parameter_area_table.insert(parameter_area_index, number_of_bytes); - } - IgvmDirectiveHeader::PageData { - gpa, - compatibility_mask, - flags, - data_type: _, - data, - } => { - assert_eq!( - compatibility_mask & tdx_compatibility_mask, - tdx_compatibility_mask - ); - - // Skip shared pages. - if flags.shared() { - continue; - } - - // If data is unmeasured, only measure the GPA. - let data = if flags.unmeasured() { - None - } else { - Some(data.as_bytes()) - }; - - measure_page(*gpa, data); - } - IgvmDirectiveHeader::ParameterInsert(param) => { - assert_eq!( - param.compatibility_mask & tdx_compatibility_mask, - tdx_compatibility_mask - ); - - let parameter_area_size = parameter_area_table - .get(¶m.parameter_area_index) - .ok_or(Error::InvalidParameterAreaIndex)?; - - for gpa in (param.gpa..param.gpa + *parameter_area_size).step_by(PAGE_SIZE_4K_USIZE) - { - measure_page(gpa, None); - } - } - _ => {} - } - } - - let mrtd: [u8; SHA_384_OUTPUT_SIZE_BYTES] = hasher.finalize().into(); - tracing::info!("MRTD: {}", hex::encode_upper(mrtd)); - Ok(mrtd) -} diff --git a/vm/loader/igvmfilegen/src/signed_measurement/vbs.rs b/vm/loader/igvmfilegen/src/signed_measurement/vbs.rs deleted file mode 100644 index 47184e697a..0000000000 --- a/vm/loader/igvmfilegen/src/signed_measurement/vbs.rs +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Support for VBS measurements - -use super::SHA_256_OUTPUT_SIZE_BYTES; -use crate::file_loader::DEFAULT_COMPATIBILITY_MASK; -use igvm::IgvmDirectiveHeader; -use igvm_defs::IgvmPageDataType; -use igvm_defs::PAGE_SIZE_4K; -use igvm_defs::VbsDigestAlgorithm; -use igvm_defs::VbsSigningAlgorithm; -use igvm_defs::VbsVpContextRegister; -use sha2::Digest; -use sha2::Sha256; -use std::collections::HashMap; -use thiserror::Error; -use vbs_defs::BootMeasurementType; -use vbs_defs::VBS_POLICY_FLAGS; -use vbs_defs::VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA; -use vbs_defs::VBS_VM_GPA_PAGE_BOOT_METADATA; -use vbs_defs::VBS_VP_CHUNK_SIZE_BYTES; -use vbs_defs::VM_GPA_PAGE_READABLE; -use vbs_defs::VM_GPA_PAGE_WRITABLE; -use vbs_defs::VbsChunkHeader; -use vbs_defs::VbsRegisterChunk; -use vbs_defs::VpGpaPageChunk; -use zerocopy::IntoBytes; - -#[derive(Debug, Error)] -pub enum Error { - #[error("invalid parameter area index")] - InvalidParameterAreaIndex, -} - -/// Iterate through all headers, creating a boot measurement which is then signed, -/// returning an [`IgvmDirectiveHeader::VbsMeasurement`] -pub fn generate_vbs_measurement( - directive_headers: &[IgvmDirectiveHeader], - enable_debug: bool, - svn: u32, -) -> Result<[u8; SHA_256_OUTPUT_SIZE_BYTES], Error> { - const VBS_COMPATIBILITY_MASK: u32 = DEFAULT_COMPATIBILITY_MASK; - - let mut digest = VbsDigestor::new()?; - let mut parameter_area_table = HashMap::new(); - let mut bsp_regs = Vec::new(); - - for header in directive_headers { - // Skip headers that have compatibility masks that do not match vbs. - if header - .compatibility_mask() - .map(|mask| mask & VBS_COMPATIBILITY_MASK != VBS_COMPATIBILITY_MASK) - .unwrap_or(false) - { - continue; - } - - match header { - IgvmDirectiveHeader::PageData { - gpa, - compatibility_mask, - flags, - data_type, - data, - } => { - assert_eq!( - compatibility_mask & VBS_COMPATIBILITY_MASK, - VBS_COMPATIBILITY_MASK - ); - - assert_eq!(*data_type, IgvmPageDataType::NORMAL); - - // Skip shared pages. - if flags.shared() { - continue; - } - - let boot_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new() - .with_acceptance(0) - .with_data_unmeasured(flags.unmeasured()); - digest.record_gpa_page(gpa / PAGE_SIZE_4K, 1, boot_metadata, data)?; - } - IgvmDirectiveHeader::ParameterInsert(param) => { - let page_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new() - .with_acceptance(0) - .with_data_unmeasured(true); - let parameter_area_size = parameter_area_table - .get(¶m.parameter_area_index) - .ok_or(Error::InvalidParameterAreaIndex)?; - digest.record_gpa_page( - param.gpa / PAGE_SIZE_4K, - parameter_area_size / PAGE_SIZE_4K, - page_metadata, - &[], - )?; - } - IgvmDirectiveHeader::X64VbsVpContext { - vtl, - registers, - compatibility_mask, - } => { - assert_eq!( - compatibility_mask & VBS_COMPATIBILITY_MASK, - VBS_COMPATIBILITY_MASK - ); - // The Vbs measurement format requires the cpu context to be measured last, measure at end - let vtl_registers: Vec = registers - .iter() - .map(|r| r.into_vbs_vp_context_reg(*vtl)) - .collect(); - bsp_regs.push(vtl_registers); - } - IgvmDirectiveHeader::AArch64VbsVpContext { - vtl, - registers, - compatibility_mask, - } => { - assert_eq!( - compatibility_mask & VBS_COMPATIBILITY_MASK, - VBS_COMPATIBILITY_MASK - ); - // The Vbs measurement format requires the cpu context to be measured last, measure at end - let vtl_registers: Vec = registers - .iter() - .map(|r| r.into_vbs_vp_context_reg(*vtl)) - .collect(); - bsp_regs.push(vtl_registers); - } - IgvmDirectiveHeader::ErrorRange { - gpa, - compatibility_mask, - size_bytes, - } => { - assert_eq!( - compatibility_mask & VBS_COMPATIBILITY_MASK, - VBS_COMPATIBILITY_MASK - ); - let page_metadata = VBS_VM_GPA_PAGE_BOOT_METADATA::new() - .with_acceptance(VM_GPA_PAGE_READABLE | VM_GPA_PAGE_WRITABLE) - .with_data_unmeasured(true); - digest.record_gpa_page( - *gpa / PAGE_SIZE_4K, - (*size_bytes as u64).div_ceil(PAGE_SIZE_4K), - page_metadata, - &[], - )?; - } - IgvmDirectiveHeader::ParameterArea { - number_of_bytes, - parameter_area_index, - initial_data: _, - } => { - if parameter_area_table.contains_key(parameter_area_index) { - return Err(Error::InvalidParameterAreaIndex); - } - parameter_area_table.insert(parameter_area_index, *number_of_bytes); - } - _ => {} - } - } - - // Measure all registers in each VTL as last step in measurement - for set in bsp_regs { - for reg in set { - digest.record_vp_register(reg)?; - } - } - - // Identifier constants chosen to maintain compatibility with internal tooling - const MSFT_PRODUCT_ID: u32 = u32::from_le_bytes(*b"msft"); - const VBS_MODULE_ID: u32 = u32::from_le_bytes(*b"vbs\0"); - const VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT: u32 = 0x1; - - let boot_measurement = VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA { - version: VBS_VM_BOOT_MEASUREMENT_VERSION_CURRENT, - product_id: MSFT_PRODUCT_ID, - module_id: VBS_MODULE_ID, - security_version: svn, - security_policy: VBS_POLICY_FLAGS::new().with_debug(enable_debug), - boot_digest_algo: VbsDigestAlgorithm::SHA256.0, - signing_algo: VbsSigningAlgorithm::ECDSA_P384.0, - boot_measurement_digest: digest.finish_digest(), - }; - // Print the signing data for reference, not currently used. - tracing::info!("Boot Measurement {:x?}", boot_measurement); - Ok(boot_measurement.boot_measurement_digest) -} - -struct VbsDigestor { - digest: [u8; SHA_256_OUTPUT_SIZE_BYTES], -} - -impl VbsDigestor { - fn new() -> Result { - Ok(VbsDigestor { - digest: [0; SHA_256_OUTPUT_SIZE_BYTES], - }) - } - - fn record_gpa_page( - &mut self, - gpa_page_base: u64, - page_count: u64, - page_metadata: VBS_VM_GPA_PAGE_BOOT_METADATA, - mut data: &[u8], - ) -> Result<(), Error> { - for page in 0..page_count { - let import_data_len: usize = match page_metadata.data_unmeasured() { - true => 0, - false => std::cmp::min(PAGE_SIZE_4K as usize, data.len()), - }; - let (import_data, data_remaining) = data.split_at(import_data_len); - data = data_remaining; - - // If page is under 4K bytes, pad to full length which will be hashed with page and chunk data - let padding = vec![0; PAGE_SIZE_4K as usize - import_data.len()]; - let page_number = gpa_page_base + page; - let chunk = VpGpaPageChunk { - header: VbsChunkHeader { - byte_count: VBS_VP_CHUNK_SIZE_BYTES as u32, - chunk_type: BootMeasurementType::VP_GPA_PAGE, - reserved: 0, - }, - metadata: page_metadata.into(), - page_number, - }; - self.create_record_entry(&[chunk.as_bytes(), import_data, &padding])?; - } - Ok(()) - } - - fn record_vp_register(&mut self, reg: VbsVpContextRegister) -> Result<(), Error> { - let chunk = VbsRegisterChunk { - header: VbsChunkHeader { - byte_count: size_of::() as u32, - chunk_type: BootMeasurementType::VP_REGISTER, - reserved: 0, - }, - reserved: 0, - vtl: reg.vtl, - reserved2: 0, - reserved3: 0, - reserved4: 0, - name: reg.register_name.into(), - value: reg.register_value, - }; - self.create_record_entry(&[chunk.as_bytes()])?; - Ok(()) - } - - fn create_record_entry(&mut self, chunks: &[&[u8]]) -> Result<(), Error> { - let mut hasher = Sha256::new(); - hasher.update(self.digest.as_bytes()); - for chunk in chunks { - hasher.update(chunk); - } - self.digest = hasher.finalize().into(); - Ok(()) - } - - fn finish_digest(&self) -> [u8; SHA_256_OUTPUT_SIZE_BYTES] { - self.digest - } -} diff --git a/vm/loader/src/importer.rs b/vm/loader/src/importer.rs index e4063771d0..a74df3b5a0 100644 --- a/vm/loader/src/importer.rs +++ b/vm/loader/src/importer.rs @@ -263,6 +263,12 @@ pub enum Aarch64Register { Pc(u64), X0(u64), X1(u64), + X2(u64), + X3(u64), + X4(u64), + X5(u64), + X6(u64), + X7(u64), Cpsr(u64), VbarEl1(u64), Ttbr0El1(u64), @@ -279,6 +285,12 @@ impl From for Aarch64Register { igvm_reg::Pc(v) => Aarch64Register::Pc(v), igvm_reg::X0(v) => Aarch64Register::X0(v), igvm_reg::X1(v) => Aarch64Register::X1(v), + igvm_reg::X2(v) => Aarch64Register::X2(v), + igvm_reg::X3(v) => Aarch64Register::X3(v), + igvm_reg::X4(v) => Aarch64Register::X4(v), + igvm_reg::X5(v) => Aarch64Register::X5(v), + igvm_reg::X6(v) => Aarch64Register::X6(v), + igvm_reg::X7(v) => Aarch64Register::X7(v), igvm_reg::Cpsr(v) => Aarch64Register::Cpsr(v), igvm_reg::SctlrEl1(v) => Aarch64Register::SctlrEl1(v), igvm_reg::TcrEl1(v) => Aarch64Register::TcrEl1(v), @@ -297,6 +309,12 @@ impl From for igvm::registers::AArch64Register { Aarch64Register::Pc(v) => igvm_reg::Pc(v), Aarch64Register::X0(v) => igvm_reg::X0(v), Aarch64Register::X1(v) => igvm_reg::X1(v), + Aarch64Register::X2(v) => igvm_reg::X2(v), + Aarch64Register::X3(v) => igvm_reg::X3(v), + Aarch64Register::X4(v) => igvm_reg::X4(v), + Aarch64Register::X5(v) => igvm_reg::X5(v), + Aarch64Register::X6(v) => igvm_reg::X6(v), + Aarch64Register::X7(v) => igvm_reg::X7(v), Aarch64Register::Cpsr(v) => igvm_reg::Cpsr(v), Aarch64Register::SctlrEl1(v) => igvm_reg::SctlrEl1(v), Aarch64Register::TcrEl1(v) => igvm_reg::TcrEl1(v), diff --git a/vm/vbs_defs/Cargo.toml b/vm/vbs_defs/Cargo.toml deleted file mode 100644 index 2d5f2a4ddb..0000000000 --- a/vm/vbs_defs/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -[package] -name = "vbs_defs" -edition.workspace = true -rust-version.workspace = true - -[dependencies] -bitfield-struct.workspace = true -igvm_defs.workspace = true -open_enum.workspace = true -static_assertions.workspace = true -zerocopy.workspace = true -[lints] -workspace = true diff --git a/vm/vbs_defs/src/lib.rs b/vm/vbs_defs/src/lib.rs deleted file mode 100644 index be0ebc040f..0000000000 --- a/vm/vbs_defs/src/lib.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// Virtualization Based Security (VBS) platform definitions defined by Hyper-V - -#![expect(missing_docs)] -#![forbid(unsafe_code)] -#![expect(non_camel_case_types)] - -use bitfield_struct::bitfield; -use igvm_defs::PAGE_SIZE_4K; -use open_enum::open_enum; -use static_assertions::const_assert; -use zerocopy::Immutable; -use zerocopy::IntoBytes; -use zerocopy::KnownLayout; - -pub const VBS_VP_CHUNK_SIZE_BYTES: usize = PAGE_SIZE_4K as usize + size_of::(); - -/// Structure containing the completed VBS boot measurement of the IGVM file. -/// The signature of the hash of this struct is the signature for [`igvm_defs::IGVM_VHS_VBS_MEASUREMENT`] -#[repr(C)] -#[derive(IntoBytes, Immutable, KnownLayout, Debug)] -pub struct VBS_VM_BOOT_MEASUREMENT_SIGNED_DATA { - /// The version of the signature structure - pub version: u32, - /// The user supplied product id - pub product_id: u32, - /// The uesr supplied module id - pub module_id: u32, - /// The user supplied svn - pub security_version: u32, - /// Security policy for the guest - pub security_policy: VBS_POLICY_FLAGS, - /// Algorithm that created the boot digest hash - pub boot_digest_algo: u32, - /// Algorithm that produces the signature - pub signing_algo: u32, - /// VBS Boot digest - pub boot_measurement_digest: [u8; 32], -} - -/// Chunk that is measured to generate digest. These consist of a 16 byte header followed by data. -/// This needs c style alignment to generate a consistent measurement. -/// Defined by the following struct in C: -/// ``` ignore -/// typedef struct _VBS_VM_BOOT_MEASUREMENT_CHUNK -/// { -/// UINT32 ByteCount; -/// VBS_VM_BOOT_MEASUREMENT_CHUNK_TYPE Type; -/// UINT64 Reserved; -/// -/// union -/// { -/// VBS_VM_BOOT_MEASUREMENT_CHUNK_VP_REGISTER VpRegister; -/// VBS_VM_BOOT_MEASUREMENT_CHUNK_VP_VTL_ENABLED VpVtlEnabled; -/// VBS_VM_BOOT_MEASUREMENT_CHUNK_GPA_PAGE GpaPage; -/// } u; -/// } VBS_VM_BOOT_MEASUREMENT_CHUNK, *PVBS_VM_BOOT_MEASUREMENT_CHUNK; -/// ``` -/// -/// Structure describing the chunk to be measured -#[repr(C)] -#[derive(IntoBytes, Immutable, KnownLayout)] -pub struct VbsChunkHeader { - /// The full size to be measured - pub byte_count: u32, - pub chunk_type: BootMeasurementType, - pub reserved: u64, -} - -/// Structure describing the register being measured. Will be padded to [`VBS_VP_CHUNK_SIZE_BYTES`] when hashed to generate digest -#[repr(C)] -#[derive(IntoBytes, Immutable, KnownLayout)] -pub struct VbsRegisterChunk { - pub header: VbsChunkHeader, - pub reserved: u32, - pub vtl: u8, - pub reserved2: u8, - pub reserved3: u16, - pub reserved4: u32, - pub name: u32, - pub value: [u8; 16], -} -const_assert!(size_of::() <= VBS_VP_CHUNK_SIZE_BYTES); - -/// Structure describing the page to be measured. -/// Page data is hashed after struct to generate digest, if not a full page, measurable data will be padded to [`VBS_VP_CHUNK_SIZE_BYTES`] -#[repr(C)] -#[derive(IntoBytes, Immutable, KnownLayout)] -pub struct VpGpaPageChunk { - pub header: VbsChunkHeader, - pub metadata: u64, - pub page_number: u64, -} - -open_enum! { -#[derive(IntoBytes, Immutable, KnownLayout)] -pub enum BootMeasurementType: u32 { - VP_REGISTER = 0, - VP_VTL_ENABLED = 1, - VP_GPA_PAGE = 2, -} -} - -/// Flags indicating read and write acceptance of a GPA Page and whether it is -/// to be measured in the digest -#[bitfield(u64)] -pub struct VBS_VM_GPA_PAGE_BOOT_METADATA { - #[bits(2)] - pub acceptance: u64, - #[bits(1)] - pub data_unmeasured: bool, - #[bits(61)] - reserved: u64, -} - -/// Flags defining the security policy for the guest -#[bitfield(u32)] -#[derive(IntoBytes, Immutable, KnownLayout)] -pub struct VBS_POLICY_FLAGS { - /// Guest supports debugging - #[bits(1)] - pub debug: bool, - #[bits(31)] - reserved: u32, -} -pub const VM_GPA_PAGE_READABLE: u64 = 0x1; -pub const VM_GPA_PAGE_WRITABLE: u64 = 0x2; diff --git a/vmm_core/vm_loader/src/initial_regs.rs b/vmm_core/vm_loader/src/initial_regs.rs index d97252b875..76f8945490 100644 --- a/vmm_core/vm_loader/src/initial_regs.rs +++ b/vmm_core/vm_loader/src/initial_regs.rs @@ -102,6 +102,12 @@ pub fn aarch64_initial_regs( Aarch64Register::Pc(v) => state.registers.pc = v, Aarch64Register::X0(v) => state.registers.x0 = v, Aarch64Register::X1(v) => state.registers.x1 = v, + Aarch64Register::X2(v) => state.registers.x2 = v, + Aarch64Register::X3(v) => state.registers.x3 = v, + Aarch64Register::X4(v) => state.registers.x4 = v, + Aarch64Register::X5(v) => state.registers.x5 = v, + Aarch64Register::X6(v) => state.registers.x6 = v, + Aarch64Register::X7(v) => state.registers.x7 = v, Aarch64Register::Cpsr(v) => state.registers.cpsr = v, Aarch64Register::Ttbr0El1(v) => state.system_registers.ttbr0_el1 = v, Aarch64Register::MairEl1(v) => state.system_registers.mair_el1 = v,