Skip to content

Commit 1f7c555

Browse files
authored
Merge pull request #47 from flashbots/peg/measurement-policy
Measurement policy allowing particular attestation types to be allowed or rejected
2 parents 97dde25 + db10c7d commit 1f7c555

File tree

6 files changed

+328
-168
lines changed

6 files changed

+328
-168
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ One or both of the proxy-client and proxy-server may be running in a confidentia
2626

2727
### Measurements File
2828

29-
Accepted measurements for the remote party are specified in a JSON file containing an array of objects, each of which specifies an accepted attestation type and set of measurements.
29+
Accepted measurements for the remote party can be specified in a JSON file containing an array of objects, each of which specifies an accepted attestation type and set of measurements.
3030

3131
This aims to match the formatting used by `cvm-reverse-proxy`.
3232

33-
These object have the following fields:
33+
These objects have the following fields:
3434
- `measurement_id` - a name used to describe the entry. For example the name and version of the CVM OS image that these measurements correspond to.
3535
- `attestation_type` - a string containing one of the attestation types (confidential computing platforms) described below.
3636
- `measurements` - an object with fields referring to the five measurement registers. Field names are the same as for the measurement headers (see below).
@@ -63,7 +63,9 @@ Example:
6363
]
6464
```
6565

66-
If a path to this file is not given or it contains an empty array, **any** attestation type and **any** measurements will be accepted, **including no attestation**. The measurements can still be checked up-stream by the source client or target service using header injection described below. But it is then up to these external programs to reject unacceptable configurations.
66+
The only mandatory field is `attestation_type`. If an attestation type is specified, but no measurements, *any* measurements will be accepted for this attestation type. The measurements can still be checked up-stream by the source client or target service using header injection described below. But it is then up to these external programs to reject unacceptable measurements.
67+
68+
If a measurements file is not provided, a single allowed attestation type **must** be specified using the `--allowed-remote-attestation-type` option. This may be `none` for cases where the remote party is not running in a CVM, but that must be explicitly specified.
6769

6870
### Measurement Headers
6971

src/attestation/measurements.rs

Lines changed: 230 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//! Measurements and policy for enforcing them when validating a remote attestation
12
use crate::attestation::{AttestationError, AttestationType};
23
use std::{collections::HashMap, path::PathBuf};
34

@@ -160,74 +161,249 @@ pub enum MeasurementFormatError {
160161
pub struct MeasurementRecord {
161162
/// An identifier, for example the name and version of the corresponding OS image
162163
pub measurement_id: String,
163-
/// The associated attestation platform
164-
pub attestation_type: AttestationType,
165164
/// The expected measurement register values
166165
pub measurements: Measurements,
167166
}
168167

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+
}
218185
}
219186

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+
}
221335
}
222336

223337
#[cfg(test)]
224338
mod tests {
225339
use super::*;
226340

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+
227355
#[tokio::test]
228356
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)
231407
.unwrap();
232408
}
233409
}

0 commit comments

Comments
 (0)