diff --git a/libraries/cbor/src/macros.rs b/libraries/cbor/src/macros.rs index 5a3d8f522dc45b80d42bfbfe3209ac16b1af3d96..e5f3781ed054d20425ee7ffd6e0abd07dbdf89b6 100644 --- a/libraries/cbor/src/macros.rs +++ b/libraries/cbor/src/macros.rs @@ -41,18 +41,30 @@ macro_rules! cbor_map_options { }; ( $( $key:expr => $value:expr ),* ) => { + cbor_extend_map_options! ( + ::alloc::collections::BTreeMap::<_, $crate::values::Value>::new(), + $( $key => $value, )* + ) + }; +} + +#[macro_export] +macro_rules! cbor_extend_map_options { + // Add trailing comma if missing. + ( $initial:expr, $( $key:expr => $value:expr ),+ ) => { + cbor_extend_map_options! ( $initial, $($key => $value,)+ ) + }; + + ( $initial:expr, $( $key:expr => $value:expr, )* ) => { { // The import is unused if the list is empty. #[allow(unused_imports)] use $crate::values::{IntoCborKey, IntoCborValueOption}; - let mut _map = ::alloc::collections::BTreeMap::<_, $crate::values::Value>::new(); + let mut _map = $initial; $( - { - let opt: Option<$crate::values::Value> = $value.into_cbor_value_option(); - if let Some(val) = opt { + if let Some(val) = $value.into_cbor_value_option() { _map.insert($key.into_cbor_key(), val); } - } )* $crate::values::Value::Map(_map) } diff --git a/libraries/cbor/src/writer.rs b/libraries/cbor/src/writer.rs index 127316091d3bfa9ebb7f3559cd977c3cbee117cb..0764851d37907a320251dd8767e2d505931d7f2a 100644 --- a/libraries/cbor/src/writer.rs +++ b/libraries/cbor/src/writer.rs @@ -36,7 +36,7 @@ impl<'a> Writer<'a> { return false; } match value { - Value::KeyValue(KeyType::Unsigned(unsigned)) => self.start_item(0, unsigned as u64), + Value::KeyValue(KeyType::Unsigned(unsigned)) => self.start_item(0, unsigned), Value::KeyValue(KeyType::Negative(negative)) => { self.start_item(1, -(negative + 1) as u64) } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 8e6c7d9c365a149d8c1b4a82e1ad019448a4ecff..6873d8a9a4a5389212376136488fc7fab3d294ca 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -445,59 +445,83 @@ pub struct PublicKeyCredentialSource { pub user_handle: Vec<u8>, // not optional, but nullable pub other_ui: Option<String>, pub cred_random: Option<Vec<u8>>, + + /// Contains the unknown fields when parsing a CBOR value. + /// + /// Those fields could either be deleted fields from older versions (they should have reserved + /// tags) or fields from newer versions (the tags should not be reserved). If this is empty, + /// then the parsed credential is probably from the same version (but not necessarily). + pub unknown_fields: BTreeMap<cbor::KeyType, cbor::Value>, +} + +// We simulate protocol buffers in CBOR with maps. Each field of a message is associated with a +// unique tag, implemented with a CBOR unsigned key. +#[repr(u64)] +enum PublicKeyCredentialSourceField { + CredentialId = 0, + PrivateKey = 1, + RpId = 2, + UserHandle = 3, + OtherUi = 4, + CredRandom = 5, + // When a field is removed, its tag should be reserved and not used for new fields. We document + // those reserved tags below. + // Reserved tags: none. +} + +impl From<PublicKeyCredentialSourceField> for cbor::KeyType { + fn from(field: PublicKeyCredentialSourceField) -> cbor::KeyType { + (field as u64).into() + } } impl From<PublicKeyCredentialSource> for cbor::Value { fn from(credential: PublicKeyCredentialSource) -> cbor::Value { - let mut private_key = [0u8; 32]; + use PublicKeyCredentialSourceField::*; + let mut private_key = [0; 32]; credential.private_key.to_bytes(&mut private_key); - let other_ui = match credential.other_ui { - None => cbor_null!(), - Some(other_ui) => cbor_text!(other_ui), - }; - let cred_random = match credential.cred_random { - None => cbor_null!(), - Some(cred_random) => cbor_bytes!(cred_random), - }; - cbor_array! { - credential.credential_id, - private_key, - credential.rp_id, - credential.user_handle, - other_ui, - cred_random, + cbor_extend_map_options! { + credential.unknown_fields, + CredentialId => Some(credential.credential_id), + PrivateKey => Some(private_key.to_vec()), + RpId => Some(credential.rp_id), + UserHandle => Some(credential.user_handle), + OtherUi => credential.other_ui, + CredRandom => credential.cred_random } } } -impl TryFrom<cbor::Value> for PublicKeyCredentialSource { - type Error = Ctap2StatusCode; - - fn try_from(cbor_value: cbor::Value) -> Result<PublicKeyCredentialSource, Ctap2StatusCode> { - use cbor::{SimpleValue, Value}; +impl PublicKeyCredentialSource { + pub fn parse_cbor(cbor_value: cbor::Value) -> Option<PublicKeyCredentialSource> { + use PublicKeyCredentialSourceField::*; + let mut map = match cbor_value { + cbor::Value::Map(x) => x, + _ => return None, + }; - let fields = read_array(&cbor_value)?; - if fields.len() != 6 { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); - } - let credential_id = read_byte_string(&fields[0])?; - let private_key = read_byte_string(&fields[1])?; + let credential_id = read_byte_string(&map.remove(&CredentialId.into())?).ok()?; + let private_key = read_byte_string(&map.remove(&PrivateKey.into())?).ok()?; if private_key.len() != 32 { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); + return None; } - let private_key = ecdsa::SecKey::from_bytes(array_ref!(private_key, 0, 32)) - .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)?; - let rp_id = read_text_string(&fields[2])?; - let user_handle = read_byte_string(&fields[3])?; - let other_ui = match &fields[4] { - Value::Simple(SimpleValue::NullValue) => None, - cbor_value => Some(read_text_string(cbor_value)?), - }; - let cred_random = match &fields[5] { - Value::Simple(SimpleValue::NullValue) => None, - cbor_value => Some(read_byte_string(cbor_value)?), - }; - Ok(PublicKeyCredentialSource { + let private_key = ecdsa::SecKey::from_bytes(array_ref!(private_key, 0, 32))?; + let rp_id = read_text_string(&map.remove(&RpId.into())?).ok()?; + let user_handle = read_byte_string(&map.remove(&UserHandle.into())?).ok()?; + let other_ui = map + .remove(&OtherUi.into()) + .as_ref() + .map(read_text_string) + .transpose() + .ok()?; + let cred_random = map + .remove(&CredRandom.into()) + .as_ref() + .map(read_byte_string) + .transpose() + .ok()?; + let unknown_fields = map; + Some(PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, private_key, @@ -505,6 +529,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource { user_handle, other_ui, cred_random, + unknown_fields, }) } } @@ -1218,11 +1243,12 @@ mod test { user_handle: b"foo".to_vec(), other_ui: None, cred_random: None, + unknown_fields: BTreeMap::new(), }; assert_eq!( - PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), - Ok(credential.clone()) + PublicKeyCredentialSource::parse_cbor(cbor::Value::from(credential.clone())), + Some(credential.clone()) ); let credential = PublicKeyCredentialSource { @@ -1231,8 +1257,8 @@ mod test { }; assert_eq!( - PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), - Ok(credential.clone()) + PublicKeyCredentialSource::parse_cbor(cbor::Value::from(credential.clone())), + Some(credential.clone()) ); let credential = PublicKeyCredentialSource { @@ -1241,15 +1267,15 @@ mod test { }; assert_eq!( - PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), - Ok(credential) + PublicKeyCredentialSource::parse_cbor(cbor::Value::from(credential.clone())), + Some(credential) ); } #[test] fn test_credential_source_invalid_cbor() { - assert!(PublicKeyCredentialSource::try_from(cbor_false!()).is_err()); - assert!(PublicKeyCredentialSource::try_from(cbor_array!(false)).is_err()); - assert!(PublicKeyCredentialSource::try_from(cbor_array!(b"foo".to_vec())).is_err()); + assert!(PublicKeyCredentialSource::parse_cbor(cbor_false!()).is_none()); + assert!(PublicKeyCredentialSource::parse_cbor(cbor_array!(false)).is_none()); + assert!(PublicKeyCredentialSource::parse_cbor(cbor_array!(b"foo".to_vec())).is_none()); } } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index a39a983f41524b2728f220b0cf4ed34da2b9b46c..55a1e406336261bcb63b16b27f0c2474e6eab655 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -335,6 +335,7 @@ where user_handle: vec![], other_ui: None, cred_random: None, + unknown_fields: BTreeMap::new(), }) } @@ -501,6 +502,7 @@ where .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random, + unknown_fields: BTreeMap::new(), }; self.persistent_store.store_credential(credential_source)?; random_id @@ -1279,6 +1281,7 @@ mod test { user_handle: vec![], other_ui: None, cred_random: None, + unknown_fields: BTreeMap::new(), }; assert!(ctap_state .persistent_store @@ -1476,6 +1479,7 @@ mod test { user_handle: vec![], other_ui: None, cred_random: None, + unknown_fields: BTreeMap::new(), }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index f0a2ca4ff0bfb3ec85e1859d44009af8c0a6ee29..41f4ebee77f6a48bae2d19b0b54bd290caab7d35 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -16,9 +16,9 @@ use crate::crypto::rng256::Rng256; use crate::ctap::data_formats::PublicKeyCredentialSource; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::PIN_AUTH_LENGTH; +use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use core::convert::TryInto; use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError, StoreIndex}; #[cfg(any(test, feature = "ram_storage"))] @@ -420,8 +420,7 @@ impl From<StoreError> for Ctap2StatusCode { } fn deserialize_credential(data: &[u8]) -> Option<PublicKeyCredentialSource> { - let cbor = cbor::read(data).ok()?; - cbor.try_into().ok() + PublicKeyCredentialSource::parse_cbor(cbor::read(data).ok()?) } fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>, Ctap2StatusCode> { @@ -454,6 +453,7 @@ mod test { user_handle, other_ui: None, cred_random: None, + unknown_fields: BTreeMap::new(), } } @@ -623,6 +623,7 @@ mod test { user_handle: vec![0x00], other_ui: None, cred_random: None, + unknown_fields: BTreeMap::new(), }; assert_eq!(found_credential, Some(expected_credential)); }