blob: f5d117c86ac45c5a86e65403332436aa6a8e833d [file] [log] [blame]
/*
* 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)
}
}