xref: /aosp_15_r20/system/secretkeeper/dice_policy/src/lib.rs (revision 3f8e9d82f4020c68ad19a99fc5fdc1fc90b79379)
1 /*
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 (keep in sync with DicePolicy.cddl):
40 //!
41 //! ```
42 //! dicePolicy = [
43 //! 1, ; dice policy version
44 //! + nodeConstraintList ; for each entry in dice chain
45 //! ]
46 //!
47 //! nodeConstraintList = [
48 //!     * nodeConstraint
49 //! ]
50 //!
51 //! ; We may add a hashConstraint item later
52 //! nodeConstraint = exactMatchConstraint / geConstraint
53 //!
54 //! exactMatchConstraint = [1, keySpec, value]
55 //! geConstraint = [2, keySpec, int]
56 //!
57 //! keySpec = [value+]
58 //!
59 //! value = bool / int / tstr / bstr
60 //! ```
61 
62 use ciborium::Value;
63 use coset::{AsCborValue, CborSerializable, CoseError, CoseError::UnexpectedItem, CoseSign1};
64 use std::borrow::Cow;
65 use std::iter::zip;
66 
67 type Error = String;
68 
69 /// Version of the Dice policy spec
70 pub const DICE_POLICY_VERSION: u64 = 1;
71 /// Identifier for `exactMatchConstraint` as per spec
72 pub const EXACT_MATCH_CONSTRAINT: u16 = 1;
73 /// Identifier for `geConstraint` as per spec
74 pub const GREATER_OR_EQUAL_CONSTRAINT: u16 = 2;
75 
76 /// Given an Android dice chain, check if it matches the given policy. This method returns
77 /// Ok(()) in case of successful match, otherwise returns error in case of failure.
chain_matches_policy(dice_chain: &[u8], policy: &[u8]) -> Result<(), Error>78 pub fn chain_matches_policy(dice_chain: &[u8], policy: &[u8]) -> Result<(), Error> {
79     DicePolicy::from_slice(policy)
80         .map_err(|e| format!("DicePolicy decoding failed {e:?}"))?
81         .matches_dice_chain(dice_chain)
82         .map_err(|e| format!("DicePolicy matching failed {e:?}"))?;
83     Ok(())
84 }
85 
86 // TODO(b/291238565): (nested_)key & value type should be (bool/int/tstr/bstr). Status quo, only
87 // integer (nested_)key is supported.
88 // and maybe convert it into struct.
89 /// Each constraint (on a dice node) is a tuple: (ConstraintType, constraint_path, value)
90 /// This is Rust equivalent of `nodeConstraint` from CDDL above. Keep in sync!
91 #[derive(Clone, Debug, PartialEq)]
92 pub struct Constraint(u16, Vec<i64>, Value);
93 
94 impl Constraint {
95     /// Construct a new Constraint
new(constraint_type: u16, path: Vec<i64>, value: Value) -> Result<Self, Error>96     pub fn new(constraint_type: u16, path: Vec<i64>, value: Value) -> Result<Self, Error> {
97         if constraint_type != EXACT_MATCH_CONSTRAINT
98             && constraint_type != GREATER_OR_EQUAL_CONSTRAINT
99         {
100             return Err(format!("Invalid Constraint type: {constraint_type}"));
101         }
102         Ok(Self(constraint_type, path, value))
103     }
104 }
105 
106 impl AsCborValue for Constraint {
from_cbor_value(value: Value) -> Result<Self, CoseError>107     fn from_cbor_value(value: Value) -> Result<Self, CoseError> {
108         let [constrained_type, constraint_path, val] = value
109             .into_array()
110             .map_err(|_| UnexpectedItem("-", "Array"))?
111             .try_into()
112             .map_err(|_| UnexpectedItem("Array", "Array of size 3"))?;
113         let constrained_type: u16 = value_to_integer(&constrained_type)?
114             .try_into()
115             .map_err(|_| UnexpectedItem("Integer", "u16"))?;
116         let path_res: Vec<i64> = constraint_path
117             .into_array()
118             .map_err(|_| UnexpectedItem("-", "Array"))?
119             .iter()
120             .map(value_to_integer)
121             .collect::<Result<_, _>>()?;
122         Ok(Self(constrained_type, path_res, val))
123     }
124 
to_cbor_value(self) -> Result<Value, CoseError>125     fn to_cbor_value(self) -> Result<Value, CoseError> {
126         Ok(Value::Array(vec![
127             Value::from(self.0),
128             Value::Array(self.1.into_iter().map(Value::from).collect()),
129             self.2,
130         ]))
131     }
132 }
133 
134 /// List of all constraints on a dice node.
135 /// This is Rust equivalent of `nodeConstraintList` in the CDDL above. Keep in sync!
136 #[derive(Clone, Debug, PartialEq)]
137 pub struct NodeConstraints(pub Box<[Constraint]>);
138 
139 impl AsCborValue for NodeConstraints {
from_cbor_value(value: Value) -> Result<Self, CoseError>140     fn from_cbor_value(value: Value) -> Result<Self, CoseError> {
141         let res: Vec<Constraint> = value
142             .into_array()
143             .map_err(|_| UnexpectedItem("-", "Array"))?
144             .into_iter()
145             .map(Constraint::from_cbor_value)
146             .collect::<Result<_, _>>()?;
147         if res.is_empty() {
148             return Err(UnexpectedItem("Empty array", "Non empty array"));
149         }
150         Ok(Self(res.into_boxed_slice()))
151     }
152 
to_cbor_value(self) -> Result<Value, CoseError>153     fn to_cbor_value(self) -> Result<Value, CoseError> {
154         let res: Vec<Value> = self
155             .0
156             .into_vec()
157             .into_iter()
158             .map(Constraint::to_cbor_value)
159             .collect::<Result<_, _>>()?;
160         Ok(Value::Array(res))
161     }
162 }
163 
164 /// This is Rust equivalent of `dicePolicy` in the CDDL above. Keep in sync!
165 #[derive(Clone, Debug, PartialEq)]
166 pub struct DicePolicy {
167     /// Dice policy version
168     pub version: u64,
169     /// List of `NodeConstraints`, one for each node of Dice chain.
170     pub node_constraints_list: Box<[NodeConstraints]>,
171 }
172 
173 impl AsCborValue for DicePolicy {
from_cbor_value(value: Value) -> Result<Self, CoseError>174     fn from_cbor_value(value: Value) -> Result<Self, CoseError> {
175         let mut arr = value.into_array().map_err(|_| UnexpectedItem("-", "Array"))?;
176         if arr.len() < 2 {
177             return Err(UnexpectedItem("Array", "Array with at least 2 elements"));
178         }
179         let (version, node_cons_list) = (value_to_integer(arr.first().unwrap())?, arr.split_off(1));
180         let version: u64 = version.try_into().map_err(|_| UnexpectedItem("-", "u64"))?;
181         let node_cons_list: Vec<NodeConstraints> = node_cons_list
182             .into_iter()
183             .map(NodeConstraints::from_cbor_value)
184             .collect::<Result<_, _>>()?;
185         Ok(Self { version, node_constraints_list: node_cons_list.into_boxed_slice() })
186     }
187 
to_cbor_value(self) -> Result<Value, CoseError>188     fn to_cbor_value(self) -> Result<Value, CoseError> {
189         let mut res: Vec<Value> = Vec::with_capacity(1 + self.node_constraints_list.len());
190         res.push(Value::from(self.version));
191         for node_cons in self.node_constraints_list.into_vec() {
192             res.push(node_cons.to_cbor_value()?)
193         }
194         Ok(Value::Array(res))
195     }
196 }
197 
198 impl CborSerializable for DicePolicy {}
199 
200 impl DicePolicy {
201     /// Dice chain policy verifier - Compare the input dice chain against this Dice policy.
202     /// The method returns Ok() if the dice chain meets the constraints set in Dice policy,
203     /// otherwise returns error in case of mismatch.
204     /// TODO(b/291238565) Create a separate error module for DicePolicy mismatches.
matches_dice_chain(&self, dice_chain: &[u8]) -> Result<(), Error>205     pub fn matches_dice_chain(&self, dice_chain: &[u8]) -> Result<(), Error> {
206         let dice_chain = deserialize_cbor_array(dice_chain)?;
207         check_is_explicit_key_dice_chain(&dice_chain)?;
208         if dice_chain.len() != self.node_constraints_list.len() {
209             return Err(format!(
210                 "Dice chain size({}) does not match policy({})",
211                 dice_chain.len(),
212                 self.node_constraints_list.len()
213             ));
214         }
215 
216         for (n, (dice_node, node_constraints)) in
217             zip(dice_chain, self.node_constraints_list.iter()).enumerate()
218         {
219             let dice_node_payload = if n <= 1 {
220                 // 1st & 2nd dice node of Explicit-key DiceCertChain format are
221                 // EXPLICIT_KEY_DICE_CERT_CHAIN_VERSION & DiceCertChainInitialPayload. The rest are
222                 // DiceChainEntry which is a CoseSign1.
223                 dice_node
224             } else {
225                 payload_value_from_cose_sign(dice_node)
226                     .map_err(|e| format!("Unable to get Cose payload at {n}: {e:?}"))?
227             };
228             check_constraints_on_node(node_constraints, &dice_node_payload)
229                 .map_err(|e| format!("Mismatch found at {n}: {e:?}"))?;
230         }
231         Ok(())
232     }
233 }
234 
check_constraints_on_node( node_constraints: &NodeConstraints, dice_node: &Value, ) -> Result<(), Error>235 fn check_constraints_on_node(
236     node_constraints: &NodeConstraints,
237     dice_node: &Value,
238 ) -> Result<(), Error> {
239     for constraint in node_constraints.0.iter() {
240         check_constraint_on_node(constraint, dice_node)?;
241     }
242     Ok(())
243 }
244 
check_constraint_on_node(constraint: &Constraint, dice_node: &Value) -> Result<(), Error>245 fn check_constraint_on_node(constraint: &Constraint, dice_node: &Value) -> Result<(), Error> {
246     let Constraint(cons_type, path, value_in_constraint) = constraint;
247     let value_in_node = lookup_in_nested_container(dice_node, path)?
248         .ok_or(format!("Value not found for constraint_path {path:?})"))?;
249     match *cons_type {
250         EXACT_MATCH_CONSTRAINT => {
251             if value_in_node != *value_in_constraint {
252                 return Err(format!(
253                     "Policy mismatch. Expected {value_in_constraint:?}; found {value_in_node:?}"
254                 ));
255             }
256         }
257         GREATER_OR_EQUAL_CONSTRAINT => {
258             let value_in_node = value_in_node
259                 .as_integer()
260                 .ok_or("Mismatch type: expected a CBOR integer".to_string())?;
261             let value_min = value_in_constraint
262                 .as_integer()
263                 .ok_or("Mismatch type: expected a CBOR integer".to_string())?;
264             if value_in_node < value_min {
265                 return Err(format!(
266                     "Policy mismatch. Expected >= {value_min:?}; found {value_in_node:?}"
267                 ));
268             }
269         }
270         cons_type => return Err(format!("Unexpected constraint type {cons_type:?}")),
271     };
272     Ok(())
273 }
274 
275 /// Lookup value corresponding to constraint path in nested container.
276 /// This function recursively calls itself.
277 /// The depth of recursion is limited by the size of constraint_path.
lookup_in_nested_container( container: &Value, constraint_path: &[i64], ) -> Result<Option<Value>, Error>278 pub fn lookup_in_nested_container(
279     container: &Value,
280     constraint_path: &[i64],
281 ) -> Result<Option<Value>, Error> {
282     if constraint_path.is_empty() {
283         return Ok(Some(container.clone()));
284     }
285     let explicit_container = get_container_from_value(container)?;
286     lookup_value_in_container(&explicit_container, constraint_path[0])
287         .map_or_else(|| Ok(None), |val| lookup_in_nested_container(val, &constraint_path[1..]))
288 }
289 
get_container_from_value(container: &Value) -> Result<Container, Error>290 fn get_container_from_value(container: &Value) -> Result<Container, Error> {
291     match container {
292         // Value can be Map/Array/Encoded Map. Encoded Arrays are not yet supported (or required).
293         // Note: Encoded Map is used for Configuration descriptor entry in DiceChainEntryPayload.
294         Value::Bytes(b) => Value::from_slice(b)
295             .map_err(|e| format!("{e:?}"))?
296             .into_map()
297             .map(|m| Container::Map(Cow::Owned(m)))
298             .map_err(|e| format!("Expected a CBOR map: {:?}", e)),
299         Value::Map(map) => Ok(Container::Map(Cow::Borrowed(map))),
300         Value::Array(array) => Ok(Container::Array(array)),
301         _ => Err(format!("Expected an array/map/bytes {container:?}")),
302     }
303 }
304 
305 #[derive(Clone)]
306 enum Container<'a> {
307     Map(Cow<'a, Vec<(Value, Value)>>),
308     Array(&'a Vec<Value>),
309 }
310 
lookup_value_in_container<'a>(container: &'a Container<'a>, key: i64) -> Option<&'a Value>311 fn lookup_value_in_container<'a>(container: &'a Container<'a>, key: i64) -> Option<&'a Value> {
312     match container {
313         Container::Array(array) => array.get(key as usize),
314         Container::Map(map) => {
315             let key = Value::Integer(key.into());
316             let mut val = None;
317             for (k, v) in map.iter() {
318                 if k == &key {
319                     val = Some(v);
320                     break;
321                 }
322             }
323             val
324         }
325     }
326 }
327 
328 /// This library only works with Explicit-key DiceCertChain format. Further we require it to have
329 /// at least 1 DiceChainEntry. Note that this is a lightweight check so that we fail early for
330 /// legacy chains.
check_is_explicit_key_dice_chain(dice_chain: &[Value]) -> Result<(), Error>331 pub fn check_is_explicit_key_dice_chain(dice_chain: &[Value]) -> Result<(), Error> {
332     if matches!(dice_chain, [Value::Integer(_version), Value::Bytes(_public_key), _entry, ..]) {
333         Ok(())
334     } else {
335         Err("Chain is not in explicit key format".to_string())
336     }
337 }
338 
339 /// Extract the payload from the COSE Sign
payload_value_from_cose_sign(cbor: Value) -> Result<Value, Error>340 pub fn payload_value_from_cose_sign(cbor: Value) -> Result<Value, Error> {
341     let sign1 = CoseSign1::from_cbor_value(cbor)
342         .map_err(|e| format!("Error extracting CoseSign1: {e:?}"))?;
343     match sign1.payload {
344         None => Err("Missing payload".to_string()),
345         Some(payload) => Value::from_slice(&payload).map_err(|e| format!("{e:?}")),
346     }
347 }
348 
349 /// Decode a CBOR array
deserialize_cbor_array(cbor_array_bytes: &[u8]) -> Result<Vec<Value>, Error>350 pub fn deserialize_cbor_array(cbor_array_bytes: &[u8]) -> Result<Vec<Value>, Error> {
351     let cbor_array = Value::from_slice(cbor_array_bytes)
352         .map_err(|e| format!("Unable to decode top-level CBOR: {e:?}"))?;
353     let cbor_array =
354         cbor_array.into_array().map_err(|e| format!("Expected an array found: {e:?}"))?;
355     Ok(cbor_array)
356 }
357 
358 // Useful to convert [`ciborium::Value`] to integer. Note we already downgrade the returned
359 // integer to i64 for convenience. Value::Integer is capable of storing bigger numbers.
value_to_integer(value: &Value) -> Result<i64, CoseError>360 fn value_to_integer(value: &Value) -> Result<i64, CoseError> {
361     let num = value
362         .as_integer()
363         .ok_or(CoseError::UnexpectedItem("-", "Integer"))?
364         .try_into()
365         .map_err(|_| CoseError::UnexpectedItem("Integer", "i64"))?;
366     Ok(num)
367 }
368