Skip to content

Commit 7ca670e

Browse files
authored
feat(corim): Support Launch Measurement CoRIM (#122)
This PR adds the support of generating Launch Measurement CoRIM that endorses the launch measurement and maps the measurement to SVN. The PR also introduces `IgvmSeralizer` that allows the user to query the launch measurement computed based on IGVM file. For now, gate all of this functionality behind the corim feature. Signed-off-by: Ming-Wei Shih <[email protected]>
1 parent f89a890 commit 7ca670e

13 files changed

Lines changed: 2325 additions & 2 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ igvm = { path = "igvm", version = "0.4.0" }
1414

1515
anyhow = "1.0"
1616
bitfield-struct = "0.12"
17+
corim = "0.1.2"
1718
crc32fast = { version = "1.3.2", default-features = false }
1819
hex = { version = "0.4", default-features = false }
1920
open-enum = "0.5.2"
2021
range_map_vec = "0.2.0"
22+
sha2 = "0.10"
2123
static_assertions = "1.1"
2224
thiserror = "2.0"
2325
tracing = "0.1"
26+
uuid = { version = "1", features = ["v5"] }
2427
zerocopy = { version = "0.8.14", features = ["derive"] }

igvm/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,20 @@ crate-type = ["staticlib", "rlib"]
4040
igvm_defs = { workspace = true, features = ["unstable"] }
4141

4242
bitfield-struct.workspace = true
43+
corim = { workspace = true, optional = true }
4344
range_map_vec.workspace = true
4445
crc32fast.workspace = true
4546
hex = { workspace = true, features = ["alloc"] }
4647
open-enum.workspace = true
48+
sha2 = { workspace = true, optional = true }
4749
thiserror.workspace = true
4850
tracing.workspace = true
51+
uuid = { workspace = true, optional = true }
4952
zerocopy = { workspace = true, features = ["alloc"] }
5053
static_assertions.workspace = true
5154

5255
[features]
5356
default = []
5457
capi = ["igvm-c"]
5558
igvm-c = [] # Add exports that allow the library to be used from C
56-
corim = ["igvm_defs/corim"]
59+
corim = ["igvm_defs/corim", "dep:corim", "dep:uuid", "dep:sha2"] # CoRIM launch measurement support

igvm/src/c_api.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ pub enum IgvmResult {
6161
IGVMAPI_INVALID_FIXED_HEADER_ARCH = -26,
6262
IGVMAPI_MERGE_REVISION = -27,
6363
IGVMAPI_INVALID_CCA_POLICY_COMPATIBILITY_MASK = -28,
64+
#[cfg(feature = "corim")]
65+
IGVMAPI_CORIM_GENERATION = -29,
66+
#[cfg(feature = "corim")]
67+
IGVMAPI_MEASUREMENT_FAILED = -30,
6468
}
6569

6670
type IgvmHandle = i32;
@@ -168,6 +172,10 @@ fn translate_error(error: Error) -> IgvmResult {
168172
Error::InvalidCcaPolicyCompatibilityMask(_) => {
169173
IgvmResult::IGVMAPI_INVALID_CCA_POLICY_COMPATIBILITY_MASK
170174
}
175+
#[cfg(feature = "corim")]
176+
Error::CorimGeneration(_) => IgvmResult::IGVMAPI_CORIM_GENERATION,
177+
#[cfg(feature = "corim")]
178+
Error::MeasurementFailed(_) => IgvmResult::IGVMAPI_MEASUREMENT_FAILED,
171179
}
172180
}
173181

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
// SPDX-License-Identifier: MIT
2+
//
3+
// Copyright (c) Microsoft Corporation.
4+
5+
//! CoRIM launch-measurement builder.
6+
7+
use corim::builder::ComidBuilder;
8+
use corim::builder::CorimBuilder;
9+
use corim::types::common::MeasuredElement;
10+
use corim::types::common::TagIdChoice;
11+
use corim::types::corim::CorimId;
12+
use corim::types::corim::ProfileChoice;
13+
use corim::types::environment::ClassMap;
14+
use corim::types::environment::EnvironmentMap;
15+
use corim::types::measurement::Digest;
16+
use corim::types::measurement::MeasurementMap;
17+
use corim::types::measurement::MeasurementValuesMap;
18+
use corim::types::measurement::SvnChoice;
19+
use corim::types::triples::ConditionalSeriesRecord;
20+
use igvm_defs::IgvmPlatformType;
21+
use uuid::Uuid;
22+
23+
use super::platform_info;
24+
use super::profile::PROFILE_URI;
25+
use super::Error;
26+
use super::TAG_ID_NAMESPACE;
27+
28+
// `ResolvedMeasurement` is intentionally profile-specific. When a second
29+
// profile arrives, common shape -- e.g., `(mkey, digest_alg, digest)` --
30+
// can be promoted to `crate::corim` if duplication justifies it.
31+
32+
/// A measurement resolved by the serializer, ready for CBOR encoding.
33+
#[derive(Debug, Clone)]
34+
pub(crate) struct ResolvedMeasurement {
35+
pub mkey: String,
36+
pub digest_alg: i64,
37+
pub digest: Vec<u8>,
38+
}
39+
40+
/// Build a complete CoRIM launch measurement endorsement as tag-501-wrapped CBOR bytes.
41+
///
42+
/// Emits a single reference-values triple containing the measurement,
43+
/// plus a single CES triple that selects on that measurement and
44+
/// endorses `svn`.
45+
pub(crate) fn build_corim_bytes(
46+
platform: IgvmPlatformType,
47+
measurement: &ResolvedMeasurement,
48+
svn: u64,
49+
) -> Result<Vec<u8>, Error> {
50+
let info = platform_info(platform).expect("platform validated by LaunchMeasurement");
51+
52+
let env = EnvironmentMap {
53+
class: Some(ClassMap {
54+
class_id: None,
55+
vendor: Some(info.vendor.into()),
56+
model: Some(info.model.into()),
57+
layer: None,
58+
index: None,
59+
}),
60+
instance: None,
61+
group: None,
62+
};
63+
64+
let tag_id = Uuid::new_v5(
65+
&TAG_ID_NAMESPACE,
66+
format!("{}/{}", info.vendor, info.model).as_bytes(),
67+
)
68+
.to_string();
69+
70+
let ref_meas = build_measurement_map(measurement);
71+
72+
// CES selection: same measurement-map as the reference value.
73+
let ces_selection = ref_meas.clone();
74+
75+
let ces_addition = MeasurementMap {
76+
mkey: None,
77+
mval: MeasurementValuesMap {
78+
svn: Some(SvnChoice::ExactValue(svn)),
79+
..MeasurementValuesMap::default()
80+
},
81+
authorized_by: None,
82+
};
83+
84+
// Declare the platform env once in the builder's catalog so the
85+
// reference triple and the CES condition share it by ref. The
86+
// `_for` builder methods resolve the ref to one inline env at
87+
// `build()` time, and `strict_links(true)` promotes any drift
88+
// between catalog-anchored envs into a build-time error.
89+
let mut comid_builder = ComidBuilder::new(TagIdChoice::Text(tag_id));
90+
let env_ref = comid_builder
91+
.declare_env("platform", env)
92+
.map_err(|e| Error::Build(Box::new(e)))?;
93+
94+
let comid = comid_builder
95+
.add_reference_triple_for(&env_ref, vec![ref_meas])
96+
.add_conditional_endorsement_series_for(
97+
&env_ref,
98+
Vec::new(),
99+
None,
100+
vec![ConditionalSeriesRecord::new(
101+
vec![ces_selection],
102+
vec![ces_addition],
103+
)],
104+
)
105+
.strict_links(true)
106+
.build()
107+
.map_err(|e| Error::Build(Box::new(e)))?;
108+
109+
// Build CoRIM with profile URI
110+
let corim_id = format!("{}/{}/launch-measurement", info.vendor, info.model);
111+
CorimBuilder::new(CorimId::Text(corim_id))
112+
.set_profile(ProfileChoice::Uri(PROFILE_URI.into()))
113+
.add_comid_tag(comid)
114+
.map_err(|e| Error::Build(Box::new(e)))?
115+
.build_bytes()
116+
.map_err(|e| Error::Build(Box::new(e)))
117+
}
118+
119+
fn build_measurement_map(m: &ResolvedMeasurement) -> MeasurementMap {
120+
MeasurementMap {
121+
mkey: Some(MeasuredElement::Text(m.mkey.clone())),
122+
mval: MeasurementValuesMap {
123+
digests: Some(vec![Digest::new(m.digest_alg, m.digest.clone())]),
124+
..MeasurementValuesMap::default()
125+
},
126+
authorized_by: None,
127+
}
128+
}
129+
130+
#[cfg(test)]
131+
mod tests {
132+
use igvm_defs::IgvmPlatformType;
133+
134+
use crate::corim::launch_measurement::Error;
135+
use crate::corim::launch_measurement::LaunchMeasurement;
136+
use crate::corim::launch_measurement::MeasurementKind;
137+
138+
fn build_and_decode(
139+
le: LaunchMeasurement,
140+
) -> (
141+
corim::types::corim::CorimMap,
142+
Vec<corim::types::comid::ComidTag>,
143+
) {
144+
// Tests use a fixed digest in place of the IGVM file's
145+
// auto-computed launch measurement.
146+
let platform = le.platform();
147+
let kind = *le.measurement_kinds().iter().next().unwrap();
148+
let (mkey, alg, len) = super::super::measurement_info(platform, kind).unwrap();
149+
let measurement = super::ResolvedMeasurement {
150+
mkey: mkey.to_string(),
151+
digest_alg: alg,
152+
digest: vec![0xAA; len],
153+
};
154+
155+
let svn = le.triples()[0].svn();
156+
157+
let bytes = super::build_corim_bytes(platform, &measurement, svn).unwrap();
158+
corim::validate::decode_and_validate(&bytes).unwrap()
159+
}
160+
161+
#[test]
162+
fn amd_sev_snp_round_trip() {
163+
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
164+
le.set_measurement(MeasurementKind::Launch).unwrap();
165+
le.endorse(1)
166+
.with(MeasurementKind::Launch)
167+
.unwrap()
168+
.finish()
169+
.unwrap();
170+
171+
let (corim, comids) = build_and_decode(le);
172+
assert_eq!(corim.id.to_string(), "AMD/SEV-SNP/launch-measurement");
173+
assert_eq!(comids.len(), 1);
174+
let tag_id = comids[0].tag_identity.tag_id.to_string();
175+
assert_eq!(tag_id, "77e8061e-4634-5e53-a848-d1d09e996843");
176+
}
177+
178+
#[test]
179+
fn intel_tdx_round_trip() {
180+
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::TDX).unwrap();
181+
le.set_measurement(MeasurementKind::Launch).unwrap();
182+
le.endorse(5)
183+
.with(MeasurementKind::Launch)
184+
.unwrap()
185+
.finish()
186+
.unwrap();
187+
188+
let (corim, _) = build_and_decode(le);
189+
assert_eq!(corim.id.to_string(), "Intel/TDX/launch-measurement");
190+
}
191+
192+
#[test]
193+
fn microsoft_vbs_round_trip() {
194+
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::VSM_ISOLATION).unwrap();
195+
le.set_measurement(MeasurementKind::Launch).unwrap();
196+
le.endorse(2)
197+
.with(MeasurementKind::Launch)
198+
.unwrap()
199+
.finish()
200+
.unwrap();
201+
202+
let (corim, _) = build_and_decode(le);
203+
assert_eq!(corim.id.to_string(), "Microsoft/VBS/launch-measurement");
204+
}
205+
206+
#[test]
207+
fn unsupported_platform_rejected() {
208+
let err = LaunchMeasurement::for_platform(IgvmPlatformType::NATIVE).unwrap_err();
209+
assert!(
210+
matches!(err, Error::UnsupportedPlatform(IgvmPlatformType::NATIVE)),
211+
"got: {err:?}"
212+
);
213+
}
214+
215+
#[test]
216+
fn select_unpopulated_kind_rejected() {
217+
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
218+
let err = le.endorse(1).with(MeasurementKind::Launch).unwrap_err();
219+
assert!(matches!(err, Error::MeasurementNotPopulated { .. }));
220+
}
221+
222+
#[test]
223+
fn duplicate_selection_rejected() {
224+
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
225+
le.set_measurement(MeasurementKind::Launch).unwrap();
226+
let err = le
227+
.endorse(1)
228+
.with(MeasurementKind::Launch)
229+
.unwrap()
230+
.with(MeasurementKind::Launch)
231+
.unwrap_err();
232+
assert!(matches!(err, Error::DuplicateSelection { .. }));
233+
}
234+
235+
#[test]
236+
fn empty_selection_rejected() {
237+
let mut le = LaunchMeasurement::for_platform(IgvmPlatformType::SEV_SNP).unwrap();
238+
le.set_measurement(MeasurementKind::Launch).unwrap();
239+
let err = le.endorse(1).finish().unwrap_err();
240+
assert!(matches!(err, Error::EmptySelection));
241+
}
242+
}

0 commit comments

Comments
 (0)