xref: /aosp_15_r20/system/secretkeeper/dice_policy/building/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 //! 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 ///    ```
209 ///    constraint_spec =[
210 ///     (ConstraintType::ExactMatch, vec![AUTHORITY_HASH]),
211 ///     (ConstraintType::GreaterOrEqual, vec![CONFIG_DESC, COMPONENT_NAME]),
212 ///    ];
213 ///    ```
214 ///
215 /// 2. For hypothetical (and highly simplified) dice chain:
216 ///
217 ///    ```
218 ///    [1, ROT_KEY, [{1 : 'a', 2 : {200 : 5, 201 : 'b'}}]]
219 ///    The following can be used
220 ///    constraint_spec =[
221 ///     ConstraintSpec(ConstraintType::ExactMatch, vec![1]),         // exact_matches value 'a'
222 ///     ConstraintSpec(ConstraintType::GreaterOrEqual, vec![2, 200]),// matches any value >= 5
223 ///    ];
224 ///    ```
policy_for_dice_chain( explicit_key_dice_chain: &[u8], mut constraint_spec: Vec<ConstraintSpec>, ) -> Result<DicePolicy, Error>225 pub fn policy_for_dice_chain(
226     explicit_key_dice_chain: &[u8],
227     mut constraint_spec: Vec<ConstraintSpec>,
228 ) -> Result<DicePolicy, Error> {
229     let dice_chain = deserialize_cbor_array(explicit_key_dice_chain)?;
230     check_is_explicit_key_dice_chain(&dice_chain)?;
231     let mut constraints_list: Vec<NodeConstraints> = Vec::with_capacity(dice_chain.len());
232     let chain_entries_len = dice_chain.len() - 2;
233     // Iterate on the reversed DICE chain! This is important because some constraint_spec
234     // (with `TargetEntry::ByType`) are applied on the last DiceChainEntry matching the spec.
235     let mut it = dice_chain.into_iter().rev();
236     for i in 0..chain_entries_len {
237         let entry = payload_value_from_cose_sign(it.next().unwrap())
238             .map_err(|e| format!("Unable to get Cose payload at pos {i} from end: {e:?}"))?;
239         constraints_list.push(constraints_on_dice_node(entry, &mut constraint_spec).map_err(
240             |e| format!("Unable to get constraints for payload at {i} from end: {e:?}"),
241         )?);
242     }
243 
244     // 1st & 2nd dice node of Explicit-key DiceCertChain format are
245     // EXPLICIT_KEY_DICE_CERT_CHAIN_VERSION & DiceCertChainInitialPayload.
246     constraints_list.push(NodeConstraints(Box::new([Constraint::new(
247         ConstraintType::ExactMatch as u16,
248         Vec::new(),
249         it.next().unwrap(),
250     )?])));
251 
252     constraints_list.push(NodeConstraints(Box::new([Constraint::new(
253         ConstraintType::ExactMatch as u16,
254         Vec::new(),
255         it.next().unwrap(),
256     )?])));
257 
258     constraints_list.reverse();
259     Ok(DicePolicy {
260         version: DICE_POLICY_VERSION,
261         node_constraints_list: constraints_list.into_boxed_slice(),
262     })
263 }
264 
265 // Take the ['node_payload'] of a dice node & construct the [`NodeConstraints`] on it. If the value
266 // corresponding to the a [`constraint_spec`] is not present in payload & iff it is marked
267 // `MissingAction::Ignore`, the corresponding constraint will be missing from the NodeConstraints.
268 // 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>269 fn constraints_on_dice_node(
270     node_payload: Value,
271     constraint_spec: &mut Vec<ConstraintSpec>,
272 ) -> Result<NodeConstraints, Error> {
273     let mut node_constraints: Vec<Constraint> = Vec::new();
274     let constraint_spec_with_retention_marker =
275         constraint_spec.iter().map(|c| (c.clone(), is_target_node(&node_payload, c)));
276 
277     for (constraint_item, is_target) in constraint_spec_with_retention_marker.clone() {
278         if !is_target {
279             continue;
280         }
281         // Some constraint spec may have wildcard entries, expand those!
282         for constraint_item_expanded in constraint_item.expand(&node_payload) {
283             let constraint_item_expanded = constraint_item_expanded?;
284             if let Some(constraint) =
285                 constraint_on_dice_node(&node_payload, &constraint_item_expanded)?
286             {
287                 node_constraints.push(constraint);
288             }
289         }
290     }
291 
292     *constraint_spec = constraint_spec_with_retention_marker
293         .filter_map(|(c, is_target)| if is_target && c.use_once() { None } else { Some(c) })
294         .collect();
295     Ok(NodeConstraints(node_constraints.into_boxed_slice()))
296 }
297 
constraint_on_dice_node( node_payload: &Value, constraint_spec: &ConstraintSpec, ) -> Result<Option<Constraint>, Error>298 fn constraint_on_dice_node(
299     node_payload: &Value,
300     constraint_spec: &ConstraintSpec,
301 ) -> Result<Option<Constraint>, Error> {
302     if constraint_spec.path.is_empty() {
303         return Err("Expected non-empty key spec".to_string());
304     }
305     let constraint = match lookup_in_nested_container(node_payload, &constraint_spec.path) {
306         Ok(Some(val)) => Some(Constraint::new(
307             constraint_spec.constraint_type as u16,
308             constraint_spec.path.to_vec(),
309             val,
310         )?),
311         Ok(None) => {
312             if constraint_spec.if_path_missing == MissingAction::Ignore {
313                 log::warn!("Value not found for {:?}, - skipping!", constraint_spec.path);
314                 None
315             } else {
316                 return Err(format!(
317                     "Value not found for : {:?}, constraint\
318                      spec is marked to fail on missing path",
319                     constraint_spec.path
320                 ));
321             }
322         }
323         Err(e) => {
324             if constraint_spec.if_path_missing == MissingAction::Ignore {
325                 log::warn!(
326                     "Error ({e:?}) getting Value for {:?}, - skipping!",
327                     constraint_spec.path
328                 );
329                 None
330             } else {
331                 return Err(format!(
332                     "Error ({e:?}) getting Value for {:?}, constraint\
333                      spec is marked to fail on missing path",
334                     constraint_spec.path
335                 ));
336             }
337         }
338     };
339     Ok(constraint)
340 }
341