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));
     }