Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ igvm = { path = "igvm", version = "0.4.0" }

anyhow = "1.0"
bitfield-struct = "0.12"
corim = "0.1.2"
crc32fast = { version = "1.3.2", default-features = false }
hex = { version = "0.4", default-features = false }
open-enum = "0.5.2"
range_map_vec = "0.2.0"
sha2 = "0.10"
static_assertions = "1.1"
thiserror = "2.0"
tracing = "0.1"
uuid = { version = "1", features = ["v5"] }
zerocopy = { version = "0.8.14", features = ["derive"] }
5 changes: 4 additions & 1 deletion igvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,20 @@ crate-type = ["staticlib", "rlib"]
igvm_defs = { workspace = true, features = ["unstable"] }

bitfield-struct.workspace = true
corim = { workspace = true, optional = true }
range_map_vec.workspace = true
crc32fast.workspace = true
hex = { workspace = true, features = ["alloc"] }
open-enum.workspace = true
sha2 = { workspace = true, optional = true }
thiserror.workspace = true
tracing.workspace = true
uuid = { workspace = true, optional = true }
zerocopy = { workspace = true, features = ["alloc"] }
static_assertions.workspace = true

[features]
default = []
capi = ["igvm-c"]
igvm-c = [] # Add exports that allow the library to be used from C
corim = ["igvm_defs/corim"]
corim = ["igvm_defs/corim", "dep:corim", "dep:uuid", "dep:sha2"] # CoRIM launch measurement support
8 changes: 8 additions & 0 deletions igvm/src/c_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ pub enum IgvmResult {
IGVMAPI_INVALID_FIXED_HEADER_ARCH = -26,
IGVMAPI_MERGE_REVISION = -27,
IGVMAPI_INVALID_CCA_POLICY_COMPATIBILITY_MASK = -28,
#[cfg(feature = "corim")]
IGVMAPI_CORIM_GENERATION = -29,
#[cfg(feature = "corim")]
IGVMAPI_MEASUREMENT_FAILED = -30,
}

type IgvmHandle = i32;
Expand Down Expand Up @@ -168,6 +172,10 @@ fn translate_error(error: Error) -> IgvmResult {
Error::InvalidCcaPolicyCompatibilityMask(_) => {
IgvmResult::IGVMAPI_INVALID_CCA_POLICY_COMPATIBILITY_MASK
}
#[cfg(feature = "corim")]
Error::CorimGeneration(_) => IgvmResult::IGVMAPI_CORIM_GENERATION,
#[cfg(feature = "corim")]
Error::MeasurementFailed(_) => IgvmResult::IGVMAPI_MEASUREMENT_FAILED,
}
}

Expand Down
242 changes: 242 additions & 0 deletions igvm/src/corim/launch_measurement/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: MIT
//
// Copyright (c) Microsoft Corporation.

//! CoRIM launch-measurement builder.

use corim::builder::ComidBuilder;
use corim::builder::CorimBuilder;
use corim::types::common::MeasuredElement;
use corim::types::common::TagIdChoice;
use corim::types::corim::CorimId;
use corim::types::corim::ProfileChoice;
use corim::types::environment::ClassMap;
use corim::types::environment::EnvironmentMap;
use corim::types::measurement::Digest;
use corim::types::measurement::MeasurementMap;
use corim::types::measurement::MeasurementValuesMap;
use corim::types::measurement::SvnChoice;
use corim::types::triples::ConditionalSeriesRecord;
use igvm_defs::IgvmPlatformType;
use uuid::Uuid;

use super::platform_info;
use super::profile::PROFILE_URI;
use super::Error;
use super::TAG_ID_NAMESPACE;

// `ResolvedMeasurement` is intentionally profile-specific. When a second
// profile arrives, common shape -- e.g., `(mkey, digest_alg, digest)` --
// can be promoted to `crate::corim` if duplication justifies it.

/// A measurement resolved by the serializer, ready for CBOR encoding.
#[derive(Debug, Clone)]
pub(crate) struct ResolvedMeasurement {
pub mkey: String,
pub digest_alg: i64,
pub digest: Vec<u8>,
}

/// Build a complete CoRIM launch measurement endorsement as tag-501-wrapped CBOR bytes.
///
/// Emits a single reference-values triple containing the measurement,
/// plus a single CES triple that selects on that measurement and
/// endorses `svn`.
pub(crate) fn build_corim_bytes(
platform: IgvmPlatformType,
measurement: &ResolvedMeasurement,
svn: u64,
) -> Result<Vec<u8>, Error> {
let info = platform_info(platform).expect("platform validated by LaunchMeasurement");

let env = EnvironmentMap {
class: Some(ClassMap {
class_id: None,
vendor: Some(info.vendor.into()),
model: Some(info.model.into()),
layer: None,
index: None,
}),
instance: None,
group: None,
};

let tag_id = Uuid::new_v5(
&TAG_ID_NAMESPACE,
format!("{}/{}", info.vendor, info.model).as_bytes(),
)
.to_string();

let ref_meas = build_measurement_map(measurement);

// CES selection: same measurement-map as the reference value.
let ces_selection = ref_meas.clone();

let ces_addition = MeasurementMap {
mkey: None,
mval: MeasurementValuesMap {
svn: Some(SvnChoice::ExactValue(svn)),
..MeasurementValuesMap::default()
},
authorized_by: None,
};

// Declare the platform env once in the builder's catalog so the
// reference triple and the CES condition share it by ref. The
// `_for` builder methods resolve the ref to one inline env at
// `build()` time, and `strict_links(true)` promotes any drift
// between catalog-anchored envs into a build-time error.
let mut comid_builder = ComidBuilder::new(TagIdChoice::Text(tag_id));
let env_ref = comid_builder
.declare_env("platform", env)
.map_err(|e| Error::Build(Box::new(e)))?;

let comid = comid_builder
.add_reference_triple_for(&env_ref, vec![ref_meas])
.add_conditional_endorsement_series_for(
&env_ref,
Vec::new(),
None,
vec![ConditionalSeriesRecord::new(
vec![ces_selection],
vec![ces_addition],
)],
)
.strict_links(true)
.build()
.map_err(|e| Error::Build(Box::new(e)))?;

// Build CoRIM with profile URI
let corim_id = format!("{}/{}/launch-measurement", info.vendor, info.model);
CorimBuilder::new(CorimId::Text(corim_id))
.set_profile(ProfileChoice::Uri(PROFILE_URI.into()))
.add_comid_tag(comid)
.map_err(|e| Error::Build(Box::new(e)))?
.build_bytes()
.map_err(|e| Error::Build(Box::new(e)))
}

fn build_measurement_map(m: &ResolvedMeasurement) -> MeasurementMap {
MeasurementMap {
mkey: Some(MeasuredElement::Text(m.mkey.clone())),
mval: MeasurementValuesMap {
digests: Some(vec![Digest::new(m.digest_alg, m.digest.clone())]),
..MeasurementValuesMap::default()
},
authorized_by: None,
}
}

#[cfg(test)]
mod tests {
use igvm_defs::IgvmPlatformType;

use crate::corim::launch_measurement::Error;
use crate::corim::launch_measurement::LaunchMeasurement;
use crate::corim::launch_measurement::MeasurementKind;

fn build_and_decode(
le: LaunchMeasurement,
) -> (
corim::types::corim::CorimMap,
Vec<corim::types::comid::ComidTag>,
) {
// Tests use a fixed digest in place of the IGVM file's
// auto-computed launch measurement.
let platform = le.platform();
let kind = *le.measurement_kinds().iter().next().unwrap();
let (mkey, alg, len) = super::super::measurement_info(platform, kind).unwrap();
let measurement = super::ResolvedMeasurement {
mkey: mkey.to_string(),
digest_alg: alg,
digest: vec![0xAA; len],
};

let svn = le.triples()[0].svn();

let bytes = super::build_corim_bytes(platform, &measurement, svn).unwrap();
corim::validate::decode_and_validate(&bytes).unwrap()
}

#[test]
fn amd_sev_snp_round_trip() {
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
le.set_measurement(MeasurementKind::Launch).unwrap();
le.endorse(1)
.with(MeasurementKind::Launch)
.unwrap()
.finish()
.unwrap();

let (corim, comids) = build_and_decode(le);
assert_eq!(corim.id.to_string(), "AMD/SEV-SNP/launch-measurement");
assert_eq!(comids.len(), 1);
let tag_id = comids[0].tag_identity.tag_id.to_string();
assert_eq!(tag_id, "77e8061e-4634-5e53-a848-d1d09e996843");
}

#[test]
fn intel_tdx_round_trip() {
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::TDX).unwrap();
le.set_measurement(MeasurementKind::Launch).unwrap();
le.endorse(5)
.with(MeasurementKind::Launch)
.unwrap()
.finish()
.unwrap();

let (corim, _) = build_and_decode(le);
assert_eq!(corim.id.to_string(), "Intel/TDX/launch-measurement");
}

#[test]
fn microsoft_vbs_round_trip() {
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::VSM_ISOLATION).unwrap();
le.set_measurement(MeasurementKind::Launch).unwrap();
le.endorse(2)
.with(MeasurementKind::Launch)
.unwrap()
.finish()
.unwrap();

let (corim, _) = build_and_decode(le);
assert_eq!(corim.id.to_string(), "Microsoft/VBS/launch-measurement");
}

#[test]
fn unsupported_platform_rejected() {
let err = LaunchMeasurement::for_platform(IgvmPlatformType::NATIVE).unwrap_err();
assert!(
matches!(err, Error::UnsupportedPlatform(IgvmPlatformType::NATIVE)),
"got: {err:?}"
);
}

#[test]
fn select_unpopulated_kind_rejected() {
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
let err = le.endorse(1).with(MeasurementKind::Launch).unwrap_err();
assert!(matches!(err, Error::MeasurementNotPopulated { .. }));
}

#[test]
fn duplicate_selection_rejected() {
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
le.set_measurement(MeasurementKind::Launch).unwrap();
let err = le
.endorse(1)
.with(MeasurementKind::Launch)
.unwrap()
.with(MeasurementKind::Launch)
.unwrap_err();
assert!(matches!(err, Error::DuplicateSelection { .. }));
}

#[test]
fn empty_selection_rejected() {
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
le.set_measurement(MeasurementKind::Launch).unwrap();
let err = le.endorse(1).finish().unwrap_err();
assert!(matches!(err, Error::EmptySelection));
}
}
Loading
Loading