Skip to content
Open
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"
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"] }
4 changes: 4 additions & 0 deletions igvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +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 = ["dep:corim", "dep:uuid", "dep:sha2"] # CoRIM launch endorsement support
45 changes: 33 additions & 12 deletions igvm/src/c_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ pub enum IgvmResult {
IGVMAPI_UNSUPPORTED_PAGE_SIZE = -25,
IGVMAPI_INVALID_FIXED_HEADER_ARCH = -26,
IGVMAPI_MERGE_REVISION = -27,
#[cfg(feature = "corim")]
IGVMAPI_CORIM_GENERATION = -28,
#[cfg(feature = "corim")]
IGVMAPI_MEASUREMENT_FAILED = -29,
}

type IgvmHandle = i32;
Expand Down Expand Up @@ -164,6 +168,10 @@ fn translate_error(error: Error) -> IgvmResult {
Error::UnsupportedPageSize(_) => IgvmResult::IGVMAPI_UNSUPPORTED_PAGE_SIZE,
Error::InvalidFixedHeaderArch(_) => IgvmResult::IGVMAPI_INVALID_FIXED_HEADER_ARCH,
Error::MergeRevision => IgvmResult::IGVMAPI_MERGE_REVISION,
#[cfg(feature = "corim")]
Error::CorimGeneration(_) => IgvmResult::IGVMAPI_CORIM_GENERATION,
#[cfg(feature = "corim")]
Error::MeasurementFailed(_) => IgvmResult::IGVMAPI_MEASUREMENT_FAILED,
}
}

Expand Down Expand Up @@ -290,7 +298,7 @@ fn get_header(
.initialization_headers
.get(index as usize)
.ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?
.write_binary_header(&mut header_binary)
.write_binary_header(&mut header_binary, &mut FileDataSerializer::new(0))
.map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?;
}
IgvmHeaderSection::HEADER_SECTION_DIRECTIVE => {
Expand Down Expand Up @@ -322,17 +330,30 @@ fn get_header_data(
let igvm = handle_lock.get_mut()?;
let mut header_data = FileDataSerializer::new(0);

if section == IgvmHeaderSection::HEADER_SECTION_DIRECTIVE {
let header = igvm
.file
.directive_headers
.get(index as usize)
.ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?;
header
.write_binary_header(&mut Vec::<u8>::new(), &mut header_data)
.map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?;
} else {
return Err(IgvmResult::IGVMAPI_INVALID_PARAMETER);
match section {
IgvmHeaderSection::HEADER_SECTION_INITIALIZATION => {
let header = igvm
.file
.initialization_headers
.get(index as usize)
.ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?;
header
.write_binary_header(&mut Vec::<u8>::new(), &mut header_data)
.map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?;
}
IgvmHeaderSection::HEADER_SECTION_DIRECTIVE => {
let header = igvm
.file
.directive_headers
.get(index as usize)
.ok_or(IgvmResult::IGVMAPI_INVALID_PARAMETER)?;
header
.write_binary_header(&mut Vec::<u8>::new(), &mut header_data)
.map_err(|_| IgvmResult::IGVMAPI_INVALID_FILE)?;
}
_ => {
return Err(IgvmResult::IGVMAPI_INVALID_PARAMETER);
}
}

let header_data = header_data.take();
Expand Down
242 changes: 242 additions & 0 deletions igvm/src/corim/launch_endorsement/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-endorsement builder.
//!
//! Uses the [`corim`] crate's typed builder API instead of manual CBOR
//! tree construction.

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::CesCondition;
use corim::types::triples::ConditionalEndorsementSeriesTriple;
use corim::types::triples::ConditionalSeriesRecord;
use corim::types::triples::ReferenceTriple;
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 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 LaunchEndorsement");

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 = build_measurement_map(measurement);

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

let ces_triple = ConditionalEndorsementSeriesTriple::new(
CesCondition {
environment: env.clone(),
claims_list: Vec::new(),
authorized_by: None,
},
vec![ConditionalSeriesRecord::new(
vec![ces_selection],
vec![ces_addition],
)],
);

// Build CoMID
let comid = ComidBuilder::new(TagIdChoice::Text(tag_id))
.add_reference_triple(ReferenceTriple::new(env, vec![ref_meas]))
.add_conditional_endorsement_series(ces_triple)
.build()
.map_err(|e| Error::Build(Box::new(e)))?;

// Build CoRIM with profile URI
let corim_id = format!("{}/{}/launch-endorsement", 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_endorsement::Error;
use crate::corim::launch_endorsement::LaunchEndorsement;
use crate::corim::launch_endorsement::MeasurementKind;

fn build_and_decode(
le: LaunchEndorsement,
) -> (
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 = LaunchEndorsement::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-endorsement");
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 = LaunchEndorsement::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-endorsement");
}

#[test]
fn microsoft_vbs_round_trip() {
let mut le = LaunchEndorsement::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-endorsement");
}

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

#[test]
fn select_unpopulated_kind_rejected() {
let mut le = LaunchEndorsement::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 = LaunchEndorsement::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 = LaunchEndorsement::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