Encode credentials as a protocol buffer message

This permits to decode a credential of a different version without failing.
This commit is contained in:
Julien Cretin
2020-05-09 15:55:55 +02:00
parent a2eff7c632
commit f4b791ed91
5 changed files with 102 additions and 59 deletions

View File

@@ -41,18 +41,30 @@ macro_rules! cbor_map_options {
}; };
( $( $key:expr => $value:expr ),* ) => { ( $( $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. // The import is unused if the list is empty.
#[allow(unused_imports)] #[allow(unused_imports)]
use $crate::values::{IntoCborKey, IntoCborValueOption}; use $crate::values::{IntoCborKey, IntoCborValueOption};
let mut _map = ::alloc::collections::BTreeMap::<_, $crate::values::Value>::new(); let mut _map = $initial;
$( $(
{ if let Some(val) = $value.into_cbor_value_option() {
let opt: Option<$crate::values::Value> = $value.into_cbor_value_option();
if let Some(val) = opt {
_map.insert($key.into_cbor_key(), val); _map.insert($key.into_cbor_key(), val);
} }
}
)* )*
$crate::values::Value::Map(_map) $crate::values::Value::Map(_map)
} }

View File

@@ -36,7 +36,7 @@ impl<'a> Writer<'a> {
return false; return false;
} }
match value { 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)) => { Value::KeyValue(KeyType::Negative(negative)) => {
self.start_item(1, -(negative + 1) as u64) self.start_item(1, -(negative + 1) as u64)
} }

View File

@@ -445,59 +445,83 @@ pub struct PublicKeyCredentialSource {
pub user_handle: Vec<u8>, // not optional, but nullable pub user_handle: Vec<u8>, // not optional, but nullable
pub other_ui: Option<String>, pub other_ui: Option<String>,
pub cred_random: Option<Vec<u8>>, 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 { impl From<PublicKeyCredentialSource> for cbor::Value {
fn from(credential: PublicKeyCredentialSource) -> 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); credential.private_key.to_bytes(&mut private_key);
let other_ui = match credential.other_ui { cbor_extend_map_options! {
None => cbor_null!(), credential.unknown_fields,
Some(other_ui) => cbor_text!(other_ui), CredentialId => Some(credential.credential_id),
}; PrivateKey => Some(private_key.to_vec()),
let cred_random = match credential.cred_random { RpId => Some(credential.rp_id),
None => cbor_null!(), UserHandle => Some(credential.user_handle),
Some(cred_random) => cbor_bytes!(cred_random), OtherUi => credential.other_ui,
}; CredRandom => credential.cred_random
cbor_array! {
credential.credential_id,
private_key,
credential.rp_id,
credential.user_handle,
other_ui,
cred_random,
} }
} }
} }
impl TryFrom<cbor::Value> for PublicKeyCredentialSource { impl PublicKeyCredentialSource {
type Error = Ctap2StatusCode; 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,
};
fn try_from(cbor_value: cbor::Value) -> Result<PublicKeyCredentialSource, Ctap2StatusCode> { let credential_id = read_byte_string(&map.remove(&CredentialId.into())?).ok()?;
use cbor::{SimpleValue, Value}; let private_key = read_byte_string(&map.remove(&PrivateKey.into())?).ok()?;
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])?;
if private_key.len() != 32 { 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)) 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(&map.remove(&RpId.into())?).ok()?;
let rp_id = read_text_string(&fields[2])?; let user_handle = read_byte_string(&map.remove(&UserHandle.into())?).ok()?;
let user_handle = read_byte_string(&fields[3])?; let other_ui = map
let other_ui = match &fields[4] { .remove(&OtherUi.into())
Value::Simple(SimpleValue::NullValue) => None, .as_ref()
cbor_value => Some(read_text_string(cbor_value)?), .map(read_text_string)
}; .transpose()
let cred_random = match &fields[5] { .ok()?;
Value::Simple(SimpleValue::NullValue) => None, let cred_random = map
cbor_value => Some(read_byte_string(cbor_value)?), .remove(&CredRandom.into())
}; .as_ref()
Ok(PublicKeyCredentialSource { .map(read_byte_string)
.transpose()
.ok()?;
let unknown_fields = map;
Some(PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey, key_type: PublicKeyCredentialType::PublicKey,
credential_id, credential_id,
private_key, private_key,
@@ -505,6 +529,7 @@ impl TryFrom<cbor::Value> for PublicKeyCredentialSource {
user_handle, user_handle,
other_ui, other_ui,
cred_random, cred_random,
unknown_fields,
}) })
} }
} }
@@ -1218,11 +1243,12 @@ mod test {
user_handle: b"foo".to_vec(), user_handle: b"foo".to_vec(),
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
unknown_fields: BTreeMap::new(),
}; };
assert_eq!( assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), PublicKeyCredentialSource::parse_cbor(cbor::Value::from(credential.clone())),
Ok(credential.clone()) Some(credential.clone())
); );
let credential = PublicKeyCredentialSource { let credential = PublicKeyCredentialSource {
@@ -1231,8 +1257,8 @@ mod test {
}; };
assert_eq!( assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), PublicKeyCredentialSource::parse_cbor(cbor::Value::from(credential.clone())),
Ok(credential.clone()) Some(credential.clone())
); );
let credential = PublicKeyCredentialSource { let credential = PublicKeyCredentialSource {
@@ -1241,15 +1267,15 @@ mod test {
}; };
assert_eq!( assert_eq!(
PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), PublicKeyCredentialSource::parse_cbor(cbor::Value::from(credential.clone())),
Ok(credential) Some(credential)
); );
} }
#[test] #[test]
fn test_credential_source_invalid_cbor() { fn test_credential_source_invalid_cbor() {
assert!(PublicKeyCredentialSource::try_from(cbor_false!()).is_err()); assert!(PublicKeyCredentialSource::parse_cbor(cbor_false!()).is_none());
assert!(PublicKeyCredentialSource::try_from(cbor_array!(false)).is_err()); assert!(PublicKeyCredentialSource::parse_cbor(cbor_array!(false)).is_none());
assert!(PublicKeyCredentialSource::try_from(cbor_array!(b"foo".to_vec())).is_err()); assert!(PublicKeyCredentialSource::parse_cbor(cbor_array!(b"foo".to_vec())).is_none());
} }
} }

View File

@@ -335,6 +335,7 @@ where
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
unknown_fields: BTreeMap::new(),
}) })
} }
@@ -501,6 +502,7 @@ where
.user_display_name .user_display_name
.map(|s| truncate_to_char_boundary(&s, 64).to_string()), .map(|s| truncate_to_char_boundary(&s, 64).to_string()),
cred_random, cred_random,
unknown_fields: BTreeMap::new(),
}; };
self.persistent_store.store_credential(credential_source)?; self.persistent_store.store_credential(credential_source)?;
random_id random_id
@@ -1279,6 +1281,7 @@ mod test {
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
unknown_fields: BTreeMap::new(),
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store
@@ -1476,6 +1479,7 @@ mod test {
user_handle: vec![], user_handle: vec![],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
unknown_fields: BTreeMap::new(),
}; };
assert!(ctap_state assert!(ctap_state
.persistent_store .persistent_store

View File

@@ -16,9 +16,9 @@ use crate::crypto::rng256::Rng256;
use crate::ctap::data_formats::PublicKeyCredentialSource; use crate::ctap::data_formats::PublicKeyCredentialSource;
use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::status_code::Ctap2StatusCode;
use crate::ctap::PIN_AUTH_LENGTH; use crate::ctap::PIN_AUTH_LENGTH;
use alloc::collections::BTreeMap;
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::convert::TryInto;
use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError, StoreIndex}; use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError, StoreIndex};
#[cfg(any(test, feature = "ram_storage"))] #[cfg(any(test, feature = "ram_storage"))]
@@ -420,8 +420,7 @@ impl From<StoreError> for Ctap2StatusCode {
} }
fn deserialize_credential(data: &[u8]) -> Option<PublicKeyCredentialSource> { fn deserialize_credential(data: &[u8]) -> Option<PublicKeyCredentialSource> {
let cbor = cbor::read(data).ok()?; PublicKeyCredentialSource::parse_cbor(cbor::read(data).ok()?)
cbor.try_into().ok()
} }
fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>, Ctap2StatusCode> { fn serialize_credential(credential: PublicKeyCredentialSource) -> Result<Vec<u8>, Ctap2StatusCode> {
@@ -454,6 +453,7 @@ mod test {
user_handle, user_handle,
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
unknown_fields: BTreeMap::new(),
} }
} }
@@ -623,6 +623,7 @@ mod test {
user_handle: vec![0x00], user_handle: vec![0x00],
other_ui: None, other_ui: None,
cred_random: None, cred_random: None,
unknown_fields: BTreeMap::new(),
}; };
assert_eq!(found_credential, Some(expected_credential)); assert_eq!(found_credential, Some(expected_credential));
} }