diff --git a/libraries/cbor/src/macros.rs b/libraries/cbor/src/macros.rs index 5a3d8f5..d96354e 100644 --- a/libraries/cbor/src/macros.rs +++ b/libraries/cbor/src/macros.rs @@ -12,6 +12,142 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::values::{KeyType, Value}; +use alloc::collections::btree_map; +use core::cmp::Ordering; +use core::iter::Peekable; + +/// This macro generates code to extract multiple values from a `BTreeMap` at once +/// in an optimized manner, consuming the input map. +/// +/// It takes as input a `BTreeMap` as well as a list of identifiers and keys, and generates code +/// that assigns the corresponding values to new variables using the given identifiers. Each of +/// these variables has type `Option`, to account for the case where keys aren't found. +/// +/// **Important:** Keys passed to the `destructure_cbor_map!` macro **must be sorted** in increasing +/// order. If not, the algorithm can yield incorrect results, such a assigning `None` to a variable +/// even if the corresponding key existed in the map. **No runtime checks** are made for this in the +/// `destructure_cbor_map!` macro, in order to avoid overhead at runtime. However, assertions that +/// keys are sorted are added in `cfg(test)` mode, so that unit tests can verify ahead of time that +/// the keys are indeed sorted. This macro is therefore **not suitable for dynamic keys** that can +/// change at runtime. +/// +/// Semantically, provided that the keys are sorted as specified above, the following two snippets +/// of code are equivalent, but the `destructure_cbor_map!` version is more optimized, as it doesn't +/// re-balance the `BTreeMap` for each key, contrary to the `BTreeMap::remove` operations. +/// +/// ```rust +/// # extern crate alloc; +/// # #[macro_use] +/// # extern crate cbor; +/// # +/// # fn main() { +/// # let map = alloc::collections::BTreeMap::new(); +/// destructure_cbor_map! { +/// let { +/// 1 => x, +/// "key" => y, +/// } = map; +/// } +/// # } +/// ``` +/// +/// ```rust +/// # extern crate alloc; +/// # #[macro_use] +/// # extern crate cbor; +/// # +/// # fn main() { +/// # let mut map = alloc::collections::BTreeMap::::new(); +/// use cbor::values::IntoCborKey; +/// let x: Option = map.remove(&1.into_cbor_key()); +/// let y: Option = map.remove(&"key".into_cbor_key()); +/// # } +/// ``` +#[macro_export] +macro_rules! destructure_cbor_map { + ( let { $( $key:expr => $variable:ident, )+ } = $map:expr; ) => { + // A pre-requisite for this algorithm to work is that the keys to extract from the map are + // sorted - the behavior is unspecified if the keys are not sorted. + // Therefore, in test mode we add assertions that the keys are indeed sorted. + #[cfg(test)] + assert_sorted_keys!($( $key, )+); + + use $crate::values::{IntoCborKey, Value}; + use $crate::macros::destructure_cbor_map_peek_value; + + // This algorithm first converts the map into a peekable iterator - whose items are sorted + // in strictly increasing order of keys. Then, the repeated calls to the "peek value" + // helper function will consume this iterator and yield values (or `None`) when reaching + // the keys to extract. + // + // This is where the pre-requisite that keys to extract are sorted is important: the + // algorithm does a single linear scan over the iterator and therefore keys to extract have + // to come in the same order (i.e. sorted). + let mut it = $map.into_iter().peekable(); + $( + let $variable: Option = destructure_cbor_map_peek_value(&mut it, $key.into_cbor_key()); + )+ + }; +} + +/// This function is an internal detail of the `destructure_cbor_map!` macro, but has public +/// visibility so that users of the macro can use it. +/// +/// Given a peekable iterator of key-value pairs sorted in strictly increasing key order and a +/// needle key, this function consumes all items whose key compares less than or equal to the +/// needle, and returns `Some(value)` if the needle was present as the key in the iterator and +/// `None` otherwise. +/// +/// The logic is separated into its own function to reduce binary size, as otherwise the logic +/// would be inlined for every use case. As of June 2020, this saves ~40KB of binary size for the +/// CTAP2 application of OpenSK. +pub fn destructure_cbor_map_peek_value( + it: &mut Peekable>, + needle: KeyType, +) -> Option { + loop { + match it.peek() { + None => return None, + Some(item) => { + let key: &KeyType = &item.0; + match key.cmp(&needle) { + Ordering::Less => { + it.next(); + } + Ordering::Equal => { + let value: Value = it.next().unwrap().1; + return Some(value); + } + Ordering::Greater => return None, + } + } + } + } +} + +#[macro_export] +macro_rules! assert_sorted_keys { + // Last key + ( $key:expr, ) => { + }; + + ( $key1:expr, $key2:expr, $( $keys:expr, )* ) => { + { + use $crate::values::{IntoCborKey, KeyType}; + let k1: KeyType = $key1.into_cbor_key(); + let k2: KeyType = $key2.into_cbor_key(); + assert!( + k1 < k2, + "{:?} < {:?} failed. The destructure_cbor_map! macro requires keys in sorted order.", + k1, + k2, + ); + } + assert_sorted_keys!($key2, $( $keys, )*); + }; +} + #[macro_export] macro_rules! cbor_map { // trailing comma case @@ -497,4 +633,99 @@ mod test { ); assert_eq!(a, b); } + + fn extract_map(cbor_value: Value) -> BTreeMap { + match cbor_value { + Value::Map(map) => map, + _ => panic!("Expected CBOR map."), + } + } + + #[test] + fn test_destructure_cbor_map_simple() { + let map = cbor_map! { + 1 => 10, + 2 => 20, + }; + + destructure_cbor_map! { + let { + 1 => x1, + 2 => x2, + } = extract_map(map); + } + + assert_eq!(x1, Some(cbor_unsigned!(10))); + assert_eq!(x2, Some(cbor_unsigned!(20))); + } + + #[test] + #[should_panic] + fn test_destructure_cbor_map_unsorted() { + let map = cbor_map! { + 1 => 10, + 2 => 20, + }; + + destructure_cbor_map! { + // The keys are not sorted here, which violates the precondition of + // destructure_cbor_map. An assertion should catch that and make the test panic. + let { + 2 => _x2, + 1 => _x1, + } = extract_map(map); + } + } + + #[test] + fn test_destructure_cbor_map_partial() { + let map = cbor_map! { + 1 => 10, + 2 => 20, + 3 => 30, + 4 => 40, + 5 => 50, + 6 => 60, + 7 => 70, + 8 => 80, + 9 => 90, + }; + + destructure_cbor_map! { + let { + 3 => x3, + 7 => x7, + } = extract_map(map); + } + + assert_eq!(x3, Some(cbor_unsigned!(30))); + assert_eq!(x7, Some(cbor_unsigned!(70))); + } + + #[test] + fn test_destructure_cbor_map_missing() { + let map = cbor_map! { + 1 => 10, + 3 => 30, + 4 => 40, + }; + + destructure_cbor_map! { + let { + 0 => x0, + 1 => x1, + 2 => x2, + 3 => x3, + 4 => x4, + 5 => x5, + } = extract_map(map); + } + + assert_eq!(x0, None); + assert_eq!(x1, Some(cbor_unsigned!(10))); + assert_eq!(x2, None); + assert_eq!(x3, Some(cbor_unsigned!(30))); + assert_eq!(x4, Some(cbor_unsigned!(40))); + assert_eq!(x5, None); + } } diff --git a/reproducible/reference_binaries_macos-10.15.sha256sum b/reproducible/reference_binaries_macos-10.15.sha256sum index 10f2e50..83e63ba 100644 --- a/reproducible/reference_binaries_macos-10.15.sha256sum +++ b/reproducible/reference_binaries_macos-10.15.sha256sum @@ -1,9 +1,9 @@ 0b54df6d548849e24d67b9b022ca09cb33c51f078ce85d0c9c4635ffc69902e1 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -9ff63684ca08375e643f14f33dc6dc8131681bb562fb0df18f9c7f637e90cc73 target/nrf52840dk_merged.hex +e93f56b4b6bb602ab37cf967f1c3fd3d253e05ccc85d4718762f68216c35d68c target/nrf52840dk_merged.hex 052eec0ae526038352b9f7573468d0cf7fb5ec331d4dc1a2df75fdbd514ea5ca third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -f976e2975d908567398456afd9996fd60639f0a5e55d0bd5a96768a7cb7797c2 target/nrf52840_dongle_merged.hex +8cff9a4d513be338ba6a3fd91d3d4cfdd63bc066e8bf9dc22f64176114da08b8 target/nrf52840_dongle_merged.hex 908d7f4f40936d968b91ab6e19b2406612fe8c2c273d9c0b71ef1f55116780e0 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -ff13439347688a8c9ab53391fd4006e9e944743c47ea754e6ae348998f7b8048 target/nrf52840_dongle_dfu_merged.hex +94452673fb0022a07ac886d4ab74576f067c2d727aed30ed368f4e5af382238f target/nrf52840_dongle_dfu_merged.hex 34ecbecaebf1188277f2310fe769c8c60310d8576493242712854deb4ba1036e third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -f76f9fe06b0db3bdf361ae8eb79f1ac9b75c376914142341e1a77c836d1bfe42 target/nrf52840_mdk_dfu_merged.hex -e3d20aa71a70d721b66bb704767db1b8ee9b3fb8fb508937f42bdf6816a61c90 target/tab/ctap2.tab +414aaf7fcc3a0121ab02b1222d508ae503e268cb7da0df5795a6d6a01aeed345 target/nrf52840_mdk_dfu_merged.hex +212698e7c7919fa4542e1263d56f601632902f86bdf3d48cf6300b96ad452cb1 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_ubuntu-18.04.sha256sum b/reproducible/reference_binaries_ubuntu-18.04.sha256sum index f0c5b18..9f5f433 100644 --- a/reproducible/reference_binaries_ubuntu-18.04.sha256sum +++ b/reproducible/reference_binaries_ubuntu-18.04.sha256sum @@ -1,9 +1,9 @@ 29382e72d0f3c6a72ce9517211952ff29ea270193d7f0ddc48ca69009ee29925 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -bb2fbf0d9dab2b489a49d1dc3db8923086ab109d14f1f1aa8296f086a03b75dd target/nrf52840dk_merged.hex +4d5165d8ff46148a585ade23d3030c8a95928a158d283ccd7c93e14902452b6f target/nrf52840dk_merged.hex 30f239390ae9bef0825731e4c82d40470fc5e9bded2bf0d942e92dbb5d4faba1 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -e3e9dd3a633a0ceb4b971bcd82e3cd02dd37ccaca486e44f136ce79e4a5a407a target/nrf52840_dongle_merged.hex +8204a87c9e93909ed79907f2d7b655d07397161ecd64bd213bc483630a38e8c9 target/nrf52840_dongle_merged.hex e3acf15d5ae3a22aecff6cc58db5fc311f538f47328d348b7ad7db7f9ab5e72c third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -fba2fb9f4fdd40ded014a7b77d387d867808281d7d751a08eafb999a360e9e3f target/nrf52840_dongle_dfu_merged.hex +b937eaeea7ae9ca3c26bee082cb5af596942947c84171cb4d03cc66bc31d35da target/nrf52840_dongle_dfu_merged.hex cae312a26a513ada6c198fdc59b2bba3860c51726b817a9fd17a4331ee12c882 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -7f9773fa345ee47e5c344bd4ce4929d93a0b036087cb1bec427c275077bbe389 target/nrf52840_mdk_dfu_merged.hex -f4f734bb9a0681f5cad0edc0c23bb30f2790ab2f7029dda5f152cee6d68d90c7 target/tab/ctap2.tab +6d125877a207744a73b0b315152188a85329d31e8f85e8205ac6033e46056931 target/nrf52840_mdk_dfu_merged.hex +e6dbbc68daa1b5269dce5ddbc91ea00169f9c8ed8d94a574dac1524e63c21b18 target/tab/ctap2.tab diff --git a/reproducible/reference_elf2tab_macos-10.15.txt b/reproducible/reference_elf2tab_macos-10.15.txt index 3d875e4..dd0b0b2 100644 --- a/reproducible/reference_elf2tab_macos-10.15.txt +++ b/reproducible/reference_elf2tab_macos-10.15.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 175524 (0x2ada4) bytes. - Adding .stack section. Offset: 175652 (0x2ae24). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179204 (0x2bc04) bytes. + Adding .stack section. Offset: 179332 (0x2bc84). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 175524 (0x2ada4) bytes. - Adding .stack section. Offset: 175652 (0x2ae24). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179204 (0x2bc04) bytes. + Adding .stack section. Offset: 179332 (0x2bc84). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 175524 (0x2ada4) bytes. - Adding .stack section. Offset: 175652 (0x2ae24). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179204 (0x2bc04) bytes. + Adding .stack section. Offset: 179332 (0x2bc84). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 175524 (0x2ada4) bytes. - Adding .stack section. Offset: 175652 (0x2ae24). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179204 (0x2bc04) bytes. + Adding .stack section. Offset: 179332 (0x2bc84). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 diff --git a/reproducible/reference_elf2tab_ubuntu-18.04.txt b/reproducible/reference_elf2tab_ubuntu-18.04.txt index f9e5fcb..6b3b7a2 100644 --- a/reproducible/reference_elf2tab_ubuntu-18.04.txt +++ b/reproducible/reference_elf2tab_ubuntu-18.04.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 174820 (0x2aae4) bytes. - Adding .stack section. Offset: 174948 (0x2ab64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179236 (0x2bc24) bytes. + Adding .stack section. Offset: 179364 (0x2bca4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 174820 (0x2aae4) bytes. - Adding .stack section. Offset: 174948 (0x2ab64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179236 (0x2bc24) bytes. + Adding .stack section. Offset: 179364 (0x2bca4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 174820 (0x2aae4) bytes. - Adding .stack section. Offset: 174948 (0x2ab64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179236 (0x2bc24) bytes. + Adding .stack section. Offset: 179364 (0x2bca4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 174820 (0x2aae4) bytes. - Adding .stack section. Offset: 174948 (0x2ab64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 179236 (0x2bc24) bytes. + Adding .stack section. Offset: 179364 (0x2bca4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 diff --git a/src/ctap/command.rs b/src/ctap/command.rs index ba44147..ef1e6ed 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -131,26 +131,31 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut param_map = extract_map(cbor_value)?; + destructure_cbor_map! { + let { + 1 => client_data_hash, + 2 => rp, + 3 => user, + 4 => cred_param_vec, + 5 => exclude_list, + 6 => extensions, + 7 => options, + 8 => pin_uv_auth_param, + 9 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } - let client_data_hash = - extract_byte_string(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?; + let client_data_hash = extract_byte_string(ok_or_missing(client_data_hash)?)?; + let rp = PublicKeyCredentialRpEntity::try_from(ok_or_missing(rp)?)?; + let user = PublicKeyCredentialUserEntity::try_from(ok_or_missing(user)?)?; - let rp = PublicKeyCredentialRpEntity::try_from(ok_or_missing( - param_map.remove(&cbor_unsigned!(2)), - )?)?; - - let user = PublicKeyCredentialUserEntity::try_from(ok_or_missing( - param_map.remove(&cbor_unsigned!(3)), - )?)?; - - let cred_param_vec = extract_array(ok_or_missing(param_map.remove(&cbor_unsigned!(4)))?)?; + let cred_param_vec = extract_array(ok_or_missing(cred_param_vec)?)?; let pub_key_cred_params = cred_param_vec .into_iter() .map(PublicKeyCredentialParameter::try_from) .collect::, Ctap2StatusCode>>()?; - let exclude_list = match param_map.remove(&cbor_unsigned!(5)) { + let exclude_list = match exclude_list { Some(entry) => { let exclude_list_vec = extract_array(entry)?; let list_len = MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(exclude_list_vec.len()); @@ -164,12 +169,11 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { None => None, }; - let extensions = param_map - .remove(&cbor_unsigned!(6)) + let extensions = extensions .map(MakeCredentialExtensions::try_from) .transpose()?; - let options = match param_map.remove(&cbor_unsigned!(7)) { + let options = match options { Some(entry) => MakeCredentialOptions::try_from(entry)?, None => MakeCredentialOptions { rk: false, @@ -177,15 +181,8 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { }, }; - let pin_uv_auth_param = param_map - .remove(&cbor_unsigned!(8)) - .map(extract_byte_string) - .transpose()?; - - let pin_uv_auth_protocol = param_map - .remove(&cbor_unsigned!(9)) - .map(extract_unsigned) - .transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -217,14 +214,22 @@ impl TryFrom for AuthenticatorGetAssertionParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut param_map = extract_map(cbor_value)?; + destructure_cbor_map! { + let { + 1 => rp_id, + 2 => client_data_hash, + 3 => allow_list, + 4 => extensions, + 5 => options, + 6 => pin_uv_auth_param, + 7 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } - let rp_id = extract_text_string(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?; + let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; + let client_data_hash = extract_byte_string(ok_or_missing(client_data_hash)?)?; - let client_data_hash = - extract_byte_string(ok_or_missing(param_map.remove(&cbor_unsigned!(2)))?)?; - - let allow_list = match param_map.remove(&cbor_unsigned!(3)) { + let allow_list = match allow_list { Some(entry) => { let allow_list_vec = extract_array(entry)?; let list_len = MAX_CREDENTIAL_COUNT_IN_LIST.unwrap_or(allow_list_vec.len()); @@ -238,12 +243,11 @@ impl TryFrom for AuthenticatorGetAssertionParameters { None => None, }; - let extensions = param_map - .remove(&cbor_unsigned!(4)) + let extensions = extensions .map(GetAssertionExtensions::try_from) .transpose()?; - let options = match param_map.remove(&cbor_unsigned!(5)) { + let options = match options { Some(entry) => GetAssertionOptions::try_from(entry)?, None => GetAssertionOptions { up: true, @@ -251,15 +255,8 @@ impl TryFrom for AuthenticatorGetAssertionParameters { }, }; - let pin_uv_auth_param = param_map - .remove(&cbor_unsigned!(6)) - .map(extract_byte_string) - .transpose()?; - - let pin_uv_auth_protocol = param_map - .remove(&cbor_unsigned!(7)) - .map(extract_unsigned) - .transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; Ok(AuthenticatorGetAssertionParameters { rp_id, @@ -287,33 +284,26 @@ impl TryFrom for AuthenticatorClientPinParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut param_map = extract_map(cbor_value)?; + destructure_cbor_map! { + let { + 1 => pin_protocol, + 2 => sub_command, + 3 => key_agreement, + 4 => pin_auth, + 5 => new_pin_enc, + 6 => pin_hash_enc, + } = extract_map(cbor_value)?; + } - let pin_protocol = extract_unsigned(ok_or_missing(param_map.remove(&cbor_unsigned!(1)))?)?; - - let sub_command = - ClientPinSubCommand::try_from(ok_or_missing(param_map.remove(&cbor_unsigned!(2)))?)?; - - let key_agreement = param_map - .remove(&cbor_unsigned!(3)) + let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; + let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; + let key_agreement = key_agreement .map(extract_map) .transpose()? .map(|x| CoseKey(x)); - - let pin_auth = param_map - .remove(&cbor_unsigned!(4)) - .map(extract_byte_string) - .transpose()?; - - let new_pin_enc = param_map - .remove(&cbor_unsigned!(5)) - .map(extract_byte_string) - .transpose()?; - - let pin_hash_enc = param_map - .remove(&cbor_unsigned!(6)) - .map(extract_byte_string) - .transpose()?; + let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?; + let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?; Ok(AuthenticatorClientPinParameters { pin_protocol, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 33dec12..b5799b4 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -31,16 +31,18 @@ impl TryFrom for PublicKeyCredentialRpEntity { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut rp_map = extract_map(cbor_value)?; - let rp_id = extract_text_string(ok_or_missing(rp_map.remove(&cbor_text!("id")))?)?; - let rp_name = rp_map - .remove(&cbor_text!("name")) - .map(extract_text_string) - .transpose()?; - let rp_icon = rp_map - .remove(&cbor_text!("icon")) - .map(extract_text_string) - .transpose()?; + destructure_cbor_map! { + let { + "id" => rp_id, + "icon" => rp_icon, + "name" => rp_name, + } = extract_map(cbor_value)?; + } + + let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; + let rp_name = rp_name.map(extract_text_string).transpose()?; + let rp_icon = rp_icon.map(extract_text_string).transpose()?; + Ok(Self { rp_id, rp_name, @@ -62,20 +64,20 @@ impl TryFrom for PublicKeyCredentialUserEntity { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut user_map = extract_map(cbor_value)?; - let user_id = extract_byte_string(ok_or_missing(user_map.remove(&cbor_text!("id")))?)?; - let user_name = user_map - .remove(&cbor_text!("name")) - .map(extract_text_string) - .transpose()?; - let user_display_name = user_map - .remove(&cbor_text!("displayName")) - .map(extract_text_string) - .transpose()?; - let user_icon = user_map - .remove(&cbor_text!("icon")) - .map(extract_text_string) - .transpose()?; + destructure_cbor_map! { + let { + "id" => user_id, + "icon" => user_icon, + "name" => user_name, + "displayName" => user_display_name, + } = extract_map(cbor_value)?; + } + + let user_id = extract_byte_string(ok_or_missing(user_id)?)?; + let user_name = user_name.map(extract_text_string).transpose()?; + let user_display_name = user_display_name.map(extract_text_string).transpose()?; + let user_icon = user_icon.map(extract_text_string).transpose()?; + Ok(Self { user_id, user_name, @@ -141,13 +143,15 @@ impl TryFrom for PublicKeyCredentialParameter { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut cred_param_map = extract_map(cbor_value)?; - let cred_type = PublicKeyCredentialType::try_from(ok_or_missing( - cred_param_map.remove(&cbor_text!("type")), - )?)?; - let alg = SignatureAlgorithm::try_from(ok_or_missing( - cred_param_map.remove(&cbor_text!("alg")), - )?)?; + destructure_cbor_map! { + let { + "alg" => alg, + "type" => cred_type, + } = extract_map(cbor_value)?; + } + + let cred_type = PublicKeyCredentialType::try_from(ok_or_missing(cred_type)?)?; + let alg = SignatureAlgorithm::try_from(ok_or_missing(alg)?)?; Ok(Self { cred_type, alg }) } } @@ -209,12 +213,17 @@ impl TryFrom for PublicKeyCredentialDescriptor { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut cred_desc_map = extract_map(cbor_value)?; - let key_type = PublicKeyCredentialType::try_from(ok_or_missing( - cred_desc_map.remove(&cbor_text!("type")), - )?)?; - let key_id = extract_byte_string(ok_or_missing(cred_desc_map.remove(&cbor_text!("id")))?)?; - let transports = match cred_desc_map.remove(&cbor_text!("transports")) { + destructure_cbor_map! { + let { + "id" => key_id, + "type" => key_type, + "transports" => transports, + } = extract_map(cbor_value)?; + } + + let key_type = PublicKeyCredentialType::try_from(ok_or_missing(key_type)?)?; + let key_id = extract_byte_string(ok_or_missing(key_id)?)?; + let transports = match transports { Some(exclude_entry) => { let transport_vec = extract_array(exclude_entry)?; let transports = transport_vec @@ -225,6 +234,7 @@ impl TryFrom for PublicKeyCredentialDescriptor { } None => None, }; + Ok(Self { key_type, key_id, @@ -253,12 +263,15 @@ impl TryFrom for MakeCredentialExtensions { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut extensions_map = extract_map(cbor_value)?; - let hmac_secret = extensions_map - .remove(&cbor_text!("hmac-secret")) - .map_or(Ok(false), extract_bool)?; - let cred_protect = extensions_map - .remove(&cbor_text!("credProtect")) + destructure_cbor_map! { + let { + "credProtect" => cred_protect, + "hmac-secret" => hmac_secret, + } = extract_map(cbor_value)?; + } + + let hmac_secret = hmac_secret.map_or(Ok(false), extract_bool)?; + let cred_protect = cred_protect .map(CredentialProtectionPolicy::try_from) .transpose()?; Ok(Self { @@ -277,9 +290,13 @@ impl TryFrom for GetAssertionExtensions { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut extensions_map = extract_map(cbor_value)?; - let hmac_secret = extensions_map - .remove(&cbor_text!("hmac-secret")) + destructure_cbor_map! { + let { + "hmac-secret" => hmac_secret, + } = extract_map(cbor_value)?; + } + + let hmac_secret = hmac_secret .map(GetAssertionHmacSecretInput::try_from) .transpose()?; Ok(Self { hmac_secret }) @@ -297,10 +314,17 @@ impl TryFrom for GetAssertionHmacSecretInput { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut input_map = extract_map(cbor_value)?; - let cose_key = extract_map(ok_or_missing(input_map.remove(&cbor_unsigned!(1)))?)?; - let salt_enc = extract_byte_string(ok_or_missing(input_map.remove(&cbor_unsigned!(2)))?)?; - let salt_auth = extract_byte_string(ok_or_missing(input_map.remove(&cbor_unsigned!(3)))?)?; + destructure_cbor_map! { + let { + 1 => cose_key, + 2 => salt_enc, + 3 => salt_auth, + } = extract_map(cbor_value)?; + } + + let cose_key = extract_map(ok_or_missing(cose_key)?)?; + let salt_enc = extract_byte_string(ok_or_missing(salt_enc)?)?; + let salt_auth = extract_byte_string(ok_or_missing(salt_auth)?)?; Ok(Self { key_agreement: CoseKey(cose_key), salt_enc, @@ -320,17 +344,24 @@ impl TryFrom for MakeCredentialOptions { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut options_map = extract_map(cbor_value)?; - let rk = match options_map.remove(&cbor_text!("rk")) { + destructure_cbor_map! { + let { + "rk" => rk, + "up" => up, + "uv" => uv, + } = extract_map(cbor_value)?; + } + + let rk = match rk { Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if let Some(options_entry) = options_map.remove(&cbor_text!("up")) { + if let Some(options_entry) = up { if !extract_bool(options_entry)? { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } } - let uv = match options_map.remove(&cbor_text!("uv")) { + let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, None => false, }; @@ -348,17 +379,24 @@ impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - let mut options_map = extract_map(cbor_value)?; - if let Some(options_entry) = options_map.remove(&cbor_text!("rk")) { + destructure_cbor_map! { + let { + "rk" => rk, + "up" => up, + "uv" => uv, + } = extract_map(cbor_value)?; + } + + if let Some(options_entry) = rk { // This is only for returning the correct status code. extract_bool(options_entry)?; return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } - let up = match options_map.remove(&cbor_text!("up")) { + let up = match up { Some(options_entry) => extract_bool(options_entry)?, None => true, }; - let uv = match options_map.remove(&cbor_text!("uv")) { + let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, None => false, }; @@ -501,27 +539,33 @@ impl TryFrom for PublicKeyCredentialSource { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - use PublicKeyCredentialSourceField::*; - let mut map = extract_map(cbor_value)?; - let credential_id = extract_byte_string(ok_or_missing(map.remove(&CredentialId.into()))?)?; - let private_key = extract_byte_string(ok_or_missing(map.remove(&PrivateKey.into()))?)?; + use PublicKeyCredentialSourceField::{ + CredProtectPolicy, CredRandom, CredentialId, OtherUi, PrivateKey, RpId, UserHandle, + }; + destructure_cbor_map! { + let { + CredentialId => credential_id, + PrivateKey => private_key, + RpId => rp_id, + UserHandle => user_handle, + OtherUi => other_ui, + CredRandom => cred_random, + CredProtectPolicy => cred_protect_policy, + } = extract_map(cbor_value)?; + } + + let credential_id = extract_byte_string(ok_or_missing(credential_id)?)?; + let private_key = extract_byte_string(ok_or_missing(private_key)?)?; if private_key.len() != 32 { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); } let private_key = ecdsa::SecKey::from_bytes(array_ref!(private_key, 0, 32)) .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)?; - let rp_id = extract_text_string(ok_or_missing(map.remove(&RpId.into()))?)?; - let user_handle = extract_byte_string(ok_or_missing(map.remove(&UserHandle.into()))?)?; - let other_ui = map - .remove(&OtherUi.into()) - .map(extract_text_string) - .transpose()?; - let cred_random = map - .remove(&CredRandom.into()) - .map(extract_byte_string) - .transpose()?; - let cred_protect_policy = map - .remove(&CredProtectPolicy.into()) + let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; + let user_handle = extract_byte_string(ok_or_missing(user_handle)?)?; + let other_ui = other_ui.map(extract_text_string).transpose()?; + let cred_random = cred_random.map(extract_byte_string).transpose()?; + let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that @@ -599,27 +643,37 @@ impl TryFrom for ecdh::PubKey { type Error = Ctap2StatusCode; fn try_from(cose_key: CoseKey) -> Result { - let mut cose_map = cose_key.0; - let key_type = extract_integer(ok_or_missing(cose_map.remove(&cbor_int!(1)))?)?; + destructure_cbor_map! { + let { + 1 => key_type, + 3 => algorithm, + -1 => curve, + -2 => x_bytes, + -3 => y_bytes, + } = cose_key.0; + } + + let key_type = extract_integer(ok_or_missing(key_type)?)?; if key_type != EC2_KEY_TYPE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let algorithm = extract_integer(ok_or_missing(cose_map.remove(&cbor_int!(3)))?)?; + let algorithm = extract_integer(ok_or_missing(algorithm)?)?; if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let curve = extract_integer(ok_or_missing(cose_map.remove(&cbor_int!(-1)))?)?; + let curve = extract_integer(ok_or_missing(curve)?)?; if curve != P_256_CURVE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let x_bytes = extract_byte_string(ok_or_missing(cose_map.remove(&cbor_int!(-2)))?)?; + let x_bytes = extract_byte_string(ok_or_missing(x_bytes)?)?; if x_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let y_bytes = extract_byte_string(ok_or_missing(cose_map.remove(&cbor_int!(-3)))?)?; + let y_bytes = extract_byte_string(ok_or_missing(y_bytes)?)?; if y_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } + let x_array_ref = array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES]; let y_array_ref = array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES]; ecdh::PubKey::from_coordinates(x_array_ref, y_array_ref) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 35bc487..027e91f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -38,7 +38,6 @@ use self::data_formats::{ SignatureAlgorithm, }; use self::hid::ChannelID; -use self::key_material::{AAGUID, ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; use self::response::{ AuthenticatorClientPinResponse, AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, ResponseData, @@ -531,7 +530,7 @@ where }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags); - auth_data.extend(AAGUID); + auth_data.extend(self.persistent_store.aaguid()?); // The length is fixed to 0x20 or 0x70 and fits one byte. if credential_id.len() > 0xFF { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG); @@ -556,18 +555,25 @@ where let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - let (signature, x5c) = if USE_BATCH_ATTESTATION { - let attestation_key = - crypto::ecdsa::SecKey::from_bytes(ATTESTATION_PRIVATE_KEY).unwrap(); - ( - attestation_key.sign_rfc6979::(&signature_data), - Some(vec![ATTESTATION_CERTIFICATE.to_vec()]), - ) - } else { - ( + // We currently use the presence of the attestation private key in the persistent storage to + // decide whether batch attestation is needed. + let (signature, x5c) = match self.persistent_store.attestation_private_key()? { + Some(attestation_private_key) => { + let attestation_key = + crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); + let attestation_certificate = self + .persistent_store + .attestation_certificate()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + ( + attestation_key.sign_rfc6979::(&signature_data), + Some(vec![attestation_certificate]), + ) + } + None => ( sk.sign_rfc6979::(&signature_data), None, - ) + ), }; let attestation_statement = PackedAttestationStatement { alg: SignatureAlgorithm::ES256 as i64, @@ -794,7 +800,7 @@ where String::from(FIDO2_VERSION_STRING), ], extensions: Some(vec![String::from("hmac-secret")]), - aaguid: *AAGUID, + aaguid: *self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), pin_protocols: Some(vec![ @@ -1160,7 +1166,7 @@ mod test { 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, ]); - expected_response.extend(AAGUID); + expected_response.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend(&[ 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, @@ -1259,7 +1265,7 @@ mod test { 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; - expected_auth_data.extend(AAGUID); + expected_auth_data.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); assert_eq!( auth_data[0..expected_auth_data.len()], @@ -1296,7 +1302,7 @@ mod test { 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; - expected_auth_data.extend(AAGUID); + expected_auth_data.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, ENCRYPTED_CREDENTIAL_ID_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], @@ -1439,7 +1445,7 @@ mod test { 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, ]; - expected_auth_data.extend(AAGUID); + expected_auth_data.extend(ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); assert_eq!( auth_data[0..expected_auth_data.len()], diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 1e0c2bf..b58b8d0 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -67,5 +67,11 @@ pub enum Ctap2StatusCode { // CTAP2_ERR_VENDOR_FIRST = 0xF0, CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0, CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1, + + /// An internal invariant is broken. + /// + /// This type of error is unexpected and the current state is undefined. + CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2, + CTAP2_ERR_VENDOR_LAST = 0xFF, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 4e33ba1..60dd99d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -15,7 +15,7 @@ use crate::crypto::rng256::Rng256; use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; use crate::ctap::status_code::Ctap2StatusCode; -use crate::ctap::PIN_AUTH_LENGTH; +use crate::ctap::{key_material, PIN_AUTH_LENGTH, USE_BATCH_ATTESTATION}; use alloc::string::String; use alloc::vec::Vec; use core::convert::TryInto; @@ -56,9 +56,14 @@ const GLOBAL_SIGNATURE_COUNTER: usize = 1; const MASTER_KEYS: usize = 2; const PIN_HASH: usize = 3; const PIN_RETRIES: usize = 4; -const NUM_TAGS: usize = 5; +const ATTESTATION_PRIVATE_KEY: usize = 5; +const ATTESTATION_CERTIFICATE: usize = 6; +const AAGUID: usize = 7; +const NUM_TAGS: usize = 8; const MAX_PIN_RETRIES: u8 = 6; +const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; +const AAGUID_LENGTH: usize = 16; #[derive(PartialEq, Eq, PartialOrd, Ord)] enum Key { @@ -73,6 +78,9 @@ enum Key { MasterKeys, PinHash, PinRetries, + AttestationPrivateKey, + AttestationCertificate, + Aaguid, } pub struct MasterKeys<'a> { @@ -124,6 +132,9 @@ impl StoreConfig for Config { MASTER_KEYS => add(Key::MasterKeys), PIN_HASH => add(Key::PinHash), PIN_RETRIES => add(Key::PinRetries), + ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey), + ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate), + AAGUID => add(Key::Aaguid), _ => debug_assert!(false), } } @@ -197,6 +208,20 @@ impl PersistentStore { }) .unwrap(); } + // The following 3 entries are meant to be written by vendor-specific commands. + if USE_BATCH_ATTESTATION { + if self.store.find_one(&Key::AttestationPrivateKey).is_none() { + self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .unwrap(); + } + if self.store.find_one(&Key::AttestationCertificate).is_none() { + self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .unwrap(); + } + } + if self.store.find_one(&Key::Aaguid).is_none() { + self.set_aaguid(key_material::AAGUID).unwrap(); + } } pub fn find_credential( @@ -395,10 +420,88 @@ impl PersistentStore { .unwrap(); } + pub fn attestation_private_key( + &self, + ) -> Result, Ctap2StatusCode> { + let data = match self.store.find_one(&Key::AttestationPrivateKey) { + None => return Ok(None), + Some((_, entry)) => entry.data, + }; + if data.len() != ATTESTATION_PRIVATE_KEY_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + Ok(Some(array_ref!(data, 0, ATTESTATION_PRIVATE_KEY_LENGTH))) + } + + pub fn set_attestation_private_key( + &mut self, + attestation_private_key: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH], + ) -> Result<(), Ctap2StatusCode> { + let entry = StoreEntry { + tag: ATTESTATION_PRIVATE_KEY, + data: attestation_private_key, + sensitive: false, + }; + match self.store.find_one(&Key::AttestationPrivateKey) { + None => self.store.insert(entry)?, + Some((index, _)) => self.store.replace(index, entry)?, + } + Ok(()) + } + + pub fn attestation_certificate(&self) -> Result>, Ctap2StatusCode> { + let data = match self.store.find_one(&Key::AttestationCertificate) { + None => return Ok(None), + Some((_, entry)) => entry.data, + }; + Ok(Some(data.to_vec())) + } + + pub fn set_attestation_certificate( + &mut self, + attestation_certificate: &[u8], + ) -> Result<(), Ctap2StatusCode> { + let entry = StoreEntry { + tag: ATTESTATION_CERTIFICATE, + data: attestation_certificate, + sensitive: false, + }; + match self.store.find_one(&Key::AttestationCertificate) { + None => self.store.insert(entry)?, + Some((index, _)) => self.store.replace(index, entry)?, + } + Ok(()) + } + + pub fn aaguid(&self) -> Result<&[u8; AAGUID_LENGTH], Ctap2StatusCode> { + let (_, entry) = self + .store + .find_one(&Key::Aaguid) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + let data = entry.data; + if data.len() != AAGUID_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + Ok(array_ref!(data, 0, AAGUID_LENGTH)) + } + + pub fn set_aaguid(&mut self, aaguid: &[u8; AAGUID_LENGTH]) -> Result<(), Ctap2StatusCode> { + let entry = StoreEntry { + tag: AAGUID, + data: aaguid, + sensitive: false, + }; + match self.store.find_one(&Key::Aaguid) { + None => self.store.insert(entry)?, + Some((index, _)) => self.store.replace(index, entry)?, + } + Ok(()) + } + pub fn reset(&mut self, rng: &mut impl Rng256) { loop { let index = { - let mut iter = self.store.iter(); + let mut iter = self.store.iter().filter(|(_, entry)| should_reset(entry)); match iter.next() { None => break, Some((index, _)) => index, @@ -420,6 +523,13 @@ impl From for Ctap2StatusCode { } } +fn should_reset(entry: &StoreEntry<'_>) -> bool { + match entry.tag { + ATTESTATION_PRIVATE_KEY | ATTESTATION_CERTIFICATE | AAGUID => false, + _ => true, + } +} + fn deserialize_credential(data: &[u8]) -> Option { let cbor = cbor::read(data).ok()?; cbor.try_into().ok() @@ -745,4 +855,41 @@ mod test { persistent_store.reset_pin_retries(); assert_eq!(persistent_store.pin_retries(), MAX_PIN_RETRIES); } + + #[test] + fn test_persistent_keys() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // Make sure the attestation are absent. There is no batch attestation in tests. + assert!(persistent_store + .attestation_private_key() + .unwrap() + .is_none()); + assert!(persistent_store + .attestation_certificate() + .unwrap() + .is_none()); + + // Make sure the persistent keys are initialized. + persistent_store + .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .unwrap(); + persistent_store + .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .unwrap(); + assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID); + + // The persistent keys stay initialized and preserve their value after a reset. + persistent_store.reset(&mut rng); + assert_eq!( + persistent_store.attestation_private_key().unwrap().unwrap(), + key_material::ATTESTATION_PRIVATE_KEY + ); + assert_eq!( + persistent_store.attestation_certificate().unwrap().unwrap(), + key_material::ATTESTATION_CERTIFICATE + ); + assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID); + } }