|
| 1 | +//! Measurements and policy for enforcing them when validating a remote attestation |
1 | 2 | use crate::attestation::{AttestationError, AttestationType}; |
2 | 3 | use std::{collections::HashMap, path::PathBuf}; |
3 | 4 |
|
@@ -160,74 +161,249 @@ pub enum MeasurementFormatError { |
160 | 161 | pub struct MeasurementRecord { |
161 | 162 | /// An identifier, for example the name and version of the corresponding OS image |
162 | 163 | pub measurement_id: String, |
163 | | - /// The associated attestation platform |
164 | | - pub attestation_type: AttestationType, |
165 | 164 | /// The expected measurement register values |
166 | 165 | pub measurements: Measurements, |
167 | 166 | } |
168 | 167 |
|
169 | | -/// Given the path to a JSON file containing measurements, return a [Vec<MeasurementRecord>] |
170 | | -pub async fn get_measurements_from_file( |
171 | | - measurement_file: PathBuf, |
172 | | -) -> Result<Vec<MeasurementRecord>, MeasurementFormatError> { |
173 | | - #[derive(Debug, Deserialize)] |
174 | | - struct MeasurementRecordSimple { |
175 | | - measurement_id: String, |
176 | | - attestation_type: String, |
177 | | - measurements: HashMap<String, MeasurementEntry>, |
178 | | - } |
179 | | - |
180 | | - #[derive(Debug, Deserialize)] |
181 | | - struct MeasurementEntry { |
182 | | - expected: String, |
183 | | - } |
184 | | - |
185 | | - let measurements_json = tokio::fs::read(measurement_file).await?; |
186 | | - let measurements_simple: Vec<MeasurementRecordSimple> = |
187 | | - serde_json::from_slice(&measurements_json)?; |
188 | | - let mut measurements = Vec::new(); |
189 | | - for measurement in measurements_simple { |
190 | | - measurements.push(MeasurementRecord { |
191 | | - measurement_id: measurement.measurement_id, |
192 | | - attestation_type: serde_json::from_value(serde_json::Value::String( |
193 | | - measurement.attestation_type, |
194 | | - )) |
195 | | - .map_err(|_| MeasurementFormatError::AttestationTypeNotValid)?, |
196 | | - measurements: Measurements { |
197 | | - platform: PlatformMeasurements { |
198 | | - mrtd: hex::decode(&measurement.measurements["0"].expected)? |
199 | | - .try_into() |
200 | | - .map_err(|_| MeasurementFormatError::BadLength)?, |
201 | | - rtmr0: hex::decode(&measurement.measurements["1"].expected)? |
202 | | - .try_into() |
203 | | - .map_err(|_| MeasurementFormatError::BadLength)?, |
204 | | - }, |
205 | | - cvm_image: CvmImageMeasurements { |
206 | | - rtmr1: hex::decode(&measurement.measurements["2"].expected)? |
207 | | - .try_into() |
208 | | - .map_err(|_| MeasurementFormatError::BadLength)?, |
209 | | - rtmr2: hex::decode(&measurement.measurements["3"].expected)? |
210 | | - .try_into() |
211 | | - .map_err(|_| MeasurementFormatError::BadLength)?, |
212 | | - rtmr3: hex::decode(&measurement.measurements["4"].expected)? |
213 | | - .try_into() |
214 | | - .map_err(|_| MeasurementFormatError::BadLength)?, |
215 | | - }, |
216 | | - }, |
217 | | - }); |
| 168 | +/// Represents the measurement policy |
| 169 | +/// |
| 170 | +/// This is a set of acceptable attestation types (CVM platforms) which may or may not enforce |
| 171 | +/// acceptable measurement values for each attestation type |
| 172 | +#[derive(Clone, Debug)] |
| 173 | +pub struct MeasurementPolicy { |
| 174 | + /// A map of accepted attestation types to accepted measurement values |
| 175 | + /// A value of None means accept any measurement value for this measurement type |
| 176 | + pub(crate) accepted_measurements: HashMap<AttestationType, Option<Vec<MeasurementRecord>>>, |
| 177 | +} |
| 178 | + |
| 179 | +impl MeasurementPolicy { |
| 180 | + /// This will only allow no attestation - and will reject it if one is given |
| 181 | + pub fn expect_none() -> Self { |
| 182 | + Self { |
| 183 | + accepted_measurements: HashMap::from([(AttestationType::None, None)]), |
| 184 | + } |
218 | 185 | } |
219 | 186 |
|
220 | | - Ok(measurements) |
| 187 | + /// Allow any measurements with the given attestation type |
| 188 | + pub fn single_attestation_type(attestation_type: AttestationType) -> Self { |
| 189 | + Self { |
| 190 | + accepted_measurements: HashMap::from([(attestation_type, None)]), |
| 191 | + } |
| 192 | + } |
| 193 | + |
| 194 | + /// Accept any attestation type with any measurements |
| 195 | + pub fn accept_anything() -> Self { |
| 196 | + Self { |
| 197 | + accepted_measurements: HashMap::from([ |
| 198 | + (AttestationType::None, None), |
| 199 | + (AttestationType::Dummy, None), |
| 200 | + (AttestationType::DcapTdx, None), |
| 201 | + (AttestationType::QemuTdx, None), |
| 202 | + (AttestationType::AzureTdx, None), |
| 203 | + (AttestationType::GcpTdx, None), |
| 204 | + ]), |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + /// Expect mock measurements used in tests |
| 209 | + #[cfg(test)] |
| 210 | + pub fn mock() -> Self { |
| 211 | + Self { |
| 212 | + accepted_measurements: HashMap::from([( |
| 213 | + AttestationType::DcapTdx, |
| 214 | + Some(vec![MeasurementRecord { |
| 215 | + measurement_id: "test".to_string(), |
| 216 | + measurements: Measurements { |
| 217 | + platform: PlatformMeasurements { |
| 218 | + mrtd: [0; 48], |
| 219 | + rtmr0: [0; 48], |
| 220 | + }, |
| 221 | + cvm_image: CvmImageMeasurements { |
| 222 | + rtmr1: [0; 48], |
| 223 | + rtmr2: [0; 48], |
| 224 | + rtmr3: [0; 48], |
| 225 | + }, |
| 226 | + }, |
| 227 | + }]), |
| 228 | + )]), |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + /// Given an attestation type and set of measurements, check whether they are acceptable |
| 233 | + pub fn check_measurement( |
| 234 | + &self, |
| 235 | + attestation_type: AttestationType, |
| 236 | + measurements: &Measurements, |
| 237 | + ) -> Result<(), AttestationError> { |
| 238 | + match self.accepted_measurements.get(&attestation_type) { |
| 239 | + Some(Some(measurement_set)) => { |
| 240 | + if measurement_set |
| 241 | + .iter() |
| 242 | + .any(|a| &a.measurements == measurements) |
| 243 | + { |
| 244 | + Ok(()) |
| 245 | + } else { |
| 246 | + Err(AttestationError::MeasurementsNotAccepted) |
| 247 | + } |
| 248 | + } |
| 249 | + Some(None) => Ok(()), |
| 250 | + None => Err(AttestationError::AttestationTypeNotAccepted), |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + /// Whether or not we require attestation |
| 255 | + pub fn has_remote_attestion(&self) -> bool { |
| 256 | + !self |
| 257 | + .accepted_measurements |
| 258 | + .contains_key(&AttestationType::None) |
| 259 | + } |
| 260 | + |
| 261 | + /// Given the path to a JSON file containing measurements, return a [MeasurementPolicy] |
| 262 | + pub async fn from_file(measurement_file: PathBuf) -> Result<Self, MeasurementFormatError> { |
| 263 | + let measurements_json = tokio::fs::read(measurement_file).await?; |
| 264 | + Self::from_json_bytes(measurements_json).await |
| 265 | + } |
| 266 | + |
| 267 | + /// Parse from JSON |
| 268 | + pub async fn from_json_bytes(json_bytes: Vec<u8>) -> Result<Self, MeasurementFormatError> { |
| 269 | + #[derive(Debug, Deserialize)] |
| 270 | + struct MeasurementRecordSimple { |
| 271 | + measurement_id: Option<String>, |
| 272 | + attestation_type: String, |
| 273 | + measurements: Option<HashMap<String, MeasurementEntry>>, |
| 274 | + } |
| 275 | + |
| 276 | + #[derive(Debug, Deserialize)] |
| 277 | + struct MeasurementEntry { |
| 278 | + expected: String, |
| 279 | + } |
| 280 | + |
| 281 | + let measurements_simple: Vec<MeasurementRecordSimple> = |
| 282 | + serde_json::from_slice(&json_bytes)?; |
| 283 | + |
| 284 | + let mut measurement_policy = HashMap::new(); |
| 285 | + |
| 286 | + for measurement in measurements_simple { |
| 287 | + let attestation_type = |
| 288 | + serde_json::from_value(serde_json::Value::String(measurement.attestation_type)) |
| 289 | + .unwrap(); |
| 290 | + |
| 291 | + if let Some(measurements) = measurement.measurements { |
| 292 | + let measurement_record = MeasurementRecord { |
| 293 | + measurement_id: measurement.measurement_id.unwrap_or_default(), |
| 294 | + measurements: Measurements { |
| 295 | + platform: PlatformMeasurements { |
| 296 | + mrtd: hex::decode(&measurements["0"].expected)? |
| 297 | + .try_into() |
| 298 | + .map_err(|_| MeasurementFormatError::BadLength)?, |
| 299 | + rtmr0: hex::decode(&measurements["1"].expected)? |
| 300 | + .try_into() |
| 301 | + .map_err(|_| MeasurementFormatError::BadLength)?, |
| 302 | + }, |
| 303 | + cvm_image: CvmImageMeasurements { |
| 304 | + rtmr1: hex::decode(&measurements["2"].expected)? |
| 305 | + .try_into() |
| 306 | + .map_err(|_| MeasurementFormatError::BadLength)?, |
| 307 | + rtmr2: hex::decode(&measurements["3"].expected)? |
| 308 | + .try_into() |
| 309 | + .map_err(|_| MeasurementFormatError::BadLength)?, |
| 310 | + rtmr3: hex::decode(&measurements["4"].expected)? |
| 311 | + .try_into() |
| 312 | + .map_err(|_| MeasurementFormatError::BadLength)?, |
| 313 | + }, |
| 314 | + }, |
| 315 | + }; |
| 316 | + |
| 317 | + measurement_policy |
| 318 | + .entry(attestation_type) |
| 319 | + .and_modify(|maybe_vec: &mut Option<Vec<MeasurementRecord>>| { |
| 320 | + match maybe_vec.as_mut() { |
| 321 | + Some(vec) => vec.push(measurement_record.clone()), |
| 322 | + None => *maybe_vec = Some(vec![measurement_record.clone()]), |
| 323 | + } |
| 324 | + }) |
| 325 | + .or_insert_with(|| Some(vec![measurement_record])); |
| 326 | + } else { |
| 327 | + measurement_policy.entry(attestation_type).or_insert(None); |
| 328 | + }; |
| 329 | + } |
| 330 | + |
| 331 | + Ok(MeasurementPolicy { |
| 332 | + accepted_measurements: measurement_policy, |
| 333 | + }) |
| 334 | + } |
221 | 335 | } |
222 | 336 |
|
223 | 337 | #[cfg(test)] |
224 | 338 | mod tests { |
225 | 339 | use super::*; |
226 | 340 |
|
| 341 | + fn mock_measurements() -> Measurements { |
| 342 | + Measurements { |
| 343 | + platform: PlatformMeasurements { |
| 344 | + mrtd: [0; 48], |
| 345 | + rtmr0: [0; 48], |
| 346 | + }, |
| 347 | + cvm_image: CvmImageMeasurements { |
| 348 | + rtmr1: [0; 48], |
| 349 | + rtmr2: [0; 48], |
| 350 | + rtmr3: [0; 48], |
| 351 | + }, |
| 352 | + } |
| 353 | + } |
| 354 | + |
227 | 355 | #[tokio::test] |
228 | 356 | async fn test_read_measurements_file() { |
229 | | - get_measurements_from_file("test-assets/measurements.json".into()) |
230 | | - .await |
| 357 | + let specific_measurements = |
| 358 | + MeasurementPolicy::from_file("test-assets/measurements.json".into()) |
| 359 | + .await |
| 360 | + .unwrap(); |
| 361 | + |
| 362 | + assert!(specific_measurements |
| 363 | + .accepted_measurements |
| 364 | + .get(&AttestationType::DcapTdx) |
| 365 | + .unwrap() |
| 366 | + .is_some()); |
| 367 | + |
| 368 | + // Will not match mock measurements |
| 369 | + assert!(matches!( |
| 370 | + specific_measurements |
| 371 | + .check_measurement(AttestationType::DcapTdx, &mock_measurements()) |
| 372 | + .unwrap_err(), |
| 373 | + AttestationError::MeasurementsNotAccepted |
| 374 | + )); |
| 375 | + |
| 376 | + // Will not match another attestation type |
| 377 | + assert!(matches!( |
| 378 | + specific_measurements |
| 379 | + .check_measurement(AttestationType::None, &mock_measurements()) |
| 380 | + .unwrap_err(), |
| 381 | + AttestationError::AttestationTypeNotAccepted |
| 382 | + )); |
| 383 | + } |
| 384 | + |
| 385 | + #[tokio::test] |
| 386 | + async fn test_read_measurements_file_non_specific() { |
| 387 | + let mock_measurements = mock_measurements(); |
| 388 | + // This specifies a particular attestation type, but not specific measurements |
| 389 | + let allowed_attestation_type = |
| 390 | + MeasurementPolicy::from_file("test-assets/measurements_2.json".into()) |
| 391 | + .await |
| 392 | + .unwrap(); |
| 393 | + |
| 394 | + allowed_attestation_type |
| 395 | + .check_measurement(AttestationType::DcapTdx, &mock_measurements) |
| 396 | + .unwrap(); |
| 397 | + |
| 398 | + assert!(allowed_attestation_type |
| 399 | + .accepted_measurements |
| 400 | + .get(&AttestationType::DcapTdx) |
| 401 | + .unwrap() |
| 402 | + .is_none()); |
| 403 | + |
| 404 | + // Will match mock measurements |
| 405 | + allowed_attestation_type |
| 406 | + .check_measurement(AttestationType::DcapTdx, &mock_measurements) |
231 | 407 | .unwrap(); |
232 | 408 | } |
233 | 409 | } |
0 commit comments