blob: 2e913053c84d71b9f000036a5bdf21c603cd222c [file] [log] [blame]
Shikha Panwar71b4f832023-08-21 17:43:09 +00001/*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! A “DICE policy” is a format for setting constraints on a DICE chain. A DICE chain policy
18//! verifier takes a policy and a DICE chain, and returns a boolean indicating whether the
19//! DICE chain meets the constraints set out on a policy.
20//!
21//! This forms the foundation of Dice Policy aware Authentication (DPA-Auth), where the server
22//! authenticates a client by comparing its dice chain against a set policy.
23//!
24//! Another use is "sealing", where clients can use an appropriately constructed dice policy to
25//! seal a secret. Unsealing is only permitted if dice chain of the component requesting unsealing
26//! complies with the policy.
27//!
28//! A typical policy will assert things like:
29//! # DK_pub must have this value
30//! # The DICE chain must be exactly five certificates long
31//! # authorityHash in the third certificate must have this value
32//! securityVersion in the fourth certificate must be an integer greater than 8
33//!
34//! These constraints used to express policy are (for now) limited to following 2 types:
35//! 1. Exact Match: useful for enforcing rules like authority hash should be exactly equal.
36//! 2. Greater than or equal to: Useful for setting policies that seal
37//! Anti-rollback protected entities (should be accessible to versions >= present).
38//!
39//! Dice Policy CDDL:
40//!
41//! dicePolicy = [
42//! 1, ; dice policy version
43//! + nodeConstraintList ; for each entry in dice chain
44//! ]
45//!
46//! nodeConstraintList = [
47//! * nodeConstraint
48//! ]
49//!
50//! ; We may add a hashConstraint item later
51//! nodeConstraint = exactMatchConstraint / geConstraint
52//!
53//! exactMatchConstraint = [1, keySpec, value]
54//! geConstraint = [2, keySpec, int]
55//!
56//! keySpec = [value+]
57//!
58//! value = bool / int / tstr / bstr
59
Shikha Panwarefe261f2023-10-20 20:46:05 +000060use anyhow::{anyhow, bail, ensure, Context, Result};
Shikha Panwar71b4f832023-08-21 17:43:09 +000061use ciborium::Value;
62use coset::{AsCborValue, CoseSign1};
Shikha Panwarefe261f2023-10-20 20:46:05 +000063use num_derive::FromPrimitive;
64use num_traits::FromPrimitive;
Shikha Panwar71b4f832023-08-21 17:43:09 +000065use std::borrow::Cow;
Shikha Panwarefe261f2023-10-20 20:46:05 +000066use std::iter::zip;
Shikha Panwar71b4f832023-08-21 17:43:09 +000067
68const DICE_POLICY_VERSION: u64 = 1;
69
70/// Constraint Types supported in Dice policy.
Shikha Panwarefe261f2023-10-20 20:46:05 +000071#[repr(u16)]
Shikha Panwar71b4f832023-08-21 17:43:09 +000072#[non_exhaustive]
Shikha Panwarefe261f2023-10-20 20:46:05 +000073#[derive(Clone, Copy, Debug, FromPrimitive, PartialEq)]
Shikha Panwar71b4f832023-08-21 17:43:09 +000074pub enum ConstraintType {
75 /// Enforce exact match criteria, indicating the policy should match
76 /// if the dice chain has exact same specified values.
77 ExactMatch = 1,
78 /// Enforce Greater than or equal to criteria. When applied on security_version, this
79 /// can be useful to set policy that matches dice chains with same or upgraded images.
80 GreaterOrEqual = 2,
81}
82
83/// ConstraintSpec is used to specify which constraint type to apply and
84/// on which all entries in a dice node.
85/// See documentation of `from_dice_chain()` for examples.
86pub struct ConstraintSpec {
87 constraint_type: ConstraintType,
88 // path is essentially a list of label/int.
89 // It identifies which entry (in a dice node) to be applying constraints on.
90 path: Vec<i64>,
91}
92
93impl ConstraintSpec {
94 /// Construct the ConstraintSpec.
95 pub fn new(constraint_type: ConstraintType, path: Vec<i64>) -> Result<Self> {
96 Ok(ConstraintSpec { constraint_type, path })
97 }
98}
99
100// TODO(b/291238565): Restrict (nested_)key & value type to (bool/int/tstr/bstr).
101// and maybe convert it into struct.
102/// Each constraint (on a dice node) is a tuple: (ConstraintType, constraint_path, value)
103#[derive(Debug, PartialEq)]
104struct Constraint(u16, Vec<i64>, Value);
105
106/// List of all constraints on a dice node.
107#[derive(Debug, PartialEq)]
108struct NodeConstraints(Box<[Constraint]>);
109
110/// Module for working with dice policy.
111#[derive(Debug, PartialEq)]
112pub struct DicePolicy {
113 version: u64,
114 node_constraints_list: Box<[NodeConstraints]>, // Constraint on each entry in dice chain.
115}
116
117impl DicePolicy {
118 /// Construct a dice policy from a given dice chain.
119 /// This can be used by clients to construct a policy to seal secrets.
120 /// Constraints on all but first dice node is applied using constraint_spec argument.
121 /// For the first node (which is a ROT key), the constraint is ExactMatch of the whole node.
122 ///
123 /// # Arguments
124 /// `dice_chain`: The serialized CBOR encoded Dice chain, adhering to Android Profile for DICE.
125 /// https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/android.md
126 ///
127 /// `constraint_spec`: List of constraints to be applied on dice node.
128 /// Each constraint is a ConstraintSpec object.
129 ///
130 /// Note: Dice node is treated as a nested map (& so the lookup is done in that fashion).
131 ///
132 /// Examples of constraint_spec:
133 /// 1. For exact_match on auth_hash & greater_or_equal on security_version
134 /// constraint_spec =[
135 /// (ConstraintType::ExactMatch, vec![AUTHORITY_HASH]),
136 /// (ConstraintType::GreaterOrEqual, vec![CONFIG_DESC, COMPONENT_NAME]),
137 /// ];
138 ///
139 /// 2. For hypothetical (and highly simplified) dice chain:
Shikha Panwarefe261f2023-10-20 20:46:05 +0000140 ///
Shikha Panwar71b4f832023-08-21 17:43:09 +0000141 /// [ROT_KEY, [{1 : 'a', 2 : {200 : 5, 201 : 'b'}}]]
142 /// The following can be used
143 /// constraint_spec =[
144 /// ConstraintSpec(ConstraintType::ExactMatch, vec![1]), // exact_matches value 'a'
145 /// ConstraintSpec(ConstraintType::GreaterOrEqual, vec![2, 200]),// matches any value >= 5
146 /// ];
147 pub fn from_dice_chain(dice_chain: &[u8], constraint_spec: &[ConstraintSpec]) -> Result<Self> {
Shikha Panwarefe261f2023-10-20 20:46:05 +0000148 let dice_chain = deserialize_dice_chain(dice_chain)?;
Shikha Panwar71b4f832023-08-21 17:43:09 +0000149 let mut constraints_list: Vec<NodeConstraints> = Vec::with_capacity(dice_chain.len());
150 let mut it = dice_chain.into_iter();
151
152 constraints_list.push(NodeConstraints(Box::new([Constraint(
153 ConstraintType::ExactMatch as u16,
154 Vec::new(),
155 it.next().unwrap(),
156 )])));
157
158 for (n, value) in it.enumerate() {
159 let entry = cbor_value_from_cose_sign(value)
160 .with_context(|| format!("Unable to get Cose payload at: {}", n))?;
161 constraints_list.push(payload_to_constraints(entry, constraint_spec)?);
162 }
163
164 Ok(DicePolicy {
165 version: DICE_POLICY_VERSION,
166 node_constraints_list: constraints_list.into_boxed_slice(),
167 })
168 }
Shikha Panwarefe261f2023-10-20 20:46:05 +0000169
170 /// Dice chain policy verifier - Compare the input dice chain against this Dice policy.
171 /// The method returns Ok() if the dice chain meets the constraints set in Dice policy,
172 /// otherwise returns error in case of mismatch.
173 /// TODO(b/291238565) Create a separate error module for DicePolicy mismatches.
174 pub fn matches_dice_chain(&self, dice_chain: &[u8]) -> Result<()> {
175 let dice_chain = deserialize_dice_chain(dice_chain)?;
176 ensure!(
177 dice_chain.len() == self.node_constraints_list.len(),
178 format!(
179 "Dice chain size({}) does not match policy({})",
180 dice_chain.len(),
181 self.node_constraints_list.len()
182 )
183 );
184
185 for (n, (dice_node, node_constraints)) in
186 zip(dice_chain, self.node_constraints_list.iter()).enumerate()
187 {
188 let dice_node_payload = if n == 0 {
189 dice_node
190 } else {
191 cbor_value_from_cose_sign(dice_node)
192 .with_context(|| format!("Unable to get Cose payload at: {}", n))?
193 };
194 check_constraints_on_node(node_constraints, &dice_node_payload)
195 .context(format!("Mismatch found at {}", n))?;
196 }
197 Ok(())
198 }
199}
200
201fn check_constraints_on_node(node_constraints: &NodeConstraints, dice_node: &Value) -> Result<()> {
202 for constraint in node_constraints.0.iter() {
203 check_constraint_on_node(constraint, dice_node)?;
204 }
205 Ok(())
206}
207
208fn check_constraint_on_node(constraint: &Constraint, dice_node: &Value) -> Result<()> {
209 let Constraint(cons_type, path, value_in_constraint) = constraint;
210 let value_in_node = lookup_value_in_nested_map(dice_node, path)?;
211 match ConstraintType::from_u16(*cons_type).ok_or(anyhow!("Unexpected Constraint type"))? {
212 ConstraintType::ExactMatch => ensure!(value_in_node == *value_in_constraint),
213 ConstraintType::GreaterOrEqual => {
214 let value_in_node = value_in_node
215 .as_integer()
216 .ok_or(anyhow!("Mismatch type: expected a cbor integer"))?;
217 let value_min = value_in_constraint
218 .as_integer()
219 .ok_or(anyhow!("Mismatch type: expected a cbor integer"))?;
220 ensure!(value_in_node >= value_min);
221 }
222 };
223 Ok(())
Shikha Panwar71b4f832023-08-21 17:43:09 +0000224}
225
226// Take the payload of a dice node & construct the constraints on it.
227fn payload_to_constraints(
228 payload: Value,
229 constraint_spec: &[ConstraintSpec],
230) -> Result<NodeConstraints> {
Shikha Panwar1d016862023-10-20 18:36:29 +0000231 let mut node_constraints: Vec<Constraint> = Vec::with_capacity(constraint_spec.len());
Shikha Panwar71b4f832023-08-21 17:43:09 +0000232 for constraint_item in constraint_spec {
233 let constraint_path = constraint_item.path.to_vec();
234 if constraint_path.is_empty() {
235 bail!("Expected non-empty key spec");
236 }
237 let val = lookup_value_in_nested_map(&payload, &constraint_path)
238 .context(format!("Value not found for constraint_path {:?}", constraint_path))?;
239 let constraint = Constraint(constraint_item.constraint_type as u16, constraint_path, val);
240 node_constraints.push(constraint);
241 }
242 Ok(NodeConstraints(node_constraints.into_boxed_slice()))
243}
244
245// Lookup value corresponding to constraint path in nested map.
246// This function recursively calls itself.
247// The depth of recursion is limited by the size of constraint_path.
248fn lookup_value_in_nested_map(cbor_map: &Value, constraint_path: &[i64]) -> Result<Value> {
249 if constraint_path.is_empty() {
250 return Ok(cbor_map.clone());
251 }
252 let explicit_map = get_map_from_value(cbor_map)?;
253 let val = lookup_value_in_map(&explicit_map, constraint_path[0])
254 .ok_or(anyhow!("Value not found for constraint key: {:?}", constraint_path[0]))?;
255 lookup_value_in_nested_map(val, &constraint_path[1..])
256}
257
258fn get_map_from_value(cbor_map: &Value) -> Result<Cow<Vec<(Value, Value)>>> {
259 match cbor_map {
260 Value::Bytes(b) => value_from_bytes(b)?
261 .into_map()
262 .map(Cow::Owned)
263 .map_err(|e| anyhow!("Expected a cbor map: {:?}", e)),
264 Value::Map(map) => Ok(Cow::Borrowed(map)),
265 _ => bail!("/Expected a cbor map {:?}", cbor_map),
266 }
267}
268
269fn lookup_value_in_map(map: &[(Value, Value)], key: i64) -> Option<&Value> {
270 let key = Value::Integer(key.into());
271 for (k, v) in map.iter() {
272 if k == &key {
273 return Some(v);
274 }
275 }
276 None
277}
278
279/// Extract the payload from the COSE Sign
280fn cbor_value_from_cose_sign(cbor: Value) -> Result<Value> {
281 let sign1 =
282 CoseSign1::from_cbor_value(cbor).map_err(|e| anyhow!("Error extracting CoseKey: {}", e))?;
283 match sign1.payload {
284 None => bail!("Missing payload"),
285 Some(payload) => Ok(value_from_bytes(&payload)?),
286 }
287}
Shikha Panwarefe261f2023-10-20 20:46:05 +0000288fn deserialize_dice_chain(dice_chain_bytes: &[u8]) -> Result<Vec<Value>> {
289 // TODO(b/298217847): Check if the given dice chain adheres to Explicit-key DiceCertChain
290 // format and if not, convert it.
291 let dice_chain =
292 value_from_bytes(dice_chain_bytes).context("Unable to decode top-level CBOR")?;
293 let dice_chain = match dice_chain {
294 Value::Array(array) if array.len() >= 2 => array,
295 _ => bail!("Expected an array of at least length 2, found: {:?}", dice_chain),
296 };
297 Ok(dice_chain)
298}
Shikha Panwar71b4f832023-08-21 17:43:09 +0000299
300/// Decodes the provided binary CBOR-encoded value and returns a
301/// ciborium::Value struct wrapped in Result.
302fn value_from_bytes(mut bytes: &[u8]) -> Result<Value> {
303 let value = ciborium::de::from_reader(&mut bytes)?;
304 // Ciborium tries to read one Value, & doesn't care if there is trailing data after it. We do.
305 if !bytes.is_empty() {
306 bail!("Unexpected trailing data while converting to CBOR value");
307 }
308 Ok(value)
309}
310
311#[cfg(test)]
312rdroidtest::test_main!();
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use ciborium::cbor;
318 use coset::{CoseKey, Header, ProtectedHeader};
319 use rdroidtest::test;
320
321 const AUTHORITY_HASH: i64 = -4670549;
322 const CONFIG_DESC: i64 = -4670548;
323 const COMPONENT_NAME: i64 = -70002;
324 const KEY_MODE: i64 = -4670551;
325
Shikha Panwar1d016862023-10-20 18:36:29 +0000326 // Helper struct to encapsulate artifacts that are useful for unit tests.
327 struct TestArtifacts {
328 // A dice chain.
329 input_dice: Vec<u8>,
330 // A list of ConstraintSpec that can be applied on the input_dice to get a dice policy.
331 constraint_spec: Vec<ConstraintSpec>,
332 // The expected dice policy if above constraint_spec is applied to input_dice.
333 expected_dice_policy: DicePolicy,
Shikha Panwarefe261f2023-10-20 20:46:05 +0000334 // Another dice chain, which is almost same as the input_dice, but (roughly) imitates
335 // an 'updated' one, ie, some int entries are higher than corresponding entry
336 // in input_chain.
337 updated_input_dice: Vec<u8>,
Shikha Panwar1d016862023-10-20 18:36:29 +0000338 }
339
340 impl TestArtifacts {
341 // Get an example instance of TestArtifacts. This uses a hard coded, hypothetical
342 // chain of certificates & a list of constraint_spec on this.
343 fn get_example() -> Self {
Shikha Panwarefe261f2023-10-20 20:46:05 +0000344 const EXAMPLE_NUM_1: i64 = 59765;
345 const EXAMPLE_NUM_2: i64 = 59766;
Shikha Panwar1d016862023-10-20 18:36:29 +0000346 const EXAMPLE_STRING: &str = "testing_dice_policy";
Shikha Panwarefe261f2023-10-20 20:46:05 +0000347 const UNCONSTRAINED_STRING: &str = "unconstrained_string";
348 const ANOTHER_UNCONSTRAINED_STRING: &str = "another_unconstrained_string";
Shikha Panwar1d016862023-10-20 18:36:29 +0000349
350 let rot_key = CoseKey::default().to_cbor_value().unwrap();
Shikha Panwarefe261f2023-10-20 20:46:05 +0000351 let input_dice = Self::get_dice_chain_helper(
352 rot_key.clone(),
353 EXAMPLE_NUM_1,
354 EXAMPLE_STRING,
355 UNCONSTRAINED_STRING,
356 );
Shikha Panwar1d016862023-10-20 18:36:29 +0000357
358 // Now construct constraint_spec on the input dice, note this will use the keys
359 // which are also hardcoded within the get_dice_chain_helper.
360
361 let constraint_spec = vec![
362 ConstraintSpec::new(ConstraintType::ExactMatch, vec![1]).unwrap(),
363 // Notice how key "2" is (deliberately) absent in ConstraintSpec
Shikha Panwarefe261f2023-10-20 20:46:05 +0000364 // so policy should not constrain it.
Shikha Panwar1d016862023-10-20 18:36:29 +0000365 ConstraintSpec::new(ConstraintType::GreaterOrEqual, vec![3, 100]).unwrap(),
366 ];
367 let expected_dice_policy = DicePolicy {
368 version: 1,
369 node_constraints_list: Box::new([
370 NodeConstraints(Box::new([Constraint(
371 ConstraintType::ExactMatch as u16,
372 vec![],
373 rot_key.clone(),
374 )])),
375 NodeConstraints(Box::new([
376 Constraint(
377 ConstraintType::ExactMatch as u16,
378 vec![1],
379 Value::Text(EXAMPLE_STRING.to_string()),
380 ),
381 Constraint(
382 ConstraintType::GreaterOrEqual as u16,
383 vec![3, 100],
Shikha Panwarefe261f2023-10-20 20:46:05 +0000384 Value::from(EXAMPLE_NUM_1),
Shikha Panwar1d016862023-10-20 18:36:29 +0000385 ),
386 ])),
387 ]),
388 };
Shikha Panwarefe261f2023-10-20 20:46:05 +0000389
390 let updated_input_dice = Self::get_dice_chain_helper(
391 rot_key.clone(),
392 EXAMPLE_NUM_2,
393 EXAMPLE_STRING,
394 ANOTHER_UNCONSTRAINED_STRING,
395 );
396 Self { input_dice, constraint_spec, expected_dice_policy, updated_input_dice }
397 }
398
399 // Helper method method to generate a dice chain with a given rot_key.
400 // Other arguments are ad-hoc values in the nested map. Callers use these to
401 // construct appropriate constrains in dice policies.
402 fn get_dice_chain_helper(
403 rot_key: Value,
404 version: i64,
405 constrained_string: &str,
406 unconstrained_string: &str,
407 ) -> Vec<u8> {
408 let nested_payload = cbor!({
409 100 => version
410 })
411 .unwrap();
412
413 let payload = cbor!({
414 1 => constrained_string,
415 2 => unconstrained_string,
416 3 => Value::Bytes(value_to_bytes(&nested_payload).unwrap()),
417 })
418 .unwrap();
419 let payload = value_to_bytes(&payload).unwrap();
420 let dice_node = CoseSign1 {
421 protected: ProtectedHeader::default(),
422 unprotected: Header::default(),
423 payload: Some(payload),
424 signature: b"ddef".to_vec(),
425 }
426 .to_cbor_value()
427 .unwrap();
428 let input_dice = Value::Array([rot_key.clone(), dice_node].to_vec());
429
430 value_to_bytes(&input_dice).unwrap()
Shikha Panwar1d016862023-10-20 18:36:29 +0000431 }
432 }
433
434 test!(policy_structure_check);
435 fn policy_structure_check() {
436 let example = TestArtifacts::get_example();
437 let policy =
438 DicePolicy::from_dice_chain(&example.input_dice, &example.constraint_spec).unwrap();
439
440 // Assert policy is exactly as expected!
441 assert_eq!(policy, example.expected_dice_policy);
442 }
Shikha Panwar71b4f832023-08-21 17:43:09 +0000443
Shikha Panwarefe261f2023-10-20 20:46:05 +0000444 test!(policy_matches_original_dice_chain);
445 fn policy_matches_original_dice_chain() {
446 let example = TestArtifacts::get_example();
447 assert!(
448 DicePolicy::from_dice_chain(&example.input_dice, &example.constraint_spec)
449 .unwrap()
450 .matches_dice_chain(&example.input_dice)
451 .is_ok(),
452 "The dice chain did not match the policy constructed out of it!"
453 );
454 }
455
456 test!(policy_matches_updated_dice_chain);
457 fn policy_matches_updated_dice_chain() {
458 let example = TestArtifacts::get_example();
459 assert!(
460 DicePolicy::from_dice_chain(&example.input_dice, &example.constraint_spec)
461 .unwrap()
462 .matches_dice_chain(&example.updated_input_dice)
463 .is_ok(),
464 "The updated dice chain did not match the original policy!"
465 );
466 }
467
468 test!(policy_mismatch_downgraded_dice_chain);
469 fn policy_mismatch_downgraded_dice_chain() {
470 let example = TestArtifacts::get_example();
471 assert!(
472 DicePolicy::from_dice_chain(&example.updated_input_dice, &example.constraint_spec)
473 .unwrap()
474 .matches_dice_chain(&example.input_dice)
475 .is_err(),
476 "The (downgraded) dice chain matched the policy constructed out of the 'updated'\
477 dice chain!!"
478 );
479 }
480
Shikha Panwar71b4f832023-08-21 17:43:09 +0000481 test!(policy_dice_size_is_same);
482 fn policy_dice_size_is_same() {
Shikha Panwar1d016862023-10-20 18:36:29 +0000483 // This is the number of certs in compos bcc (including the first ROT)
484 // To analyze a bcc use hwtrust tool from /tools/security/remote_provisioning/hwtrust
485 // `hwtrust --verbose dice-chain [path]/composbcc`
486 let compos_dice_chain_size: usize = 5;
Shikha Panwar71b4f832023-08-21 17:43:09 +0000487 let input_dice = include_bytes!("../testdata/composbcc");
488 let constraint_spec = [
489 ConstraintSpec::new(ConstraintType::ExactMatch, vec![AUTHORITY_HASH]).unwrap(),
490 ConstraintSpec::new(ConstraintType::ExactMatch, vec![KEY_MODE]).unwrap(),
491 ConstraintSpec::new(ConstraintType::GreaterOrEqual, vec![CONFIG_DESC, COMPONENT_NAME])
492 .unwrap(),
493 ];
494 let policy = DicePolicy::from_dice_chain(input_dice, &constraint_spec).unwrap();
Shikha Panwar1d016862023-10-20 18:36:29 +0000495 assert_eq!(policy.node_constraints_list.len(), compos_dice_chain_size);
Shikha Panwar71b4f832023-08-21 17:43:09 +0000496 }
497
498 /// Encodes a ciborium::Value into bytes.
499 fn value_to_bytes(value: &Value) -> Result<Vec<u8>> {
500 let mut bytes: Vec<u8> = Vec::new();
501 ciborium::ser::into_writer(&value, &mut bytes)?;
502 Ok(bytes)
503 }
504}