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