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 //! This library supports constructing Dice Policies on a Dice chains, enabling various ways to
18 //! specify the constraints. This adheres to the Dice Policy spec at DicePolicy.cddl & works with
19 //! the rust structs exported by libdice_policy.
20 
21 #![allow(missing_docs)] // Sadly needed due to use of enumn::N
22 
23 use ciborium::Value;
24 use dice_policy::{
25     check_is_explicit_key_dice_chain, deserialize_cbor_array, lookup_in_nested_container,
26     payload_value_from_cose_sign, Constraint, DicePolicy, NodeConstraints, DICE_POLICY_VERSION,
27     EXACT_MATCH_CONSTRAINT, GREATER_OR_EQUAL_CONSTRAINT,
28 };
29 use enumn::N;
30 use itertools::Either;
31 use std::borrow::Cow;
32 use std::iter::once;
33 
34 type Error = String;
35 
36 /// Supported version of dice policy spec.
37 pub const SUPPORTED_DICE_POLICY_VERSION: u64 = 1;
38 /// Wildcard key to specify all indexes of an array in a nested container. The library only
39 /// supports 1 Wildcard key per `ConstraintSpec`.
40 pub const WILDCARD_FULL_ARRAY: i64 = -1;
41 
42 const CONFIG_DESC: i64 = -4670548;
43 const COMPONENT_NAME: i64 = -70002;
44 const PATH_TO_COMPONENT_NAME: [i64; 2] = [CONFIG_DESC, COMPONENT_NAME];
45 
46 /// Constraint Types supported in Dice policy.
47 #[repr(u16)]
48 #[non_exhaustive]
49 #[derive(Clone, Copy, Debug, PartialEq, N)]
50 pub enum ConstraintType {
51     /// Enforce exact match criteria, indicating the policy should match
52     /// if the dice chain has exact same specified values.
53     ExactMatch = EXACT_MATCH_CONSTRAINT,
54     /// Enforce Greater than or equal to criteria. When applied on security_version, this
55     /// can be useful to set policy that matches dice chains with same or upgraded images.
56     GreaterOrEqual = GREATER_OR_EQUAL_CONSTRAINT,
57 }
58 
59 /// ConstraintSpec is used to specify which constraint type to apply and on which all paths in a
60 /// DiceChainEntry.
61 /// See documentation of `policy_for_dice_chain()` for examples.
62 #[derive(Clone)]
63 pub struct ConstraintSpec {
64     constraint_type: ConstraintType,
65     // `path` is essentially a series of indexes (of map keys or array positions) to locate a
66     // particular value nested inside a DiceChainEntry.
67     path: Vec<i64>,
68     // If set to `Ignore`, the constraint is skipped if the `path` is missing in a
69     // DiceChainEntry while constructing a policy. Note that this does not affect policy
70     // matching.
71     if_path_missing: MissingAction,
72     // Which DiceChainEntry to apply the constraint on.
73     target_entry: TargetEntry,
74 }
75 
76 impl ConstraintSpec {
77     /// Constraint the value corresponding to `path`. `constraint_type` specifies how and
78     /// `if_path_missing` specifies what to do if the `path` is missing in a DiceChainEntry.
new( constraint_type: ConstraintType, path: Vec<i64>, if_path_missing: MissingAction, target_entry: TargetEntry, ) -> Self79     pub fn new(
80         constraint_type: ConstraintType,
81         path: Vec<i64>,
82         if_path_missing: MissingAction,
83         target_entry: TargetEntry,
84     ) -> Self {
85         Self { constraint_type, path, if_path_missing, target_entry }
86     }
87 
has_wildcard_entry(&self) -> bool88     fn has_wildcard_entry(&self) -> bool {
89         self.path.contains(&WILDCARD_FULL_ARRAY)
90     }
91 
92     // Expand the ConstrainSpec into an iterable list. This expand to 1 or more specs depending
93     // on presence of Wildcard entry.
expand( &self, node_payload: &Value, ) -> impl Iterator<Item = Result<Cow<ConstraintSpec>, Error>>94     fn expand(
95         &self,
96         node_payload: &Value,
97     ) -> impl Iterator<Item = Result<Cow<ConstraintSpec>, Error>> {
98         if self.has_wildcard_entry() {
99             expand_wildcard_entry(self, node_payload).map_or_else(
100                 |e| Either::Left(once(Err(e))),
101                 |cs| Either::Right(cs.into_iter().map(|c| Ok(Cow::Owned(c)))),
102             )
103         } else {
104             Either::Left(once(Ok(Cow::Borrowed(self))))
105         }
106     }
107 
use_once(&self) -> bool108     fn use_once(&self) -> bool {
109         matches!(self.target_entry, TargetEntry::ByName(_))
110     }
111 }
112 
113 /// Indicates the DiceChainEntry in the chain. Note this covers only DiceChainEntries in
114 /// ExplicitKeyDiceCertChain, ie, this does not include the first 2 dice nodes.
115 #[derive(Clone, PartialEq, Eq, Debug)]
116 pub enum TargetEntry {
117     /// All Entries
118     All,
119     /// Specify a particular DiceChain entry by component name. Lookup is done from the end of the
120     /// chain, i.e., the last entry with given component_name will be targeted.
121     ByName(String),
122 }
123 
124 // Extract the component name from the DiceChainEntry, if present.
try_extract_component_name(node_payload: &Value) -> Option<String>125 fn try_extract_component_name(node_payload: &Value) -> Option<String> {
126     let component_name = lookup_in_nested_container(node_payload, &PATH_TO_COMPONENT_NAME)
127         .unwrap_or_else(|e| {
128             log::warn!("Lookup for component_name in the node failed {e:?}- ignoring!");
129             None
130         })?;
131     component_name.into_text().ok()
132 }
133 
is_target_node(node_payload: &Value, spec: &ConstraintSpec) -> bool134 fn is_target_node(node_payload: &Value, spec: &ConstraintSpec) -> bool {
135     match &spec.target_entry {
136         TargetEntry::All => true,
137         TargetEntry::ByName(name) => {
138             Some(name) == try_extract_component_name(node_payload).as_ref()
139         }
140     }
141 }
142 
143 // Position of the first index marked `WILDCARD_FULL_ARRAY` in path.
wildcard_position(path: &[i64]) -> Result<usize, Error>144 fn wildcard_position(path: &[i64]) -> Result<usize, Error> {
145     let Some(pos) = path.iter().position(|&key| key == WILDCARD_FULL_ARRAY) else {
146         return Err("InvalidInput: Path does not have wildcard key".to_string());
147     };
148     Ok(pos)
149 }
150 
151 // Size of array at specified `path` in the nested container.
size_of_array_in_nested_container(container: &Value, path: &[i64]) -> Result<usize, Error>152 fn size_of_array_in_nested_container(container: &Value, path: &[i64]) -> Result<usize, Error> {
153     if let Some(Value::Array(array)) = lookup_in_nested_container(container, path)? {
154         Ok(array.len())
155     } else {
156         Err("InternalError: Expected nested array not found".to_string())
157     }
158 }
159 
160 // Expand `ConstraintSpec` containing `WILDCARD_FULL_ARRAY` to entries with path covering each
161 // array index.
expand_wildcard_entry( spec: &ConstraintSpec, target_node: &Value, ) -> Result<Vec<ConstraintSpec>, Error>162 fn expand_wildcard_entry(
163     spec: &ConstraintSpec,
164     target_node: &Value,
165 ) -> Result<Vec<ConstraintSpec>, Error> {
166     let pos = wildcard_position(&spec.path)?;
167     // Notice depth of the array in nested container is same as the position in path.
168     let size = size_of_array_in_nested_container(target_node, &spec.path[..pos])?;
169     Ok((0..size as i64).map(|i| replace_key(spec, pos, i)).collect())
170 }
171 
172 // Get a ConstraintSpec same as `spec` except for changing the key at `pos` in its `path`
173 // This method panics if `pos` in out of index of path.
replace_key(spec: &ConstraintSpec, pos: usize, new_key: i64) -> ConstraintSpec174 fn replace_key(spec: &ConstraintSpec, pos: usize, new_key: i64) -> ConstraintSpec {
175     let mut spec: ConstraintSpec = spec.clone();
176     spec.path[pos] = new_key;
177     spec
178 }
179 
180 /// Used to define a `ConstraintSpec` requirement. This allows client to set behavior of
181 /// policy construction in case the `path` is missing in a DiceChainEntry.
182 #[derive(Clone, Eq, PartialEq)]
183 pub enum MissingAction {
184     /// Indicates that a constraint `path` is required in each DiceChainEntry. Fail if missing!
185     Fail,
186     /// Indicates that a constraint `path` may be missing in some DiceChainEntry. Ignore!
187     Ignore,
188 }
189 
190 /// Construct a dice policy on a given dice chain.
191 /// This can be used by clients to construct a policy to seal secrets.
192 /// Constraints on all but (first & second) dice nodes is applied using constraint_spec
193 /// argument. For the first and second node (version and root public key), the constraint is
194 /// ExactMatch of the whole node.
195 ///
196 /// # Arguments
197 /// `dice_chain`: The serialized CBOR encoded Dice chain, adhering to Explicit-key
198 /// DiceCertChain format. See definition at ExplicitKeyDiceCertChain.cddl
199 ///
200 /// `constraint_spec`: List of constraints to be applied on dice node.
201 /// Each constraint is a ConstraintSpec object.
202 ///
203 /// Note: Dice node is treated as a nested structure (map or array) (& so the lookup is done in
204 /// that fashion).
205 ///
206 /// Examples of constraint_spec:
207 ///  1. For exact_match on auth_hash & greater_or_equal on security_version
208 ///    constraint_spec =[
209 ///     (ConstraintType::ExactMatch, vec![AUTHORITY_HASH]),
210 ///     (ConstraintType::GreaterOrEqual, vec![CONFIG_DESC, COMPONENT_NAME]),
211 ///    ];
212 ///
213 /// 2. For hypothetical (and highly simplified) dice chain:
214 ///
215 ///    [1, ROT_KEY, [{1 : 'a', 2 : {200 : 5, 201 : 'b'}}]]
216 ///    The following can be used
217 ///    constraint_spec =[
218 ///     ConstraintSpec(ConstraintType::ExactMatch, vec![1]),         // exact_matches value 'a'
219 ///     ConstraintSpec(ConstraintType::GreaterOrEqual, vec![2, 200]),// matches any value >= 5
220 ///    ];
policy_for_dice_chain( explicit_key_dice_chain: &[u8], mut constraint_spec: Vec<ConstraintSpec>, ) -> Result<DicePolicy, Error>221 pub fn policy_for_dice_chain(
222     explicit_key_dice_chain: &[u8],
223     mut constraint_spec: Vec<ConstraintSpec>,
224 ) -> Result<DicePolicy, Error> {
225     let dice_chain = deserialize_cbor_array(explicit_key_dice_chain)?;
226     check_is_explicit_key_dice_chain(&dice_chain)?;
227     let mut constraints_list: Vec<NodeConstraints> = Vec::with_capacity(dice_chain.len());
228     let chain_entries_len = dice_chain.len() - 2;
229     // Iterate on the reversed DICE chain! This is important because some constraint_spec
230     // (with `TargetEntry::ByType`) are applied on the last DiceChainEntry matching the spec.
231     let mut it = dice_chain.into_iter().rev();
232     for i in 0..chain_entries_len {
233         let entry = payload_value_from_cose_sign(it.next().unwrap())
234             .map_err(|e| format!("Unable to get Cose payload at pos {i} from end: {e:?}"))?;
235         constraints_list.push(constraints_on_dice_node(entry, &mut constraint_spec).map_err(
236             |e| format!("Unable to get constraints for payload at {i} from end: {e:?}"),
237         )?);
238     }
239 
240     // 1st & 2nd dice node of Explicit-key DiceCertChain format are
241     // EXPLICIT_KEY_DICE_CERT_CHAIN_VERSION & DiceCertChainInitialPayload.
242     constraints_list.push(NodeConstraints(Box::new([Constraint::new(
243         ConstraintType::ExactMatch as u16,
244         Vec::new(),
245         it.next().unwrap(),
246     )?])));
247 
248     constraints_list.push(NodeConstraints(Box::new([Constraint::new(
249         ConstraintType::ExactMatch as u16,
250         Vec::new(),
251         it.next().unwrap(),
252     )?])));
253 
254     constraints_list.reverse();
255     Ok(DicePolicy {
256         version: DICE_POLICY_VERSION,
257         node_constraints_list: constraints_list.into_boxed_slice(),
258     })
259 }
260 
261 // Take the ['node_payload'] of a dice node & construct the [`NodeConstraints`] on it. If the value
262 // corresponding to the a [`constraint_spec`] is not present in payload & iff it is marked
263 // `MissingAction::Ignore`, the corresponding constraint will be missing from the NodeConstraints.
264 // Not all constraint_spec applies to all DiceChainEntries, see `TargetEntry::ByName`.
constraints_on_dice_node( node_payload: Value, constraint_spec: &mut Vec<ConstraintSpec>, ) -> Result<NodeConstraints, Error>265 fn constraints_on_dice_node(
266     node_payload: Value,
267     constraint_spec: &mut Vec<ConstraintSpec>,
268 ) -> Result<NodeConstraints, Error> {
269     let mut node_constraints: Vec<Constraint> = Vec::new();
270     let constraint_spec_with_retention_marker =
271         constraint_spec.iter().map(|c| (c.clone(), is_target_node(&node_payload, c)));
272 
273     for (constraint_item, is_target) in constraint_spec_with_retention_marker.clone() {
274         if !is_target {
275             continue;
276         }
277         // Some constraint spec may have wildcard entries, expand those!
278         for constraint_item_expanded in constraint_item.expand(&node_payload) {
279             let constraint_item_expanded = constraint_item_expanded?;
280             if let Some(constraint) =
281                 constraint_on_dice_node(&node_payload, &constraint_item_expanded)?
282             {
283                 node_constraints.push(constraint);
284             }
285         }
286     }
287 
288     *constraint_spec = constraint_spec_with_retention_marker
289         .filter_map(|(c, is_target)| if is_target && c.use_once() { None } else { Some(c) })
290         .collect();
291     Ok(NodeConstraints(node_constraints.into_boxed_slice()))
292 }
293 
constraint_on_dice_node( node_payload: &Value, constraint_spec: &ConstraintSpec, ) -> Result<Option<Constraint>, Error>294 fn constraint_on_dice_node(
295     node_payload: &Value,
296     constraint_spec: &ConstraintSpec,
297 ) -> Result<Option<Constraint>, Error> {
298     if constraint_spec.path.is_empty() {
299         return Err("Expected non-empty key spec".to_string());
300     }
301     let constraint = match lookup_in_nested_container(node_payload, &constraint_spec.path) {
302         Ok(Some(val)) => Some(Constraint::new(
303             constraint_spec.constraint_type as u16,
304             constraint_spec.path.to_vec(),
305             val,
306         )?),
307         Ok(None) => {
308             if constraint_spec.if_path_missing == MissingAction::Ignore {
309                 log::warn!("Value not found for {:?}, - skipping!", constraint_spec.path);
310                 None
311             } else {
312                 return Err(format!(
313                     "Value not found for : {:?}, constraint\
314                      spec is marked to fail on missing path",
315                     constraint_spec.path
316                 ));
317             }
318         }
319         Err(e) => {
320             if constraint_spec.if_path_missing == MissingAction::Ignore {
321                 log::warn!(
322                     "Error ({e:?}) getting Value for {:?}, - skipping!",
323                     constraint_spec.path
324                 );
325                 None
326             } else {
327                 return Err(format!(
328                     "Error ({e:?}) getting Value for {:?}, constraint\
329                      spec is marked to fail on missing path",
330                     constraint_spec.path
331                 ));
332             }
333         }
334     };
335     Ok(constraint)
336 }
337