blob: 07f76057ca7352e3591b91781e7552451a54e17f [file] [log] [blame]
use super::cose_key_from_cbor_value;
use super::profile::{ComponentVersionType, ModeType, Profile};
use crate::cbor::{field_value::FieldValue, value_from_bytes};
use crate::dice::{
ComponentVersion, ConfigDesc, ConfigDescBuilder, DiceMode, Payload, PayloadBuilder,
ProfileVersion,
};
use crate::publickey::PublicKey;
use crate::session::Session;
use anyhow::{anyhow, bail, ensure, Context, Result};
use ciborium::value::Value;
use coset::{AsCborValue, CoseSign1};
use openssl::sha::{sha256, sha384, sha512};
use std::collections::hash_map::Entry::{Occupied, Vacant};
use std::collections::HashMap;
use std::str::FromStr;
const ISS: i64 = 1;
const SUB: i64 = 2;
const CODE_HASH: i64 = -4670545;
const CODE_DESC: i64 = -4670546;
const CONFIG_HASH: i64 = -4670547;
const CONFIG_DESC: i64 = -4670548;
const AUTHORITY_HASH: i64 = -4670549;
const AUTHORITY_DESC: i64 = -4670550;
const MODE: i64 = -4670551;
const SUBJECT_PUBLIC_KEY: i64 = -4670552;
const KEY_USAGE: i64 = -4670553;
const PROFILE_NAME: i64 = -4670554;
const CONFIG_DESC_RESERVED_MAX: i64 = -70000;
const CONFIG_DESC_RESERVED_MIN: i64 = -70999;
const COMPONENT_NAME: i64 = -70002;
const COMPONENT_VERSION: i64 = -70003;
const RESETTABLE: i64 = -70004;
const SECURITY_VERSION: i64 = -70005;
pub(super) struct Entry {
payload: Vec<u8>,
}
impl Entry {
pub(super) fn verify_cbor_value(cbor: Value, key: &PublicKey) -> Result<Self> {
let sign1 = CoseSign1::from_cbor_value(cbor)
.context("Given CBOR does not appear to be a COSE_sign1")?;
key.verify_cose_sign1(&sign1, b"").context("cannot verify COSE_sign1")?;
match sign1.payload {
None => bail!("Missing payload"),
Some(payload) => Ok(Self { payload }),
}
}
pub(super) fn payload(&self) -> &[u8] {
&self.payload
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) enum ConfigFormat {
/// The configuration descriptor format specified by Android.
#[default]
Android,
/// The configuration descriptor format is either that specified by Android or is ignored.
AndroidOrIgnored,
}
impl Payload {
pub(super) fn from_cbor(
session: &Session,
bytes: &[u8],
config_format: ConfigFormat,
) -> Result<Self> {
let entries = cbor_map_from_slice(bytes)?;
let profile_version = PayloadFields::extract_profile_version(session, &entries)?;
Self::from_entries(&profile_version.into(), entries, config_format)
}
fn from_entries(
profile: &Profile,
entries: Vec<(Value, Value)>,
config_format: ConfigFormat,
) -> Result<Self> {
let f = PayloadFields::from_entries(profile, entries, config_format)?;
PayloadBuilder::with_subject_public_key(f.subject_public_key)
.issuer(f.issuer)
.subject(f.subject)
.mode(f.mode.ok_or_else(|| anyhow!("mode required"))?)
.code_desc(f.code_desc)
.code_hash(f.code_hash.ok_or_else(|| anyhow!("code hash required"))?)
.config_desc(f.config_desc.ok_or_else(|| anyhow!("config desc required"))?)
.config_hash(f.config_hash)
.authority_desc(f.authority_desc)
.authority_hash(f.authority_hash.ok_or_else(|| anyhow!("authority hash required"))?)
.build()
.context("building payload")
}
}
pub(super) struct PayloadFields {
pub(super) issuer: String,
pub(super) subject: String,
pub(super) subject_public_key: PublicKey,
mode: Option<DiceMode>,
code_desc: Option<Vec<u8>>,
code_hash: Option<Vec<u8>>,
config_desc: Option<ConfigDesc>,
config_hash: Option<Vec<u8>>,
authority_desc: Option<Vec<u8>>,
authority_hash: Option<Vec<u8>>,
}
impl PayloadFields {
pub(super) fn from_cbor(
session: &Session,
bytes: &[u8],
config_format: ConfigFormat,
) -> Result<Self> {
let entries = cbor_map_from_slice(bytes)?;
let profile_version = Self::extract_profile_version(session, &entries)?;
Self::from_entries(&profile_version.into(), entries, config_format)
}
fn extract_profile_version(
session: &Session,
entries: &[(Value, Value)],
) -> Result<ProfileVersion> {
let mut profile_name = FieldValue::new("profile name");
for (key, value) in entries.iter() {
if key == &Value::from(PROFILE_NAME) {
profile_name.set_once(value.clone())?;
}
}
let profile_version = match profile_name.into_optional_string()? {
None => {
let version = session.options.dice_profile_range.start();
ensure!(version <= ProfileVersion::Android14, "profile name is required");
version
}
Some(profile_name) => {
ProfileVersion::from_str(&profile_name).with_context(|| profile_name.clone())?
}
};
ensure!(
session.options.dice_profile_range.contains(profile_version),
"profile version \"{profile_version}\" is less than \"{}\" or greater than \"{}\"",
session.options.dice_profile_range.start(),
session.options.dice_profile_range.end(),
);
Ok(profile_version)
}
fn from_entries(
profile: &Profile,
entries: Vec<(Value, Value)>,
config_format: ConfigFormat,
) -> Result<Self> {
let mut issuer = FieldValue::new("issuer");
let mut subject = FieldValue::new("subject");
let mut subject_public_key = FieldValue::new("subject public key");
let mut mode = FieldValue::new("mode");
let mut code_desc = FieldValue::new("code desc");
let mut code_hash = FieldValue::new("code hash");
let mut config_desc = FieldValue::new("config desc");
let mut config_hash = FieldValue::new("config hash");
let mut authority_desc = FieldValue::new("authority desc");
let mut authority_hash = FieldValue::new("authority hash");
let mut key_usage = FieldValue::new("key usage");
let mut profile_name = FieldValue::new("profile name");
for (key, value) in entries.into_iter() {
if let Some(Ok(key)) = key.as_integer().map(TryInto::try_into) {
let field = match key {
ISS => &mut issuer,
SUB => &mut subject,
SUBJECT_PUBLIC_KEY => &mut subject_public_key,
MODE => &mut mode,
CODE_DESC => &mut code_desc,
CODE_HASH => &mut code_hash,
CONFIG_DESC => &mut config_desc,
CONFIG_HASH => &mut config_hash,
AUTHORITY_DESC => &mut authority_desc,
AUTHORITY_HASH => &mut authority_hash,
KEY_USAGE => &mut key_usage,
PROFILE_NAME => &mut profile_name,
_ => bail!("Unknown key {}", key),
};
field.set_once(value)?
} else {
bail!("Invalid key: {:?}", key);
}
}
validate_key_usage(profile, key_usage)?;
let (config_desc, config_hash) =
validate_config(profile, config_desc, config_hash, config_format).context("config")?;
Ok(Self {
issuer: issuer.into_string()?,
subject: subject.into_string()?,
subject_public_key: validate_subject_public_key(profile, subject_public_key)?,
mode: validate_mode(profile, mode)?,
code_desc: code_desc.into_optional_bytes()?,
code_hash: code_hash.into_optional_bytes()?,
config_desc,
config_hash,
authority_desc: authority_desc.into_optional_bytes()?,
authority_hash: authority_hash.into_optional_bytes()?,
})
}
}
fn validate_key_usage(profile: &Profile, key_usage: FieldValue) -> Result<()> {
let key_usage = key_usage.into_bytes().context("key usage")?;
let key_cert_sign = 1 << 5;
if key_usage.len() > 1
&& profile.allow_big_endian_key_usage
&& key_usage[key_usage.len() - 1] == key_cert_sign
&& key_usage.iter().take(key_usage.len() - 1).all(|&x| x == 0)
{
return Ok(());
}
if key_usage.is_empty()
|| key_usage[0] != key_cert_sign
|| !key_usage.iter().skip(1).all(|&x| x == 0)
{
bail!("key usage must only contain keyCertSign (bit 5)");
};
Ok(())
}
fn validate_subject_public_key(
profile: &Profile,
subject_public_key: FieldValue,
) -> Result<PublicKey> {
let subject_public_key = subject_public_key.into_bytes()?;
let subject_public_key = value_from_bytes(&subject_public_key).context("decode CBOR")?;
let subject_public_key = cose_key_from_cbor_value(subject_public_key, profile.key_ops_type)
.context("parsing subject public key")?;
PublicKey::from_cose_key(&subject_public_key)
.context("parsing subject public key from COSE_key")
}
fn validate_mode(profile: &Profile, mode: FieldValue) -> Result<Option<DiceMode>> {
Ok(if !mode.is_bytes() && profile.mode_type == ModeType::IntOrBytes {
mode.into_optional_i64()?
} else {
mode.into_optional_bytes()?
.map(|mode| {
if mode.len() != 1 {
bail!("Expected mode to be a single byte, actual byte count: {}", mode.len())
};
Ok(mode[0].into())
})
.transpose()?
}
.map(|mode| match mode {
1 => DiceMode::Normal,
2 => DiceMode::Debug,
3 => DiceMode::Recovery,
_ => DiceMode::NotConfigured,
}))
}
fn validate_config(
profile: &Profile,
config_desc: FieldValue,
config_hash: FieldValue,
config_format: ConfigFormat,
) -> Result<(Option<ConfigDesc>, Option<Vec<u8>>)> {
let config_desc = config_desc.into_optional_bytes()?;
let config_hash = config_hash.into_optional_bytes()?;
if let Some(config_desc) = config_desc {
let config = config_desc_from_slice(profile, &config_desc).context("parsing descriptor");
if config.is_err() && config_format == ConfigFormat::AndroidOrIgnored {
return Ok((Some(ConfigDesc::default()), config_hash));
}
if !profile.config_hash_unverified {
let Some(ref hash) = config_hash else { bail!("hash required") };
match hash.len() {
32 => ensure!(hash == &sha256(&config_desc)),
48 => ensure!(hash == &sha384(&config_desc)),
64 => ensure!(hash == &sha512(&config_desc)),
_ => bail!("unsupported hash size"),
};
}
Ok((Some(config?), config_hash))
} else {
Ok((None, config_hash))
}
}
fn cbor_map_from_slice(bytes: &[u8]) -> Result<Vec<(Value, Value)>> {
let value = value_from_bytes(bytes).context("Error parsing CBOR into a map")?;
let entries = match value {
Value::Map(entries) => entries,
_ => bail!("Not a map: {:?}", value),
};
Ok(entries)
}
fn config_desc_from_slice(profile: &Profile, bytes: &[u8]) -> Result<ConfigDesc> {
let entries = cbor_map_from_slice(bytes)?;
let mut component_name = FieldValue::new("component name");
let mut component_version = FieldValue::new("component version");
let mut resettable = FieldValue::new("resettable");
let mut security_version = FieldValue::new("security version");
let mut extensions = HashMap::new();
for (key, value) in entries.into_iter() {
if let Some(Ok(key)) = key.as_integer().map(TryInto::try_into) {
let field = match key {
COMPONENT_NAME => &mut component_name,
COMPONENT_VERSION => &mut component_version,
RESETTABLE => &mut resettable,
SECURITY_VERSION => &mut security_version,
key if (CONFIG_DESC_RESERVED_MIN..=CONFIG_DESC_RESERVED_MAX).contains(&key) => {
bail!("Reserved key {}", key);
}
_ => match extensions.entry(key) {
Vacant(entry) => {
entry.insert(value);
continue;
}
Occupied(entry) => {
bail!("Duplicate values for {}: {:?} and {:?}", key, entry.get(), value)
}
},
};
field.set_once(value)?
} else {
bail!("Invalid key: {:?}", key);
}
}
let extensions =
extensions.into_iter().map(|(k, v)| (k.to_string(), format!("{v:?}"))).collect();
let security_version = if profile.security_version_optional {
security_version.into_optional_u64()
} else {
security_version.into_u64().map(Some)
}
.context("Security version")?;
Ok(ConfigDescBuilder::new()
.component_name(component_name.into_optional_string().context("Component name")?)
.component_version(
validate_version(profile, component_version).context("Component version")?,
)
.resettable(resettable.is_null().context("Resettable")?)
.security_version(security_version)
.extensions(extensions)
.build())
}
fn validate_version(profile: &Profile, field: FieldValue) -> Result<Option<ComponentVersion>> {
Ok(
if !field.is_integer()
&& profile.component_version_type == ComponentVersionType::IntOrString
{
field.into_optional_string()?.map(ComponentVersion::String)
} else {
field.into_optional_i64()?.map(ComponentVersion::Integer)
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cbor::dice::KeyOpsType;
use crate::cbor::serialize;
use crate::publickey::testkeys::{PrivateKey, ED25519_KEY_PEM};
use crate::session::{DiceProfileRange, Options};
use ciborium::cbor;
use coset::iana::{self, EnumI64};
use coset::CborSerializable;
use std::collections::HashMap;
impl Entry {
pub(in super::super) fn from_payload(payload: &Payload) -> Result<Self> {
Ok(Self { payload: serialize(payload.to_cbor_value()?) })
}
pub(in super::super) fn sign(self, key: &PrivateKey) -> CoseSign1 {
key.sign_cose_sign1(self.payload)
}
}
impl Payload {
pub(in super::super) fn to_cbor_value(&self) -> Result<Value> {
let subject_public_key = self.subject_public_key().to_cose_key()?.to_vec()?;
let config_desc = serialize(self.config_desc().to_cbor_value());
let mut map = vec![
(Value::from(ISS), Value::from(self.issuer())),
(Value::from(SUB), Value::from(self.subject())),
(Value::from(SUBJECT_PUBLIC_KEY), Value::from(subject_public_key)),
(Value::from(MODE), encode_mode(self.mode())),
(Value::from(CODE_HASH), Value::from(self.code_hash())),
(Value::from(CONFIG_DESC), Value::from(config_desc)),
(Value::from(AUTHORITY_HASH), Value::from(self.authority_hash())),
(Value::from(KEY_USAGE), Value::from(vec![0x20])),
];
if let Some(code_desc) = self.code_desc() {
map.push((Value::from(CODE_DESC), Value::from(code_desc)));
}
if let Some(config_hash) = self.config_hash() {
map.push((Value::from(CONFIG_HASH), Value::from(config_hash)));
}
if let Some(authority_desc) = self.authority_desc() {
map.push((Value::from(AUTHORITY_DESC), Value::from(authority_desc)));
}
Ok(Value::Map(map))
}
}
impl ConfigDesc {
pub(in super::super) fn to_cbor_value(&self) -> Value {
let mut map = Vec::new();
if let Some(component_name) = self.component_name() {
map.push((Value::from(COMPONENT_NAME), Value::from(component_name)));
}
if let Some(component_version) = self.component_version() {
map.push((
Value::from(COMPONENT_VERSION),
match component_version {
ComponentVersion::Integer(n) => Value::from(*n),
ComponentVersion::String(s) => Value::from(s.as_str()),
},
))
}
if self.resettable() {
map.push((Value::from(RESETTABLE), Value::Null));
}
if let Some(security_version) = self.security_version() {
map.push((Value::from(SECURITY_VERSION), Value::from(security_version)));
}
Value::Map(map)
}
}
fn encode_mode(mode: DiceMode) -> Value {
let mode = match mode {
DiceMode::NotConfigured => 0,
DiceMode::Normal => 1,
DiceMode::Debug => 2,
DiceMode::Recovery => 3,
};
Value::Bytes(vec![mode])
}
#[test]
fn valid_payload_sha256() {
let config_desc = serialize(cbor!({COMPONENT_NAME => "sha256 test"}).unwrap());
let config_hash = sha256(&config_desc).to_vec();
let mut fields = valid_payload_fields();
fields.insert(CODE_HASH, Value::Bytes(vec![1; 32]));
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
fields.insert(AUTHORITY_HASH, Value::Bytes(vec![2; 32]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
}
#[test]
fn valid_payload_sha384() {
let config_desc = serialize(cbor!({COMPONENT_NAME => "sha384 test"}).unwrap());
let config_hash = sha384(&config_desc).to_vec();
let mut fields = valid_payload_fields();
fields.insert(CODE_HASH, Value::Bytes(vec![1; 48]));
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
fields.insert(AUTHORITY_HASH, Value::Bytes(vec![2; 48]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
}
#[test]
fn valid_payload_sha512() {
let fields = valid_payload_fields();
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
}
#[test]
fn key_usage_only_key_cert_sign() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x20]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
}
#[test]
fn key_usage_too_long() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x20, 0x30, 0x40]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn key_usage_lacks_key_cert_sign() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x10]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn key_usage_not_just_key_cert_sign() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x21]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn mode_not_configured() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::Bytes(vec![0]));
let session = Session { options: Options::default() };
let payload =
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
assert_eq!(payload.mode(), DiceMode::NotConfigured);
}
#[test]
fn mode_normal() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::Bytes(vec![1]));
let session = Session { options: Options::default() };
let payload =
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
assert_eq!(payload.mode(), DiceMode::Normal);
}
#[test]
fn mode_debug() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::Bytes(vec![2]));
let session = Session { options: Options::default() };
let payload =
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
assert_eq!(payload.mode(), DiceMode::Debug);
}
#[test]
fn mode_recovery() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::Bytes(vec![3]));
let session = Session { options: Options::default() };
let payload =
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
assert_eq!(payload.mode(), DiceMode::Recovery);
}
#[test]
fn mode_invalid_becomes_not_configured() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::Bytes(vec![4]));
let session = Session { options: Options::default() };
let payload =
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
assert_eq!(payload.mode(), DiceMode::NotConfigured);
}
#[test]
fn mode_multiple_bytes() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::Bytes(vec![0, 1]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn mode_int_debug() {
let mut fields = valid_payload_fields();
fields.insert(MODE, Value::from(2));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { mode_type: ModeType::IntOrBytes, ..Profile::default() };
let payload = Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap();
assert_eq!(payload.mode(), DiceMode::Debug);
}
#[test]
fn subject_public_key_garbage() {
let mut fields = valid_payload_fields();
fields.insert(SUBJECT_PUBLIC_KEY, Value::Bytes(vec![17; 64]));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn key_usage_little_endian() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x20, 0x00, 0x00]));
let cbor = serialize_fields(fields);
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &cbor, ConfigFormat::Android).unwrap();
}
#[test]
fn key_usage_little_endian_invalid() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x20, 0xbe, 0xef]));
let cbor = serialize_fields(fields);
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &cbor, ConfigFormat::Android).unwrap_err();
}
#[test]
fn key_usage_big_endian() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x00, 0x20]));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { allow_big_endian_key_usage: true, ..Profile::default() };
Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap();
}
#[test]
fn key_usage_big_endian_invalid() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x00, 0xfe, 0x20]));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { allow_big_endian_key_usage: true, ..Profile::default() };
Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap_err();
}
#[test]
fn key_usage_invalid() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![0x00, 0x10]));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { allow_big_endian_key_usage: true, ..Profile::default() };
Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap_err();
}
#[test]
fn key_usage_empty() {
let mut fields = valid_payload_fields();
fields.insert(KEY_USAGE, Value::Bytes(vec![]));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { allow_big_endian_key_usage: true, ..Profile::default() };
Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap_err();
}
#[test]
fn config_desc_custom_field_above() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({-69999 => "custom"}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
}
#[test]
fn config_desc_reserved_field_max() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({-70000 => "reserved"}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn config_desc_reserved_field_min() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({-70999 => "reserved"}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap_err();
}
#[test]
fn config_desc_custom_field_below() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({-71000 => "custom"}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
}
#[test]
fn config_desc_custom_fields() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({-71000 => "custom hi", -69999 => "custom lo"}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let session = Session { options: Options::default() };
let payload =
Payload::from_cbor(&session, &serialize_fields(fields), ConfigFormat::Android).unwrap();
let extensions = payload.config_desc().extensions();
let extensions = HashMap::<_, _>::from_iter(extensions.to_owned());
assert_eq!(extensions.get("-71000").unwrap(), "Text(\"custom hi\")");
assert_eq!(extensions.get("-69999").unwrap(), "Text(\"custom lo\")");
assert_eq!(extensions.len(), 2);
}
#[test]
fn config_desc_not_android_spec() {
let mut fields = valid_payload_fields();
fields.insert(CONFIG_DESC, Value::Bytes(vec![0xcd; 64]));
let cbor = serialize_fields(fields);
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &cbor, ConfigFormat::Android).unwrap_err();
let payload = Payload::from_cbor(&session, &cbor, ConfigFormat::AndroidOrIgnored).unwrap();
assert_eq!(payload.config_desc(), &ConfigDesc::default());
}
#[test]
fn config_desc_component_version_string() {
let mut fields = valid_payload_fields();
let config_desc = serialize(
cbor!({COMPONENT_VERSION => "It's version 4", SECURITY_VERSION => 99999999}).unwrap(),
);
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let entries = encode_fields(fields);
let profile =
Profile { component_version_type: ComponentVersionType::Int, ..Profile::default() };
Payload::from_entries(&profile, entries.clone(), ConfigFormat::Android).unwrap_err();
let payload =
Payload::from_entries(&Profile::default(), entries, ConfigFormat::Android).unwrap();
assert_eq!(
payload.config_desc().component_version(),
Some(&ComponentVersion::String("It's version 4".to_string()))
);
}
#[test]
fn config_desc_security_version() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({SECURITY_VERSION => 0x12345678}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let cbor = serialize_fields(fields);
let session = Session { options: Options::default() };
let payload = Payload::from_cbor(&session, &cbor, ConfigFormat::Android).unwrap();
assert_eq!(payload.config_desc().security_version(), Some(0x12345678));
}
#[test]
fn config_desc_security_version_omitted() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({}).unwrap());
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { security_version_optional: true, ..Profile::default() };
let payload = Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap();
assert_eq!(payload.config_desc().security_version(), None);
}
#[test]
fn config_desc_security_version_fixed_size_encoding() {
let mut fields = valid_payload_fields();
let config_desc = vec![
0xa1, // Map of one element.
0x3a, 0x00, 0x01, 0x11, 0x74, // SECURITY_VERSION.
0x1a, 0x00, 0x00, 0xca, 0xfe, // Non-deterministic encoding of 0xcafe.
];
let config_hash = sha512(&config_desc).to_vec();
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
fields.insert(CONFIG_HASH, Value::Bytes(config_hash));
let cbor = serialize_fields(fields);
let session = Session { options: Options::default() };
let payload = Payload::from_cbor(&session, &cbor, ConfigFormat::Android).unwrap();
assert_eq!(payload.config_desc().security_version(), Some(0xcafe));
}
#[test]
fn config_desc_security_version_negative() {
let mut fields = valid_payload_fields();
let config_desc = serialize(cbor!({SECURITY_VERSION => Value::from(-12)}).unwrap());
fields.insert(CONFIG_DESC, Value::Bytes(config_desc));
let cbor = serialize_fields(fields);
let session = Session { options: Options::default() };
Payload::from_cbor(&session, &cbor, ConfigFormat::Android).unwrap_err();
}
#[test]
fn config_hash_missing() {
let mut fields = valid_payload_fields();
fields.remove(&CONFIG_HASH);
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries, ConfigFormat::Android).unwrap_err();
}
#[test]
fn integer_key_ops() {
let mut fields = valid_payload_fields();
let subject_public_key = cbor!({
iana::KeyParameter::Kty.to_i64() => iana::KeyType::OKP.to_i64(),
iana::KeyParameter::Alg.to_i64() => iana::Algorithm::EdDSA.to_i64(),
iana::KeyParameter::KeyOps.to_i64() => iana::KeyOperation::Verify.to_i64(),
iana::OkpKeyParameter::Crv.to_i64() => iana::EllipticCurve::Ed25519.to_i64(),
iana::OkpKeyParameter::X.to_i64() => Value::Bytes(vec![0; 32]),
})
.unwrap();
fields.insert(SUBJECT_PUBLIC_KEY, Value::Bytes(serialize(subject_public_key)));
let entries = encode_fields(fields);
Payload::from_entries(&Profile::default(), entries.clone(), ConfigFormat::Android)
.unwrap_err();
let profile = Profile { key_ops_type: KeyOpsType::IntOrArray, ..Profile::default() };
Payload::from_entries(&profile, entries, ConfigFormat::Android).unwrap();
}
#[test]
fn extract_profile_version_named_profiles() {
let test_cases = [
("android.14", ProfileVersion::Android14),
("android.15", ProfileVersion::Android15),
("android.16", ProfileVersion::Android16),
];
for (profile_name, expected_version) in test_cases {
let mut fields = valid_payload_fields();
fields.insert(PROFILE_NAME, Value::from(profile_name));
let entries = encode_fields(fields);
let session = Session {
options: Options {
dice_profile_range: DiceProfileRange::new(expected_version, expected_version),
},
};
let profile_version =
PayloadFields::extract_profile_version(&session, &entries).unwrap();
assert_eq!(profile_version, expected_version);
}
}
#[test]
fn extract_profile_version_named_android_13_fails() {
let session = Session {
options: Options {
dice_profile_range: DiceProfileRange::new(
ProfileVersion::Android13,
ProfileVersion::Android16,
),
},
};
let mut fields = valid_payload_fields();
fields.insert(PROFILE_NAME, Value::from("android.13"));
let entries = encode_fields(fields);
PayloadFields::extract_profile_version(&session, &entries).unwrap_err();
}
#[test]
fn extract_profile_version_multiple_profile_name_entries_fails() {
let session = Session {
options: Options {
dice_profile_range: DiceProfileRange::new(
ProfileVersion::Android13,
ProfileVersion::Android16,
),
},
};
let mut fields = valid_payload_fields();
fields.insert(PROFILE_NAME, Value::from("android.15"));
let mut entries = encode_fields(fields);
entries.push((Value::from(PROFILE_NAME), Value::from("android.15")));
PayloadFields::extract_profile_version(&session, &entries).unwrap_err();
}
#[test]
fn extract_profile_version_out_of_range_fails() {
let session = Session {
options: Options {
dice_profile_range: DiceProfileRange::new(
ProfileVersion::Android15,
ProfileVersion::Android15,
),
},
};
let mut fields = valid_payload_fields();
fields.insert(PROFILE_NAME, Value::from("android.14"));
let entries = encode_fields(fields.clone());
PayloadFields::extract_profile_version(&session, &entries).unwrap_err();
fields.insert(PROFILE_NAME, Value::from("android.16"));
let entries = encode_fields(fields);
PayloadFields::extract_profile_version(&session, &entries).unwrap_err();
}
#[test]
fn extract_profile_version_default_when_not_named_up_to_android_14() {
let entries = encode_fields(valid_payload_fields());
for expected_version in [ProfileVersion::Android13, ProfileVersion::Android14] {
let session = Session {
options: Options {
dice_profile_range: DiceProfileRange::new(
expected_version,
ProfileVersion::Android16,
),
},
};
let profile_version =
PayloadFields::extract_profile_version(&session, &entries).unwrap();
assert_eq!(profile_version, expected_version);
}
}
#[test]
fn extract_profile_version_named_profile_required_from_android_15() {
let entries = encode_fields(valid_payload_fields());
for min_version in [ProfileVersion::Android15, ProfileVersion::Android16] {
let session = Session {
options: Options {
dice_profile_range: DiceProfileRange::new(
min_version,
ProfileVersion::Android16,
),
},
};
PayloadFields::extract_profile_version(&session, &entries).unwrap_err();
}
}
fn valid_payload_fields() -> HashMap<i64, Value> {
let key = PrivateKey::from_pem(ED25519_KEY_PEM[0]).public_key();
let subject_public_key = key.to_cose_key().unwrap().to_vec().unwrap();
let config_desc = serialize(
cbor!({COMPONENT_NAME => "component name", SECURITY_VERSION => 1234}).unwrap(),
);
let config_hash = sha512(&config_desc).to_vec();
HashMap::from([
(ISS, Value::from("issuer")),
(SUB, Value::from("subject")),
(SUBJECT_PUBLIC_KEY, Value::Bytes(subject_public_key)),
(KEY_USAGE, Value::Bytes(vec![0x20])),
(CODE_HASH, Value::Bytes(vec![1; 64])),
(CONFIG_DESC, Value::Bytes(config_desc)),
(CONFIG_HASH, Value::Bytes(config_hash)),
(AUTHORITY_HASH, Value::Bytes(vec![2; 64])),
(MODE, Value::Bytes(vec![0])),
])
}
fn encode_fields(mut fields: HashMap<i64, Value>) -> Vec<(Value, Value)> {
fields.drain().map(|(k, v)| (Value::from(k), v)).collect()
}
fn serialize_fields(fields: HashMap<i64, Value>) -> Vec<u8> {
serialize(Value::Map(encode_fields(fields)))
}
}