Dice Policy Aware authentication: Rust library
Dice policies are to be used by pVMs to seal the secrets in
Secretkeeper. The policies are such that only pVM with certain dice
chains can access the secrets. The constraints will be set by pVM (see
`constraint_spec` argument).
This patch introduces libdice_policy required for managing dice
policies. In particular, we write fn - `from_dice_chain()` which can be
used by client to construct appropriate policy out of dice chains.
Also includes unit tests.
Note on Trunkstable feature flagging: This patch creates a library, but
the lib is not used by any module/target that is included on device &
hence is no-op as far as feature flagging is concerned.
Test: atest libdice_policy.test
Bug: 291233378
Bug: 291238565
Change-Id: I32b78cefd77a9fd1f62800fd15569aea912f60bd
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 3bc7aba..171389b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -28,6 +28,9 @@
},
{
"name": "initrd_bootconfig.test"
+ },
+ {
+ "name": "libdice_policy.test"
}
],
"avf-postsubmit": [
diff --git a/libs/dice_policy/Android.bp b/libs/dice_policy/Android.bp
new file mode 100644
index 0000000..a7ac5b9
--- /dev/null
+++ b/libs/dice_policy/Android.bp
@@ -0,0 +1,35 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "libdice_policy.defaults",
+ crate_name: "dice_policy",
+ defaults: ["avf_build_flags_rust"],
+ srcs: ["src/lib.rs"],
+ edition: "2021",
+ prefer_rlib: true,
+ rustlibs: [
+ "libanyhow",
+ "libciborium",
+ "libcoset",
+ ],
+}
+
+rust_library {
+ name: "libdice_policy",
+ defaults: ["libdice_policy.defaults"],
+}
+
+rust_test {
+ name: "libdice_policy.test",
+ defaults: [
+ "libdice_policy.defaults",
+ "rdroidtest.defaults",
+ ],
+ test_suites: ["general-tests"],
+ rustlibs: [
+ "librustutils",
+ "libscopeguard",
+ ],
+}
diff --git a/libs/dice_policy/src/lib.rs b/libs/dice_policy/src/lib.rs
new file mode 100644
index 0000000..f5d117c
--- /dev/null
+++ b/libs/dice_policy/src/lib.rs
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//! A “DICE policy” is a format for setting constraints on a DICE chain. A DICE chain policy
+//! verifier takes a policy and a DICE chain, and returns a boolean indicating whether the
+//! DICE chain meets the constraints set out on a policy.
+//!
+//! This forms the foundation of Dice Policy aware Authentication (DPA-Auth), where the server
+//! authenticates a client by comparing its dice chain against a set policy.
+//!
+//! Another use is "sealing", where clients can use an appropriately constructed dice policy to
+//! seal a secret. Unsealing is only permitted if dice chain of the component requesting unsealing
+//! complies with the policy.
+//!
+//! A typical policy will assert things like:
+//! # DK_pub must have this value
+//! # The DICE chain must be exactly five certificates long
+//! # authorityHash in the third certificate must have this value
+//! securityVersion in the fourth certificate must be an integer greater than 8
+//!
+//! These constraints used to express policy are (for now) limited to following 2 types:
+//! 1. Exact Match: useful for enforcing rules like authority hash should be exactly equal.
+//! 2. Greater than or equal to: Useful for setting policies that seal
+//! Anti-rollback protected entities (should be accessible to versions >= present).
+//!
+//! Dice Policy CDDL:
+//!
+//! dicePolicy = [
+//! 1, ; dice policy version
+//! + nodeConstraintList ; for each entry in dice chain
+//! ]
+//!
+//! nodeConstraintList = [
+//! * nodeConstraint
+//! ]
+//!
+//! ; We may add a hashConstraint item later
+//! nodeConstraint = exactMatchConstraint / geConstraint
+//!
+//! exactMatchConstraint = [1, keySpec, value]
+//! geConstraint = [2, keySpec, int]
+//!
+//! keySpec = [value+]
+//!
+//! value = bool / int / tstr / bstr
+
+use anyhow::{anyhow, bail, Context, Result};
+use ciborium::Value;
+use coset::{AsCborValue, CoseSign1};
+use std::borrow::Cow;
+
+const DICE_POLICY_VERSION: u64 = 1;
+
+/// Constraint Types supported in Dice policy.
+#[non_exhaustive]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum ConstraintType {
+ /// Enforce exact match criteria, indicating the policy should match
+ /// if the dice chain has exact same specified values.
+ ExactMatch = 1,
+ /// Enforce Greater than or equal to criteria. When applied on security_version, this
+ /// can be useful to set policy that matches dice chains with same or upgraded images.
+ GreaterOrEqual = 2,
+}
+
+/// ConstraintSpec is used to specify which constraint type to apply and
+/// on which all entries in a dice node.
+/// See documentation of `from_dice_chain()` for examples.
+pub struct ConstraintSpec {
+ constraint_type: ConstraintType,
+ // path is essentially a list of label/int.
+ // It identifies which entry (in a dice node) to be applying constraints on.
+ path: Vec<i64>,
+}
+
+impl ConstraintSpec {
+ /// Construct the ConstraintSpec.
+ pub fn new(constraint_type: ConstraintType, path: Vec<i64>) -> Result<Self> {
+ Ok(ConstraintSpec { constraint_type, path })
+ }
+}
+
+// TODO(b/291238565): Restrict (nested_)key & value type to (bool/int/tstr/bstr).
+// and maybe convert it into struct.
+/// Each constraint (on a dice node) is a tuple: (ConstraintType, constraint_path, value)
+#[derive(Debug, PartialEq)]
+struct Constraint(u16, Vec<i64>, Value);
+
+/// List of all constraints on a dice node.
+#[derive(Debug, PartialEq)]
+struct NodeConstraints(Box<[Constraint]>);
+
+/// Module for working with dice policy.
+#[derive(Debug, PartialEq)]
+pub struct DicePolicy {
+ version: u64,
+ node_constraints_list: Box<[NodeConstraints]>, // Constraint on each entry in dice chain.
+}
+
+impl DicePolicy {
+ /// Construct a dice policy from a given dice chain.
+ /// This can be used by clients to construct a policy to seal secrets.
+ /// Constraints on all but first dice node is applied using constraint_spec argument.
+ /// For the first node (which is a ROT key), the constraint is ExactMatch of the whole node.
+ ///
+ /// # Arguments
+ /// `dice_chain`: The serialized CBOR encoded Dice chain, adhering to Android Profile for DICE.
+ /// https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/android.md
+ ///
+ /// `constraint_spec`: List of constraints to be applied on dice node.
+ /// Each constraint is a ConstraintSpec object.
+ ///
+ /// Note: Dice node is treated as a nested map (& so the lookup is done in that fashion).
+ ///
+ /// Examples of constraint_spec:
+ /// 1. For exact_match on auth_hash & greater_or_equal on security_version
+ /// constraint_spec =[
+ /// (ConstraintType::ExactMatch, vec![AUTHORITY_HASH]),
+ /// (ConstraintType::GreaterOrEqual, vec![CONFIG_DESC, COMPONENT_NAME]),
+ /// ];
+ ///
+ /// 2. For hypothetical (and highly simplified) dice chain:
+ /// [ROT_KEY, [{1 : 'a', 2 : {200 : 5, 201 : 'b'}}]]
+ /// The following can be used
+ /// constraint_spec =[
+ /// ConstraintSpec(ConstraintType::ExactMatch, vec![1]), // exact_matches value 'a'
+ /// ConstraintSpec(ConstraintType::GreaterOrEqual, vec![2, 200]),// matches any value >= 5
+ /// ];
+ pub fn from_dice_chain(dice_chain: &[u8], constraint_spec: &[ConstraintSpec]) -> Result<Self> {
+ // TODO(b/298217847): Check if the given dice chain adheres to Explicit-key DiceCertChain
+ // format and if not, convert it before policy construction.
+ let dice_chain = value_from_bytes(dice_chain).context("Unable to decode top-level CBOR")?;
+ let dice_chain = match dice_chain {
+ Value::Array(array) if array.len() >= 2 => array,
+ _ => bail!("Expected an array of at least length 2, found: {:?}", dice_chain),
+ };
+ let mut constraints_list: Vec<NodeConstraints> = Vec::with_capacity(dice_chain.len());
+ let mut it = dice_chain.into_iter();
+
+ constraints_list.push(NodeConstraints(Box::new([Constraint(
+ ConstraintType::ExactMatch as u16,
+ Vec::new(),
+ it.next().unwrap(),
+ )])));
+
+ for (n, value) in it.enumerate() {
+ let entry = cbor_value_from_cose_sign(value)
+ .with_context(|| format!("Unable to get Cose payload at: {}", n))?;
+ constraints_list.push(payload_to_constraints(entry, constraint_spec)?);
+ }
+
+ Ok(DicePolicy {
+ version: DICE_POLICY_VERSION,
+ node_constraints_list: constraints_list.into_boxed_slice(),
+ })
+ }
+}
+
+// Take the payload of a dice node & construct the constraints on it.
+fn payload_to_constraints(
+ payload: Value,
+ constraint_spec: &[ConstraintSpec],
+) -> Result<NodeConstraints> {
+ let mut node_constraints: Vec<Constraint> = Vec::new();
+ for constraint_item in constraint_spec {
+ let constraint_path = constraint_item.path.to_vec();
+ if constraint_path.is_empty() {
+ bail!("Expected non-empty key spec");
+ }
+ let val = lookup_value_in_nested_map(&payload, &constraint_path)
+ .context(format!("Value not found for constraint_path {:?}", constraint_path))?;
+ let constraint = Constraint(constraint_item.constraint_type as u16, constraint_path, val);
+ node_constraints.push(constraint);
+ }
+ Ok(NodeConstraints(node_constraints.into_boxed_slice()))
+}
+
+// Lookup value corresponding to constraint path in nested map.
+// This function recursively calls itself.
+// The depth of recursion is limited by the size of constraint_path.
+fn lookup_value_in_nested_map(cbor_map: &Value, constraint_path: &[i64]) -> Result<Value> {
+ if constraint_path.is_empty() {
+ return Ok(cbor_map.clone());
+ }
+ let explicit_map = get_map_from_value(cbor_map)?;
+ let val = lookup_value_in_map(&explicit_map, constraint_path[0])
+ .ok_or(anyhow!("Value not found for constraint key: {:?}", constraint_path[0]))?;
+ lookup_value_in_nested_map(val, &constraint_path[1..])
+}
+
+fn get_map_from_value(cbor_map: &Value) -> Result<Cow<Vec<(Value, Value)>>> {
+ match cbor_map {
+ Value::Bytes(b) => value_from_bytes(b)?
+ .into_map()
+ .map(Cow::Owned)
+ .map_err(|e| anyhow!("Expected a cbor map: {:?}", e)),
+ Value::Map(map) => Ok(Cow::Borrowed(map)),
+ _ => bail!("/Expected a cbor map {:?}", cbor_map),
+ }
+}
+
+fn lookup_value_in_map(map: &[(Value, Value)], key: i64) -> Option<&Value> {
+ let key = Value::Integer(key.into());
+ for (k, v) in map.iter() {
+ if k == &key {
+ return Some(v);
+ }
+ }
+ None
+}
+
+/// Extract the payload from the COSE Sign
+fn cbor_value_from_cose_sign(cbor: Value) -> Result<Value> {
+ let sign1 =
+ CoseSign1::from_cbor_value(cbor).map_err(|e| anyhow!("Error extracting CoseKey: {}", e))?;
+ match sign1.payload {
+ None => bail!("Missing payload"),
+ Some(payload) => Ok(value_from_bytes(&payload)?),
+ }
+}
+
+/// Decodes the provided binary CBOR-encoded value and returns a
+/// ciborium::Value struct wrapped in Result.
+fn value_from_bytes(mut bytes: &[u8]) -> Result<Value> {
+ let value = ciborium::de::from_reader(&mut bytes)?;
+ // Ciborium tries to read one Value, & doesn't care if there is trailing data after it. We do.
+ if !bytes.is_empty() {
+ bail!("Unexpected trailing data while converting to CBOR value");
+ }
+ Ok(value)
+}
+
+#[cfg(test)]
+rdroidtest::test_main!();
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use ciborium::cbor;
+ use coset::{CoseKey, Header, ProtectedHeader};
+ use rdroidtest::test;
+
+ const AUTHORITY_HASH: i64 = -4670549;
+ const CONFIG_DESC: i64 = -4670548;
+ const COMPONENT_NAME: i64 = -70002;
+ const KEY_MODE: i64 = -4670551;
+
+ // This is the number of certs in compos bcc (including the first ROT)
+ // To analyze a bcc use hwtrust tool from /tools/security/remote_provisioning/hwtrust
+ // `hwtrust --verbose dice-chain [path]/composbcc`
+ const COMPOS_DICE_CHAIN_SIZE: usize = 5;
+ const EXAMPLE_STRING: &str = "testing_dice_policy";
+ const EXAMPLE_NUM: i64 = 59765;
+
+ test!(policy_dice_size_is_same);
+ fn policy_dice_size_is_same() {
+ let input_dice = include_bytes!("../testdata/composbcc");
+ let constraint_spec = [
+ ConstraintSpec::new(ConstraintType::ExactMatch, vec![AUTHORITY_HASH]).unwrap(),
+ ConstraintSpec::new(ConstraintType::ExactMatch, vec![KEY_MODE]).unwrap(),
+ ConstraintSpec::new(ConstraintType::GreaterOrEqual, vec![CONFIG_DESC, COMPONENT_NAME])
+ .unwrap(),
+ ];
+ let policy = DicePolicy::from_dice_chain(input_dice, &constraint_spec).unwrap();
+ assert_eq!(policy.node_constraints_list.len(), COMPOS_DICE_CHAIN_SIZE);
+ }
+
+ test!(policy_structure_check);
+ fn policy_structure_check() {
+ let rot_key = CoseKey::default().to_cbor_value().unwrap();
+ let nested_payload = cbor!({
+ 100 => EXAMPLE_NUM
+ })
+ .unwrap();
+ let payload = cbor!({
+ 1 => EXAMPLE_STRING,
+ 2 => "some_other_example_string",
+ 3 => Value::Bytes(value_to_bytes(&nested_payload).unwrap()),
+ })
+ .unwrap();
+ let payload = value_to_bytes(&payload).unwrap();
+ let dice_node = CoseSign1 {
+ protected: ProtectedHeader::default(),
+ unprotected: Header::default(),
+ payload: Some(payload),
+ signature: b"ddef".to_vec(),
+ }
+ .to_cbor_value()
+ .unwrap();
+ let input_dice = Value::Array([rot_key.clone(), dice_node].to_vec());
+
+ let input_dice = value_to_bytes(&input_dice).unwrap();
+ let constraint_spec = [
+ ConstraintSpec::new(ConstraintType::ExactMatch, vec![1]).unwrap(),
+ ConstraintSpec::new(ConstraintType::GreaterOrEqual, vec![3, 100]).unwrap(),
+ ];
+ let policy = DicePolicy::from_dice_chain(&input_dice, &constraint_spec).unwrap();
+
+ // Assert policy is exactly as expected!
+ assert_eq!(
+ policy,
+ DicePolicy {
+ version: 1,
+ node_constraints_list: Box::new([
+ NodeConstraints(Box::new([Constraint(
+ ConstraintType::ExactMatch as u16,
+ vec![],
+ rot_key
+ )])),
+ NodeConstraints(Box::new([
+ Constraint(
+ ConstraintType::ExactMatch as u16,
+ vec![1],
+ Value::Text(EXAMPLE_STRING.to_string())
+ ),
+ Constraint(
+ ConstraintType::GreaterOrEqual as u16,
+ vec![3, 100],
+ Value::from(EXAMPLE_NUM)
+ )
+ ])),
+ ])
+ }
+ );
+ }
+
+ /// Encodes a ciborium::Value into bytes.
+ fn value_to_bytes(value: &Value) -> Result<Vec<u8>> {
+ let mut bytes: Vec<u8> = Vec::new();
+ ciborium::ser::into_writer(&value, &mut bytes)?;
+ Ok(bytes)
+ }
+}
diff --git a/libs/dice_policy/testdata/composbcc b/libs/dice_policy/testdata/composbcc
new file mode 100644
index 0000000..fb3e006
--- /dev/null
+++ b/libs/dice_policy/testdata/composbcc
Binary files differ