From edcc206e9d1cfbc4616d4ef6adb33c4f24ebba18 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 18:29:31 +0100 Subject: [PATCH 01/86] Make store operations constant wrt flash operations --- examples/store_latency.rs | 14 +- libraries/persistent_store/src/format.rs | 8 + libraries/persistent_store/src/lib.rs | 1 + libraries/persistent_store/src/store.rs | 260 +++++++++++++---------- src/ctap/storage.rs | 2 +- 5 files changed, 162 insertions(+), 123 deletions(-) diff --git a/examples/store_latency.rs b/examples/store_latency.rs index c3f3e71..fd6f504 100644 --- a/examples/store_latency.rs +++ b/examples/store_latency.rs @@ -124,15 +124,15 @@ fn main() { compute_latency(&timer, 20, 1, 50); // Those overwritten 1 word entries simulate counters. compute_latency(&timer, 3, 0, 1); - compute_latency(&timer, 6, 0, 1); + compute_latency(&timer, 20, 0, 1); writeln!(Console::new(), "\nDone.").unwrap(); // Results on nrf52840dk: // - // | Pages | Overwrite | Length | Boot | Compaction | Insert | Remove | - // | ----- | --------- | --------- | ------- | ---------- | ------ | ------- | - // | 3 | no | 50 words | 2.0 ms | 132.5 ms | 4.8 ms | 1.2 ms | - // | 20 | no | 50 words | 7.4 ms | 135.5 ms | 10.2 ms | 3.9 ms | - // | 3 | yes | 1 word | 21.9 ms | 94.5 ms | 12.4 ms | 5.9 ms | - // | 6 | yes | 1 word | 55.2 ms | 100.8 ms | 24.8 ms | 12.1 ms | + // | Pages | Overwrite | Length | Boot | Compaction | Insert | Remove | + // | ----- | --------- | --------- | ------- | ---------- | ------ | ------ | + // | 3 | no | 50 words | 2.0 ms | 132.8 ms | 4.3 ms | 1.2 ms | + // | 20 | no | 50 words | 7.8 ms | 135.7 ms | 9.9 ms | 4.0 ms | + // | 3 | yes | 1 word | 19.6 ms | 90.8 ms | 4.7 ms | 2.3 ms | + // | 20 | yes | 1 word | 183.3 ms | 90.9 ms | 4.8 ms | 2.3 ms | } diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 1f87ef3..8de88e4 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -1077,4 +1077,12 @@ mod tests { 0xff800000 ); } + + #[test] + fn position_offsets_fit_in_a_halfword() { + // The store stores the entry positions as their offset from the head. Those offsets are + // represented as u16. The bound below is a large over-approximation of the maximal offset + // but it already fits. + assert_eq!((MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE, 0xff80); + } } diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index c8be44b..06a3a68 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -344,6 +344,7 @@ //! storage, the store is checked not to crash. #![cfg_attr(not(feature = "std"), no_std)] +#![feature(try_trait)] #[macro_use] extern crate alloc; diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index 2559485..ba7ab4b 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -23,8 +23,11 @@ use crate::{usize_to_nat, Nat, Storage, StorageError, StorageIndex}; pub use crate::{ BufferStorage, StoreDriver, StoreDriverOff, StoreDriverOn, StoreInterruption, StoreInvariant, }; +use alloc::boxed::Box; use alloc::vec::Vec; use core::cmp::{max, min, Ordering}; +use core::convert::TryFrom; +use core::option::NoneError; #[cfg(feature = "std")] use std::collections::HashSet; @@ -75,6 +78,14 @@ impl From for StoreError { } } +impl From for StoreError { + fn from(error: NoneError) -> StoreError { + match error { + NoneError => StoreError::InvalidStorage, + } + } +} + /// Result of store operations. pub type StoreResult = Result; @@ -174,6 +185,8 @@ impl StoreUpdate { } } +pub type StoreIter<'a> = Box> + 'a>; + /// Implements a store with a map interface over a storage. #[derive(Clone)] pub struct Store { @@ -182,6 +195,14 @@ pub struct Store { /// The storage configuration. format: Format, + + /// The position of the first word in the store. + head: Option, + + /// The list of the position of the user entries. + /// + /// The position is encoded as the word offset from the [head](Store#structfield.head). + entries: Option>, } impl Store { @@ -199,7 +220,12 @@ impl Store { None => return Err((StoreError::InvalidArgument, storage)), Some(x) => x, }; - let mut store = Store { storage, format }; + let mut store = Store { + storage, + format, + head: None, + entries: None, + }; if let Err(error) = store.recover() { return Err((error, store.storage)); } @@ -207,8 +233,19 @@ impl Store { } /// Iterates over the entries. - pub fn iter<'a>(&'a self) -> StoreResult> { - StoreIter::new(self) + pub fn iter<'a>(&'a self) -> StoreResult> { + let head = self.head?; + Ok(Box::new(self.entries.as_ref()?.iter().map( + move |&offset| { + let pos = head + offset as Nat; + match self.parse_entry(&mut pos.clone())? { + ParsedEntry::User(Header { + key, length: len, .. + }) => Ok(StoreHandle { key, pos, len }), + _ => Err(StoreError::InvalidStorage), + } + }, + ))) } /// Returns the current capacity in words. @@ -217,16 +254,9 @@ impl Store { pub fn capacity(&self) -> StoreResult { let total = self.format.total_capacity(); let mut used = 0; - let mut pos = self.head()?; - let end = pos + self.format.virt_size(); - while pos < end { - let entry_pos = pos; - match self.parse_entry(&mut pos)? { - ParsedEntry::Tail => break, - ParsedEntry::Padding => (), - ParsedEntry::User(_) => used += pos - entry_pos, - _ => return Err(StoreError::InvalidStorage), - } + for handle in self.iter()? { + let handle = handle?; + used += 1 + self.format.bytes_to_words(handle.len); } Ok(StoreRatio { used, total }) } @@ -381,6 +411,7 @@ impl Store { let footer = entry_len / word_size - 1; self.write_slice(tail, &entry[..(footer * word_size) as usize])?; self.write_slice(tail + footer, &entry[(footer * word_size) as usize..])?; + self.push_entry(tail)?; self.insert_init(tail, footer, key) } @@ -398,7 +429,8 @@ impl Store { /// Removes an entry given a handle. pub fn remove_handle(&mut self, handle: &StoreHandle) -> StoreResult<()> { self.check_handle(handle)?; - self.delete_pos(handle.pos, self.format.bytes_to_words(handle.len)) + self.delete_pos(handle.pos, self.format.bytes_to_words(handle.len))?; + self.remove_entry(handle.pos) } /// Returns the maximum length in bytes of a value. @@ -460,7 +492,9 @@ impl Store { /// Recovers a possible compaction interrupted while copying the entries. fn recover_compaction(&mut self) -> StoreResult<()> { - let head_page = self.head()?.page(&self.format); + let head = self.get_extremum_page_head(Ordering::Less)?; + self.head = Some(head); + let head_page = head.page(&self.format); match self.parse_compact(head_page)? { WordState::Erased => Ok(()), WordState::Partial => self.compact(), @@ -470,14 +504,15 @@ impl Store { /// Recover a possible interrupted operation which is not a compaction. fn recover_operation(&mut self) -> StoreResult<()> { - let mut pos = self.head()?; + self.entries = Some(Vec::new()); + let mut pos = self.head?; let mut prev_pos = pos; let end = pos + self.format.virt_size(); while pos < end { let entry_pos = pos; match self.parse_entry(&mut pos)? { ParsedEntry::Tail => break, - ParsedEntry::User(_) => (), + ParsedEntry::User(_) => self.push_entry(entry_pos)?, ParsedEntry::Padding => { self.wipe_span(entry_pos + 1, pos - entry_pos - 1)?; } @@ -610,7 +645,7 @@ impl Store { /// /// In particular, the handle has not been compacted. fn check_handle(&self, handle: &StoreHandle) -> StoreResult<()> { - if handle.pos < self.head()? { + if handle.pos < self.head? { Err(StoreError::InvalidArgument) } else { Ok(()) @@ -640,7 +675,7 @@ impl Store { /// Compacts one page. fn compact(&mut self) -> StoreResult<()> { - let head = self.head()?; + let head = self.head?; if head.cycle(&self.format) >= self.format.max_page_erases() { return Err(StoreError::NoLifetime); } @@ -653,7 +688,7 @@ impl Store { /// Continues a compaction after its compact page info has been written. fn compact_copy(&mut self) -> StoreResult<()> { - let mut head = self.head()?; + let mut head = self.head?; let page = head.page(&self.format); let end = head.next_page(&self.format); let mut tail = match self.parse_compact(page)? { @@ -667,8 +702,12 @@ impl Store { let pos = head; match self.parse_entry(&mut head)? { ParsedEntry::Tail => break, + // This can happen if we copy to the next page. We actually reached the tail but we + // read what we just copied. + ParsedEntry::Partial if head > end => break, ParsedEntry::User(_) => (), - _ => continue, + ParsedEntry::Padding => continue, + _ => return Err(StoreError::InvalidStorage), }; let length = head - pos; // We have to copy the slice for 2 reasons: @@ -676,7 +715,9 @@ impl Store { // 2. We can't pass a flash slice to the kernel. This should get fixed with // https://github.com/tock/tock/issues/1274. let entry = self.read_slice(pos, length * self.format.word_size()); + self.remove_entry(pos)?; self.write_slice(tail, &entry)?; + self.push_entry(tail)?; self.init_page(tail, tail + (length - 1))?; tail += length; } @@ -688,14 +729,31 @@ impl Store { /// Continues a compaction after its erase entry has been written. fn compact_erase(&mut self, erase: Position) -> StoreResult<()> { - let page = match self.parse_entry(&mut erase.clone())? { + // Read the page to erase from the erase entry. + let mut page = match self.parse_entry(&mut erase.clone())? { ParsedEntry::Internal(InternalEntry::Erase { page }) => page, _ => return Err(StoreError::InvalidStorage), }; + // Erase the page. self.storage_erase_page(page)?; - let head = self.head()?; + // Update the head. + page = (page + 1) % self.format.num_pages(); + let init = match self.parse_init(page)? { + WordState::Valid(x) => x, + _ => return Err(StoreError::InvalidStorage), + }; + let head = self.format.page_head(init, page); + if let Some(entries) = &mut self.entries { + let head_offset = u16::try_from(head - self.head?).ok()?; + for entry in entries { + *entry = entry.checked_sub(head_offset)?; + } + } + self.head = Some(head); + // Wipe the overlapping entry from the erased page. let pos = head.page_begin(&self.format); self.wipe_span(pos, head - pos)?; + // Mark the erase entry as done. self.set_padding(erase)?; Ok(()) } @@ -704,13 +762,13 @@ impl Store { fn transaction_apply(&mut self, sorted_keys: &[Nat], marker: Position) -> StoreResult<()> { self.delete_keys(&sorted_keys, marker)?; self.set_padding(marker)?; - let end = self.head()? + self.format.virt_size(); + let end = self.head? + self.format.virt_size(); let mut pos = marker + 1; while pos < end { let entry_pos = pos; match self.parse_entry(&mut pos)? { ParsedEntry::Tail => break, - ParsedEntry::User(_) => (), + ParsedEntry::User(_) => self.push_entry(entry_pos)?, ParsedEntry::Internal(InternalEntry::Remove { .. }) => { self.set_padding(entry_pos)? } @@ -727,37 +785,38 @@ impl Store { ParsedEntry::Internal(InternalEntry::Clear { min_key }) => min_key, _ => return Err(StoreError::InvalidStorage), }; - let mut pos = self.head()?; - let end = pos + self.format.virt_size(); - while pos < end { - let entry_pos = pos; - match self.parse_entry(&mut pos)? { - ParsedEntry::Internal(InternalEntry::Clear { .. }) if entry_pos == clear => break, - ParsedEntry::User(header) if header.key >= min_key => { - self.delete_pos(entry_pos, pos - entry_pos - 1)?; - } - ParsedEntry::Padding | ParsedEntry::User(_) => (), - _ => return Err(StoreError::InvalidStorage), - } - } + self.delete_if(clear, |key| key >= min_key)?; self.set_padding(clear)?; Ok(()) } /// Deletes a set of entries up to a certain position. fn delete_keys(&mut self, sorted_keys: &[Nat], end: Position) -> StoreResult<()> { - let mut pos = self.head()?; - while pos < end { - let entry_pos = pos; - match self.parse_entry(&mut pos)? { - ParsedEntry::Tail => break, - ParsedEntry::User(header) if sorted_keys.binary_search(&header.key).is_ok() => { - self.delete_pos(entry_pos, pos - entry_pos - 1)?; - } - ParsedEntry::Padding | ParsedEntry::User(_) => (), + self.delete_if(end, |key| sorted_keys.binary_search(&key).is_ok()) + } + + /// Deletes entries matching a predicate up to a certain position. + fn delete_if(&mut self, end: Position, delete: impl Fn(Nat) -> bool) -> StoreResult<()> { + let head = self.head?; + let mut entries = self.entries.take()?; + let mut i = 0; + while i < entries.len() { + let pos = head + entries[i] as Nat; + if pos >= end { + break; + } + let header = match self.parse_entry(&mut pos.clone())? { + ParsedEntry::User(x) => x, _ => return Err(StoreError::InvalidStorage), + }; + if delete(header.key) { + self.delete_pos(pos, self.format.bytes_to_words(header.length))?; + entries.swap_remove(i); + } else { + i += 1; } } + self.entries = Some(entries); Ok(()) } @@ -836,19 +895,20 @@ impl Store { } } // There is always at least one initialized page. - best.ok_or(StoreError::InvalidStorage) + Ok(best?) } /// Returns the number of words that can be written without compaction. fn immediate_capacity(&self) -> StoreResult { let tail = self.tail()?; - let end = self.head()? + self.format.virt_size(); + let end = self.head? + self.format.virt_size(); Ok(end.get().saturating_sub(tail.get())) } /// Returns the position of the first word in the store. + #[cfg(feature = "std")] pub(crate) fn head(&self) -> StoreResult { - self.get_extremum_page_head(Ordering::Less) + Ok(self.head?) } /// Returns one past the position of the last word in the store. @@ -863,6 +923,30 @@ impl Store { Ok(pos) } + fn push_entry(&mut self, pos: Position) -> StoreResult<()> { + let entries = match &mut self.entries { + None => return Ok(()), + Some(x) => x, + }; + let head = self.head?; + let offset = u16::try_from(pos - head).ok()?; + debug_assert!(!entries.contains(&offset)); + entries.push(offset); + Ok(()) + } + + fn remove_entry(&mut self, pos: Position) -> StoreResult<()> { + let entries = match &mut self.entries { + None => return Ok(()), + Some(x) => x, + }; + let head = self.head?; + let offset = u16::try_from(pos - head).ok()?; + let i = entries.iter().position(|x| *x == offset)?; + entries.swap_remove(i); + Ok(()) + } + /// Parses the entry at a given position. /// /// The position is updated to point to the next entry. @@ -1061,7 +1145,7 @@ impl Store { /// If the value has been partially compacted, only return the non-compacted part. Returns an /// empty value if it has been fully compacted. pub fn inspect_value(&self, handle: &StoreHandle) -> Vec { - let head = self.head().unwrap(); + let head = self.head.unwrap(); let length = self.format.bytes_to_words(handle.len); if head <= handle.pos { // The value has not been compacted. @@ -1087,20 +1171,21 @@ impl Store { store .iter() .unwrap() - .map(|x| x.unwrap()) - .filter(|x| delete_key(x.key as usize)) - .collect::>() + .filter(|x| x.is_err() || delete_key(x.as_ref().unwrap().key as usize)) + .collect::, _>>() }; match *operation { StoreOperation::Transaction { ref updates } => { let keys: HashSet = updates.iter().map(|x| x.key()).collect(); - let deleted = deleted(self, &|key| keys.contains(&key)); - (deleted, self.transaction(updates)) - } - StoreOperation::Clear { min_key } => { - let deleted = deleted(self, &|key| key >= min_key); - (deleted, self.clear(min_key)) + match deleted(self, &|key| keys.contains(&key)) { + Ok(deleted) => (deleted, self.transaction(updates)), + Err(error) => (Vec::new(), Err(error)), + } } + StoreOperation::Clear { min_key } => match deleted(self, &|key| key >= min_key) { + Ok(deleted) => (deleted, self.clear(min_key)), + Err(error) => (Vec::new(), Err(error)), + }, StoreOperation::Prepare { length } => (Vec::new(), self.prepare(length)), } } @@ -1165,61 +1250,6 @@ enum ParsedEntry { Tail, } -/// Iterates over the entries of a store. -pub struct StoreIter<'a, S: Storage> { - /// The store being iterated. - store: &'a Store, - - /// The position of the next entry. - pos: Position, - - /// Iteration stops when reaching this position. - end: Position, -} - -impl<'a, S: Storage> StoreIter<'a, S> { - /// Creates an iterator over the entries of a store. - fn new(store: &'a Store) -> StoreResult> { - let pos = store.head()?; - let end = pos + store.format.virt_size(); - Ok(StoreIter { store, pos, end }) - } -} - -impl<'a, S: Storage> StoreIter<'a, S> { - /// Returns the next entry and advances the iterator. - fn transposed_next(&mut self) -> StoreResult> { - if self.pos >= self.end { - return Ok(None); - } - while self.pos < self.end { - let entry_pos = self.pos; - match self.store.parse_entry(&mut self.pos)? { - ParsedEntry::Tail => break, - ParsedEntry::Padding => (), - ParsedEntry::User(header) => { - return Ok(Some(StoreHandle { - key: header.key, - pos: entry_pos, - len: header.length, - })) - } - _ => return Err(StoreError::InvalidStorage), - } - } - self.pos = self.end; - Ok(None) - } -} - -impl<'a, S: Storage> Iterator for StoreIter<'a, S> { - type Item = StoreResult; - - fn next(&mut self) -> Option> { - self.transposed_next().transpose() - } -} - /// Returns whether 2 slices are different. /// /// Returns an error if `target` has a bit set to one for which `source` is set to zero. diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5f17052..6b7ff20 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -532,7 +532,7 @@ struct IterCredentials<'a> { store: &'a persistent_store::Store, /// The store iterator. - iter: persistent_store::StoreIter<'a, Storage>, + iter: persistent_store::StoreIter<'a>, /// The iteration result. /// From fb15032f0b191d55dd586113fd5b528b685c082b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 18:52:13 +0100 Subject: [PATCH 02/86] Test with nightly --- .github/workflows/persistent_store_test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/persistent_store_test.yml b/.github/workflows/persistent_store_test.yml index 1a1d942..ffe10ff 100644 --- a/.github/workflows/persistent_store_test.yml +++ b/.github/workflows/persistent_store_test.yml @@ -13,6 +13,11 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + - name: Unit testing of Persistent store library (release mode) uses: actions-rs/cargo@v1 with: From 1d576fdd316027f0cf4d565cb16b2002bc631ae6 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 14 Dec 2020 21:06:12 +0100 Subject: [PATCH 03/86] Add unit-test for Store::entries --- libraries/persistent_store/src/store.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index ba7ab4b..bc4258a 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -1468,4 +1468,22 @@ mod tests { driver = driver.power_off().power_on().unwrap(); driver.check().unwrap(); } + + #[test] + fn entries_ok() { + let mut driver = MINIMAL.new_driver().power_on().unwrap(); + + // The store is initially empty. + assert!(driver.store().entries.as_ref().unwrap().is_empty()); + + // Inserted elements are added. + const LEN: usize = 6; + driver.insert(0, &[0x38; (LEN - 1) * 4]).unwrap(); + driver.insert(1, &[0x5c; 4]).unwrap(); + assert_eq!(driver.store().entries, Some(vec![0, LEN as u16])); + + // Deleted elements are removed. + driver.remove(0).unwrap(); + assert_eq!(driver.store().entries, Some(vec![LEN as u16])); + } } From d6adab4381f43cfe3188ec68761fe23006c95838 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 18 Dec 2020 11:52:29 +0100 Subject: [PATCH 04/86] updates status codes for RD02 --- src/ctap/hid/mod.rs | 2 +- src/ctap/mod.rs | 22 ++++++++++------------ src/ctap/pin_protocol_v1.rs | 15 ++++++--------- src/ctap/status_code.rs | 23 +++++++++++------------ src/ctap/storage.rs | 4 ++-- 5 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index ef96eef..01c0b11 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -219,7 +219,7 @@ impl CtapHid { cid, cmd: CtapHid::COMMAND_CBOR, payload: vec![ - Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG as u8, + Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8, ], }) .unwrap() diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index dfa2d9b..7a23a1d 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -371,10 +371,8 @@ where let mut response_vec = vec![0x00]; if let Some(value) = response_data.into() { if !cbor::write(value, &mut response_vec) { - response_vec = vec![ - Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR - as u8, - ]; + response_vec = + vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]; } } response_vec @@ -496,7 +494,7 @@ where } None => { if self.persistent_store.pin_hash()?.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED); + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); } if options.uv { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); @@ -542,13 +540,13 @@ where 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); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); let cose_key = match pk.to_cose_key() { Some(cose_key) => cose_key, - None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR), + None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), }; auth_data.extend(cose_key); if has_extension_output { @@ -558,7 +556,7 @@ where "credProtect" => cred_protect_policy, }; if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } @@ -639,7 +637,7 @@ where "hmac-secret" => encrypted_output, }; if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } @@ -722,7 +720,7 @@ where let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); if hmac_secret_input.is_some() && !options.up { // The extension is actually supported, but we need user presence. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } // The user verification bit depends on the existance of PIN auth, since we do @@ -1592,7 +1590,7 @@ mod test { assert_eq!( get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) ); } @@ -1643,7 +1641,7 @@ mod test { assert_eq!( get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) ); } diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 410dac7..96e92b3 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -59,7 +59,7 @@ fn encrypt_hmac_secret_output( cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { if salt_enc.len() != 32 && salt_enc.len() != 64 { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); @@ -232,7 +232,7 @@ impl PinProtocolV1 { } } // This status code is not explicitly mentioned in the specification. - None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED), + None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED), } persistent_store.reset_pin_retries()?; self.consecutive_pin_mismatches = 0; @@ -400,7 +400,7 @@ impl PinProtocolV1 { pin_auth: Option>, ) -> Result<(), Ctap2StatusCode> { if min_pin_length_rp_ids.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if persistent_store.pin_hash()?.is_some() { match pin_auth { @@ -419,7 +419,7 @@ impl PinProtocolV1 { // TODO(kaczmarczyck) commented code is useful for the extension // https://github.com/google/OpenSK/issues/129 // if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) { - // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); // } if !verify_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); @@ -593,7 +593,7 @@ impl PinProtocolV1 { // HMAC-secret does the same 16 byte truncated check. if !verify_pin_auth(&shared_secret, &salt_enc, &salt_auth) { // Hard to tell what the correct error code here is. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } @@ -1174,10 +1174,7 @@ mod test { let salt_enc = [0x5E; 48]; let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); + assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); let salt_enc = [0x5E; 64]; let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 097d7ec..40f258e 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -31,11 +31,10 @@ pub enum Ctap2StatusCode { CTAP2_ERR_INVALID_CBOR = 0x12, CTAP2_ERR_MISSING_PARAMETER = 0x14, CTAP2_ERR_LIMIT_EXCEEDED = 0x15, - CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16, #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_FP_DATABASE_FULL = 0x17, #[cfg(feature = "with_ctap2_1")] - CTAP2_ERR_PC_STORAGE_FULL = 0x18, + CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18, CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, CTAP2_ERR_PROCESSING = 0x21, CTAP2_ERR_INVALID_CREDENTIAL = 0x22, @@ -57,7 +56,7 @@ pub enum Ctap2StatusCode { CTAP2_ERR_PIN_AUTH_INVALID = 0x33, CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34, CTAP2_ERR_PIN_NOT_SET = 0x35, - CTAP2_ERR_PIN_REQUIRED = 0x36, + CTAP2_ERR_PUAT_REQUIRED = 0x36, CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37, CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38, CTAP2_ERR_REQUEST_TOO_LARGE = 0x39, @@ -68,14 +67,15 @@ pub enum Ctap2StatusCode { CTAP2_ERR_INTEGRITY_FAILURE = 0x3D, #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_UV_INVALID = 0x3F, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40, CTAP1_ERR_OTHER = 0x7F, - CTAP2_ERR_SPEC_LAST = 0xDF, - CTAP2_ERR_EXTENSION_FIRST = 0xE0, - CTAP2_ERR_EXTENSION_LAST = 0xEF, - // CTAP2_ERR_VENDOR_FIRST = 0xF0, - CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0, - CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1, - + _CTAP2_ERR_SPEC_LAST = 0xDF, + _CTAP2_ERR_EXTENSION_FIRST = 0xE0, + _CTAP2_ERR_EXTENSION_LAST = 0xEF, + _CTAP2_ERR_VENDOR_FIRST = 0xF0, /// An internal invariant is broken. /// /// This type of error is unexpected and the current state is undefined. @@ -85,6 +85,5 @@ pub enum Ctap2StatusCode { /// /// It may be possible that some of those errors are actually internal errors. CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3, - - CTAP2_ERR_VENDOR_LAST = 0xFF, + _CTAP2_ERR_VENDOR_LAST = 0xFF, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a701325..73bbc16 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -577,7 +577,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result if cbor::write(credential.into(), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } @@ -600,7 +600,7 @@ fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap if cbor::write(cbor_array_vec!(rp_ids), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } From f67fdbc451963edf8b64e5ceeb14e0138078b334 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 22 Dec 2020 15:33:14 +0100 Subject: [PATCH 05/86] Add erase_storage application example --- deploy.py | 11 +++++++- examples/erase_storage.rs | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 examples/erase_storage.rs diff --git a/deploy.py b/deploy.py index b3daa6f..d5dcf2d 100755 --- a/deploy.py +++ b/deploy.py @@ -947,7 +947,16 @@ if __name__ == "__main__": dest="application", action="store_const", const="store_latency", - help=("Compiles and installs the store_latency example.")) + help=("Compiles and installs the store_latency example which print " + "latency statistics of the persistent store library.")) + apps_group.add_argument( + "--erase_storage", + dest="application", + action="store_const", + const="erase_storage", + help=("Compiles and installs the erase_storage example which erases " + "the storage. During operation the dongle red light is on. Once " + "the operation is completed the dongle green light is on.")) apps_group.add_argument( "--panic_test", dest="application", diff --git a/examples/erase_storage.rs b/examples/erase_storage.rs new file mode 100644 index 0000000..6076348 --- /dev/null +++ b/examples/erase_storage.rs @@ -0,0 +1,53 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] + +extern crate lang_items; + +use core::fmt::Write; +use ctap2::embedded_flash::new_storage; +use libtock_drivers::console::Console; +use libtock_drivers::led; +use libtock_drivers::result::FlexUnwrap; +use persistent_store::{Storage, StorageIndex}; + +fn is_page_erased(storage: &dyn Storage, page: usize) -> bool { + let index = StorageIndex { page, byte: 0 }; + let length = storage.page_size(); + storage + .read_slice(index, length) + .unwrap() + .iter() + .all(|&x| x == 0xff) +} + +fn main() { + led::get(1).flex_unwrap().on().flex_unwrap(); // red on dongle + const NUM_PAGES: usize = 20; // should be at least ctap::storage::NUM_PAGES + let mut storage = new_storage(NUM_PAGES); + writeln!(Console::new(), "Erase {} pages of storage:", NUM_PAGES).unwrap(); + for page in 0..NUM_PAGES { + write!(Console::new(), "- Page {} ", page).unwrap(); + if is_page_erased(&storage, page) { + writeln!(Console::new(), "skipped (was already erased).").unwrap(); + } else { + storage.erase_page(page).unwrap(); + writeln!(Console::new(), "erased.").unwrap(); + } + } + writeln!(Console::new(), "Done.").unwrap(); + led::get(1).flex_unwrap().off().flex_unwrap(); + led::get(0).flex_unwrap().on().flex_unwrap(); // green on dongle +} From de360a6cb6714cf6989bbebbf1cb1ce418686a74 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 6 Jan 2021 19:24:56 +0100 Subject: [PATCH 06/86] removes all occurences of CTAP 2.1 flags from workflows --- .github/workflows/cargo_check.yml | 16 ++-------------- .github/workflows/opensk_test.yml | 24 ------------------------ run_desktop_tests.sh | 13 ------------- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/.github/workflows/cargo_check.yml b/.github/workflows/cargo_check.yml index fd39614..7151806 100644 --- a/.github/workflows/cargo_check.yml +++ b/.github/workflows/cargo_check.yml @@ -42,12 +42,6 @@ jobs: command: check args: --target thumbv7em-none-eabi --release --features with_ctap1 - - name: Check OpenSK with_ctap2_1 - uses: actions-rs/cargo@v1 - with: - command: check - args: --target thumbv7em-none-eabi --release --features with_ctap2_1 - - name: Check OpenSK debug_ctap uses: actions-rs/cargo@v1 with: @@ -78,17 +72,11 @@ jobs: command: check args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1 - - name: Check OpenSK debug_ctap,with_ctap2_1 + - name: Check OpenSK debug_ctap,with_ctap1,panic_console,debug_allocations,verbose uses: actions-rs/cargo@v1 with: command: check - args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap2_1 - - - name: Check OpenSK debug_ctap,with_ctap1,with_ctap2_1,panic_console,debug_allocations,verbose - uses: actions-rs/cargo@v1 - with: - command: check - args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,with_ctap2_1,panic_console,debug_allocations,verbose + args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,,panic_console,debug_allocations,verbose - name: Check examples uses: actions-rs/cargo@v1 diff --git a/.github/workflows/opensk_test.yml b/.github/workflows/opensk_test.yml index 588dab6..406a7f3 100644 --- a/.github/workflows/opensk_test.yml +++ b/.github/workflows/opensk_test.yml @@ -51,27 +51,3 @@ jobs: command: test args: --features std,with_ctap1 - - name: Unit testing of CTAP2 (release mode + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --features std,with_ctap2_1 - - - name: Unit testing of CTAP2 (debug mode + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --features std,with_ctap2_1 - - - name: Unit testing of CTAP2 (release mode + CTAP1 + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --features std,with_ctap1,with_ctap2_1 - - - name: Unit testing of CTAP2 (debug mode + CTAP1 + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --features std,with_ctap1,with_ctap2_1 - diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 2e80b3d..24771f7 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -44,7 +44,6 @@ cargo test --manifest-path tools/heapviz/Cargo.toml echo "Checking that CTAP2 builds properly..." cargo check --release --target=thumbv7em-none-eabi cargo check --release --target=thumbv7em-none-eabi --features with_ctap1 -cargo check --release --target=thumbv7em-none-eabi --features with_ctap2_1 cargo check --release --target=thumbv7em-none-eabi --features debug_ctap cargo check --release --target=thumbv7em-none-eabi --features panic_console cargo check --release --target=thumbv7em-none-eabi --features debug_allocations @@ -116,16 +115,4 @@ then echo "Running unit tests on the desktop (debug mode + CTAP1)..." cargo test --features std,with_ctap1 - - echo "Running unit tests on the desktop (release mode + CTAP2.1)..." - cargo test --release --features std,with_ctap2_1 - - echo "Running unit tests on the desktop (debug mode + CTAP2.1)..." - cargo test --features std,with_ctap2_1 - - echo "Running unit tests on the desktop (release mode + CTAP1 + CTAP2.1)..." - cargo test --release --features std,with_ctap1,with_ctap2_1 - - echo "Running unit tests on the desktop (debug mode + CTAP1 + CTAP2.1)..." - cargo test --features std,with_ctap1,with_ctap2_1 fi From c873d3b6147e81f1be7518718308d15deec04b85 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 6 Jan 2021 19:24:56 +0100 Subject: [PATCH 07/86] removes all occurences of CTAP 2.1 flags --- Cargo.toml | 1 - README.md | 15 ++++--- deploy.py | 10 +---- src/ctap/command.rs | 43 ------------------- src/ctap/data_formats.rs | 11 ----- src/ctap/mod.rs | 82 +++++++++++-------------------------- src/ctap/pin_protocol_v1.rs | 79 ++++------------------------------- src/ctap/response.rs | 61 +-------------------------- src/ctap/status_code.rs | 6 --- src/ctap/storage.rs | 22 ++-------- src/ctap/storage/key.rs | 2 - 11 files changed, 47 insertions(+), 285 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15984a0..bca9210 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ panic_console = ["lang_items/panic_console"] std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] with_ctap1 = ["crypto/with_ctap1"] -with_ctap2_1 = [] with_nfc = ["libtock_drivers/with_nfc"] [dev-dependencies] diff --git a/README.md b/README.md index 68e9f71..decee76 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,14 @@ few limitations: ### FIDO2 -Although we tested and implemented our firmware based on the published +The stable branch implements the published [CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html), -our implementation was not reviewed nor officially tested and doesn't claim to -be FIDO Certified. -We started adding features of the upcoming next version of the -[CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). -The development is currently between 2.0 and 2.1, with updates hidden behind -a feature flag. -Please add the flag `--ctap2.1` to the deploy command to include them. +but our implementation was not reviewed nor officially tested and doesn't claim +to be FIDO Certified. It already contains some preview features of 2.1, that you +can try by adding the flag `--ctap2.1` to the deploy command. +The develop branch offers only the +[CTAP2.1 specifications](https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html). +The new features of 2.1 are currently work in progress. ### Cryptography diff --git a/deploy.py b/deploy.py index d5dcf2d..e8d5ffd 100755 --- a/deploy.py +++ b/deploy.py @@ -881,14 +881,6 @@ if __name__ == "__main__": help=("Compiles the OpenSK application without backward compatible " "support for U2F/CTAP1 protocol."), ) - main_parser.add_argument( - "--ctap2.1", - action="append_const", - const="with_ctap2_1", - dest="features", - help=("Compiles the OpenSK application with backward compatible " - "support for CTAP2.1 protocol."), - ) main_parser.add_argument( "--nfc", action="append_const", @@ -947,7 +939,7 @@ if __name__ == "__main__": dest="application", action="store_const", const="store_latency", - help=("Compiles and installs the store_latency example which print " + help=("Compiles and installs the store_latency example which prints " "latency statistics of the persistent store library.")) apps_group.add_argument( "--erase_storage", diff --git a/src/ctap/command.rs b/src/ctap/command.rs index ecfae9e..0a86093 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -41,7 +41,6 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, - #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands @@ -111,7 +110,6 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } - #[cfg(feature = "with_ctap2_1")] Command::AUTHENTICATOR_SELECTION => { // Parameters are ignored. Ok(Command::AuthenticatorSelection) @@ -292,13 +290,9 @@ pub struct AuthenticatorClientPinParameters { pub pin_auth: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, - #[cfg(feature = "with_ctap2_1")] pub min_pin_length: Option, - #[cfg(feature = "with_ctap2_1")] pub min_pin_length_rp_ids: Option>, - #[cfg(feature = "with_ctap2_1")] pub permissions: Option, - #[cfg(feature = "with_ctap2_1")] pub permissions_rp_id: Option, } @@ -306,18 +300,6 @@ impl TryFrom for AuthenticatorClientPinParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - #[cfg(not(feature = "with_ctap2_1"))] - 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)?; - } - #[cfg(feature = "with_ctap2_1")] destructure_cbor_map! { let { 1 => pin_protocol, @@ -339,14 +321,12 @@ impl TryFrom for AuthenticatorClientPinParameters { 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()?; - #[cfg(feature = "with_ctap2_1")] let min_pin_length = min_pin_length .map(extract_unsigned) .transpose()? .map(u8::try_from) .transpose() .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - #[cfg(feature = "with_ctap2_1")] let min_pin_length_rp_ids = match min_pin_length_rp_ids { Some(entry) => Some( extract_array(entry)? @@ -356,14 +336,12 @@ impl TryFrom for AuthenticatorClientPinParameters { ), None => None, }; - #[cfg(feature = "with_ctap2_1")] // We expect a bit field of 8 bits, and drop everything else. // This means we ignore extensions in future versions. let permissions = permissions .map(extract_unsigned) .transpose()? .map(|p| p as u8); - #[cfg(feature = "with_ctap2_1")] let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; Ok(AuthenticatorClientPinParameters { @@ -373,13 +351,9 @@ impl TryFrom for AuthenticatorClientPinParameters { pin_auth, new_pin_enc, pin_hash_enc, - #[cfg(feature = "with_ctap2_1")] min_pin_length, - #[cfg(feature = "with_ctap2_1")] min_pin_length_rp_ids, - #[cfg(feature = "with_ctap2_1")] permissions, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id, }) } @@ -560,18 +534,6 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { - // TODO(kaczmarczyck) inline the #cfg when #128 is resolved: - // https://github.com/google/OpenSK/issues/128 - #[cfg(not(feature = "with_ctap2_1"))] - let cbor_value = cbor_map! { - 1 => 1, - 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor_map!{}, - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], - }; - #[cfg(feature = "with_ctap2_1")] let cbor_value = cbor_map! { 1 => 1, 2 => ClientPinSubCommand::GetPinRetries, @@ -594,13 +556,9 @@ mod test { pin_auth: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), - #[cfg(feature = "with_ctap2_1")] min_pin_length: Some(4), - #[cfg(feature = "with_ctap2_1")] min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), - #[cfg(feature = "with_ctap2_1")] permissions: Some(0x03), - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: Some("example.com".to_string()), }; @@ -632,7 +590,6 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_deserialize_selection() { let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index a2b490d..8081567 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -704,13 +704,9 @@ pub enum ClientPinSubCommand { SetPin = 0x03, ChangePin = 0x04, GetPinToken = 0x05, - #[cfg(feature = "with_ctap2_1")] GetPinUvAuthTokenUsingUvWithPermissions = 0x06, - #[cfg(feature = "with_ctap2_1")] GetUvRetries = 0x07, - #[cfg(feature = "with_ctap2_1")] SetMinPinLength = 0x08, - #[cfg(feature = "with_ctap2_1")] GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } @@ -731,18 +727,11 @@ impl TryFrom for ClientPinSubCommand { 0x03 => Ok(ClientPinSubCommand::SetPin), 0x04 => Ok(ClientPinSubCommand::ChangePin), 0x05 => Ok(ClientPinSubCommand::GetPinToken), - #[cfg(feature = "with_ctap2_1")] 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), - #[cfg(feature = "with_ctap2_1")] 0x07 => Ok(ClientPinSubCommand::GetUvRetries), - #[cfg(feature = "with_ctap2_1")] 0x08 => Ok(ClientPinSubCommand::SetMinPinLength), - #[cfg(feature = "with_ctap2_1")] 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), - #[cfg(feature = "with_ctap2_1")] _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), - #[cfg(not(feature = "with_ctap2_1"))] - _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), } } } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 7a23a1d..4168f8f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -25,23 +25,19 @@ pub mod status_code; mod storage; mod timed_permission; -#[cfg(feature = "with_ctap2_1")] -use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, + MAX_CREDENTIAL_COUNT_IN_LIST, }; -#[cfg(feature = "with_ctap2_1")] -use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, - PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, + AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, + SignatureAlgorithm, }; use self::hid::ChannelID; -#[cfg(feature = "with_ctap2_1")] -use self::pin_protocol_v1::PinPermission; -use self::pin_protocol_v1::PinProtocolV1; +use self::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, @@ -108,7 +104,6 @@ pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] pub const U2F_VERSION_STRING: &str = "U2F_V2"; // TODO(#106) change to final string when ready -#[cfg(feature = "with_ctap2_1")] pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; // We currently only support one algorithm for signatures: ES256. @@ -339,7 +334,6 @@ where // GetInfo does not reset stateful commands. (Command::AuthenticatorGetInfo, _) => (), // AuthenticatorSelection does not reset stateful commands. - #[cfg(feature = "with_ctap2_1")] (Command::AuthenticatorSelection, _) => (), (_, _) => { self.stateful_command_type = None; @@ -356,7 +350,6 @@ where Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), - #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands @@ -484,12 +477,9 @@ where { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - #[cfg(feature = "with_ctap2_1")] - { - self.pin_protocol_v1 - .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; - } + self.pin_protocol_v1 + .has_permission(PinPermission::MakeCredential)?; + self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -738,12 +728,9 @@ where { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - #[cfg(feature = "with_ctap2_1")] - { - self.pin_protocol_v1 - .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; - } + self.pin_protocol_v1 + .has_permission(PinPermission::GetAssertion)?; + self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; UV_FLAG } None => { @@ -851,7 +838,6 @@ where #[cfg(feature = "with_ctap1")] String::from(U2F_VERSION_STRING), String::from(FIDO2_VERSION_STRING), - #[cfg(feature = "with_ctap2_1")] String::from(FIDO2_1_VERSION_STRING), ], extensions: Some(vec![String::from("hmac-secret")]), @@ -861,19 +847,13 @@ where pin_protocols: Some(vec![ CtapState::::PIN_PROTOCOL_VERSION, ]), - #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), // #TODO(106) update with version 2.1 of HMAC-secret - #[cfg(feature = "with_ctap2_1")] max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), - #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), - #[cfg(feature = "with_ctap2_1")] algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, - #[cfg(feature = "with_ctap2_1")] min_pin_length: self.persistent_store.min_pin_length()?, - #[cfg(feature = "with_ctap2_1")] firmware_version: None, }, )) @@ -916,7 +896,6 @@ where Ok(ResponseData::AuthenticatorReset) } - #[cfg(feature = "with_ctap2_1")] fn process_selection(&self, cid: ChannelID) -> Result { (self.check_user_presence)(cid)?; Ok(ResponseData::AuthenticatorSelection) @@ -1036,42 +1015,31 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - #[cfg(feature = "with_ctap2_1")] let mut expected_response = vec![0x00, 0xAA, 0x01]; - #[cfg(not(feature = "with_ctap2_1"))] - let mut expected_response = vec![0x00, 0xA6, 0x01]; // The difference here is a longer array of supported versions. let mut version_count = 0; - // CTAP 2 is always supported - version_count += 1; + // CTAP 2.0 and 2.1 are always supported + version_count += 2; #[cfg(feature = "with_ctap1")] { version_count += 1; } - #[cfg(feature = "with_ctap2_1")] - { - version_count += 1; - } expected_response.push(0x80 + version_count); #[cfg(feature = "with_ctap1")] expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); - expected_response.extend(&[0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30]); - #[cfg(feature = "with_ctap2_1")] - expected_response.extend(&[ - 0x6C, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, - ]); - expected_response.extend(&[ - 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x03, 0x50, - ]); - 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, - ]); - #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ + 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, + 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, + ] + .iter(), + ); + 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, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 96e92b3..b8aeb21 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -17,7 +17,6 @@ use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretIn use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -#[cfg(feature = "with_ctap2_1")] use alloc::string::String; use alloc::vec; use alloc::vec::Vec; @@ -28,7 +27,7 @@ use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; use crypto::Hash256; -#[cfg(all(test, feature = "with_ctap2_1"))] +#[cfg(test)] use enum_iterator::IntoEnumIterator; use subtle::ConstantTimeEq; @@ -141,10 +140,7 @@ fn check_and_store_new_pin( let pin = decrypt_pin(aes_dec_key, new_pin_enc) .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - #[cfg(feature = "with_ctap2_1")] let min_pin_length = persistent_store.min_pin_length()? as usize; - #[cfg(not(feature = "with_ctap2_1"))] - let min_pin_length = 4; if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { // TODO(kaczmarczyck) check 4 code point minimum instead return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); @@ -155,7 +151,6 @@ fn check_and_store_new_pin( Ok(()) } -#[cfg(feature = "with_ctap2_1")] #[cfg_attr(test, derive(IntoEnumIterator))] // TODO remove when all variants are used #[allow(dead_code)] @@ -173,9 +168,7 @@ pub struct PinProtocolV1 { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], consecutive_pin_mismatches: u8, - #[cfg(feature = "with_ctap2_1")] permissions: u8, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: Option, } @@ -187,9 +180,7 @@ impl PinProtocolV1 { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, - #[cfg(feature = "with_ctap2_1")] permissions: 0, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: None, } } @@ -345,11 +336,8 @@ impl PinProtocolV1 { cbc_encrypt(&token_encryption_key, iv, &mut blocks); let pin_token: Vec = blocks.iter().flatten().cloned().collect(); - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0x03; - self.permissions_rp_id = None; - } + self.permissions = 0x03; + self.permissions_rp_id = None; Ok(AuthenticatorClientPinResponse { key_agreement: None, @@ -358,7 +346,6 @@ impl PinProtocolV1 { }) } - #[cfg(feature = "with_ctap2_1")] fn process_get_pin_uv_auth_token_using_uv_with_permissions( &self, // If you want to support local user verification, implement this function. @@ -368,30 +355,14 @@ impl PinProtocolV1 { _permissions_rp_id: Option, ) -> Result { // User verifications is only supported through PIN currently. - #[cfg(not(feature = "with_ctap2_1"))] - { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - #[cfg(feature = "with_ctap2_1")] - { - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) - } + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } - #[cfg(feature = "with_ctap2_1")] fn process_get_uv_retries(&self) -> Result { // User verifications is only supported through PIN currently. - #[cfg(not(feature = "with_ctap2_1"))] - { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - #[cfg(feature = "with_ctap2_1")] - { - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) - } + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } - #[cfg(feature = "with_ctap2_1")] fn process_set_min_pin_length( &mut self, persistent_store: &mut PersistentStore, @@ -440,7 +411,6 @@ impl PinProtocolV1 { Ok(()) } - #[cfg(feature = "with_ctap2_1")] fn process_get_pin_uv_auth_token_using_pin_with_permissions( &mut self, rng: &mut impl Rng256, @@ -480,20 +450,13 @@ impl PinProtocolV1 { pin_auth, new_pin_enc, pin_hash_enc, - #[cfg(feature = "with_ctap2_1")] min_pin_length, - #[cfg(feature = "with_ctap2_1")] min_pin_length_rp_ids, - #[cfg(feature = "with_ctap2_1")] permissions, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id, } = client_pin_params; if pin_protocol != 1 { - #[cfg(not(feature = "with_ctap2_1"))] - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - #[cfg(feature = "with_ctap2_1")] return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } @@ -528,7 +491,6 @@ impl PinProtocolV1 { key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?), - #[cfg(feature = "with_ctap2_1")] ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( self.process_get_pin_uv_auth_token_using_uv_with_permissions( key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, @@ -536,9 +498,7 @@ impl PinProtocolV1 { permissions_rp_id, )?, ), - #[cfg(feature = "with_ctap2_1")] ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), - #[cfg(feature = "with_ctap2_1")] ClientPinSubCommand::SetMinPinLength => { self.process_set_min_pin_length( persistent_store, @@ -548,7 +508,6 @@ impl PinProtocolV1 { )?; None } - #[cfg(feature = "with_ctap2_1")] ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( rng, @@ -571,11 +530,8 @@ impl PinProtocolV1 { self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); self.pin_uv_auth_token = rng.gen_uniform_u8x32(); self.consecutive_pin_mismatches = 0; - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0; - self.permissions_rp_id = None; - } + self.permissions = 0; + self.permissions_rp_id = None; } pub fn process_hmac_secret( @@ -598,7 +554,6 @@ impl PinProtocolV1 { encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } - #[cfg(feature = "with_ctap2_1")] pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { // Relies on the fact that all permissions are represented by powers of two. if permission as u8 & self.permissions != 0 { @@ -608,7 +563,6 @@ impl PinProtocolV1 { } } - #[cfg(feature = "with_ctap2_1")] pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { if let Some(permissions_rp_id) = &self.permissions_rp_id { if rp_id != permissions_rp_id { @@ -629,9 +583,7 @@ impl PinProtocolV1 { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, - #[cfg(feature = "with_ctap2_1")] permissions: 0xFF, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: None, } } @@ -919,7 +871,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { let mut rng = ThreadRng256 {}; @@ -963,7 +914,7 @@ mod test { &mut rng, &mut persistent_store, key_agreement.clone(), - pin_hash_enc.clone(), + pin_hash_enc, 0x03, None, ), @@ -984,7 +935,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_process_set_min_pin_length() { let mut rng = ThreadRng256 {}; @@ -1031,13 +981,9 @@ mod test { pin_auth: None, new_pin_enc: None, pin_hash_enc: None, - #[cfg(feature = "with_ctap2_1")] min_pin_length: None, - #[cfg(feature = "with_ctap2_1")] min_pin_length_rp_ids: None, - #[cfg(feature = "with_ctap2_1")] permissions: None, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: None, }; assert!(pin_protocol_v1 @@ -1051,18 +997,11 @@ mod test { pin_auth: None, new_pin_enc: None, pin_hash_enc: None, - #[cfg(feature = "with_ctap2_1")] min_pin_length: None, - #[cfg(feature = "with_ctap2_1")] min_pin_length_rp_ids: None, - #[cfg(feature = "with_ctap2_1")] permissions: None, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: None, }; - #[cfg(not(feature = "with_ctap2_1"))] - let error_code = Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID; - #[cfg(feature = "with_ctap2_1")] let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; assert_eq!( pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params), @@ -1231,7 +1170,6 @@ mod test { assert_eq!(&output_dec[..32], &expected_output1); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_has_permission() { let mut rng = ThreadRng256 {}; @@ -1249,7 +1187,6 @@ mod test { } } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_has_permission_for_rp_id() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 6422959..390b0cb 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "with_ctap2_1")] -use super::data_formats::{AuthenticatorTransport, PublicKeyCredentialParameter}; use super::data_formats::{ - CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialUserEntity, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity, }; use alloc::collections::BTreeMap; use alloc::string::String; @@ -32,7 +30,6 @@ pub enum ResponseData { AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorClientPin(Option), AuthenticatorReset, - #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -47,7 +44,6 @@ impl From for Option { ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), ResponseData::AuthenticatorClientPin(None) => None, ResponseData::AuthenticatorReset => None, - #[cfg(feature = "with_ctap2_1")] ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -118,23 +114,16 @@ pub struct AuthenticatorGetInfoResponse { pub options: Option>, pub max_msg_size: Option, pub pin_protocols: Option>, - #[cfg(feature = "with_ctap2_1")] pub max_credential_count_in_list: Option, - #[cfg(feature = "with_ctap2_1")] pub max_credential_id_length: Option, - #[cfg(feature = "with_ctap2_1")] pub transports: Option>, - #[cfg(feature = "with_ctap2_1")] pub algorithms: Option>, pub default_cred_protect: Option, - #[cfg(feature = "with_ctap2_1")] pub min_pin_length: u8, - #[cfg(feature = "with_ctap2_1")] pub firmware_version: Option, } impl From for cbor::Value { - #[cfg(feature = "with_ctap2_1")] fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { let AuthenticatorGetInfoResponse { versions, @@ -176,37 +165,6 @@ impl From for cbor::Value { 0x0E => firmware_version, } } - - #[cfg(not(feature = "with_ctap2_1"))] - fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { - let AuthenticatorGetInfoResponse { - versions, - extensions, - aaguid, - options, - max_msg_size, - pin_protocols, - default_cred_protect, - } = get_info_response; - - let options_cbor: Option = options.map(|options| { - let option_map: BTreeMap<_, _> = options - .into_iter() - .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) - .collect(); - cbor_map_btree!(option_map) - }); - - cbor_map_options! { - 0x01 => cbor_array_vec!(versions), - 0x02 => extensions.map(|vec| cbor_array_vec!(vec)), - 0x03 => &aaguid, - 0x04 => options_cbor, - 0x05 => max_msg_size, - 0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)), - 0x0C => default_cred_protect.map(|p| p as u64), - } - } } #[cfg_attr(test, derive(PartialEq))] @@ -257,7 +215,6 @@ impl From for cbor::Value { #[cfg(test)] mod test { use super::super::data_formats::PackedAttestationStatement; - #[cfg(feature = "with_ctap2_1")] use super::super::ES256_CRED_PARAM; use super::*; use cbor::{cbor_bytes, cbor_map}; @@ -321,28 +278,16 @@ mod test { options: None, max_msg_size: None, pin_protocols: None, - #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: None, - #[cfg(feature = "with_ctap2_1")] max_credential_id_length: None, - #[cfg(feature = "with_ctap2_1")] transports: None, - #[cfg(feature = "with_ctap2_1")] algorithms: None, default_cred_protect: None, - #[cfg(feature = "with_ctap2_1")] min_pin_length: 4, - #[cfg(feature = "with_ctap2_1")] firmware_version: None, }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); - #[cfg(not(feature = "with_ctap2_1"))] - let expected_cbor = cbor_map_options! { - 0x01 => cbor_array_vec![versions], - 0x03 => vec![0x00; 16], - }; - #[cfg(feature = "with_ctap2_1")] let expected_cbor = cbor_map_options! { 0x01 => cbor_array_vec![versions], 0x03 => vec![0x00; 16], @@ -352,7 +297,6 @@ mod test { } #[test] - #[cfg(feature = "with_ctap2_1")] fn test_get_info_optionals_into_cbor() { let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); @@ -418,7 +362,6 @@ mod test { assert_eq!(response_cbor, None); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_selection_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 40f258e..5a9ec71 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -31,9 +31,7 @@ pub enum Ctap2StatusCode { CTAP2_ERR_INVALID_CBOR = 0x12, CTAP2_ERR_MISSING_PARAMETER = 0x14, CTAP2_ERR_LIMIT_EXCEEDED = 0x15, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_FP_DATABASE_FULL = 0x17, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18, CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, CTAP2_ERR_PROCESSING = 0x21, @@ -63,13 +61,9 @@ pub enum Ctap2StatusCode { CTAP2_ERR_ACTION_TIMEOUT = 0x3A, CTAP2_ERR_UP_REQUIRED = 0x3B, CTAP2_ERR_UV_BLOCKED = 0x3C, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_INTEGRITY_FAILURE = 0x3D, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_UV_INVALID = 0x3F, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40, CTAP1_ERR_OTHER = 0x7F, _CTAP2_ERR_SPEC_LAST = 0xDF, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 73bbc16..28c1599 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,20 +14,18 @@ mod key; -#[cfg(feature = "with_ctap2_1")] -use crate::ctap::data_formats::{extract_array, extract_text_string}; -use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; +use crate::ctap::data_formats::{ + extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, +}; use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; use crate::embedded_flash::{new_storage, Storage}; -#[cfg(feature = "with_ctap2_1")] use alloc::string::String; use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; -#[cfg(feature = "with_ctap2_1")] use cbor::cbor_array_vec; use core::convert::TryInto; use crypto::rng256::Rng256; @@ -54,14 +52,11 @@ const NUM_PAGES: usize = 20; const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; -#[cfg(feature = "with_ctap2_1")] const DEFAULT_MIN_PIN_LENGTH: u8 = 4; // TODO(kaczmarczyck) use this for the minPinLength extension // https://github.com/google/OpenSK/issues/129 -#[cfg(feature = "with_ctap2_1")] const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. -#[cfg(feature = "with_ctap2_1")] const _MAX_RP_IDS_LENGTH: usize = 8; /// Wrapper for master keys. @@ -348,7 +343,6 @@ impl PersistentStore { } /// Returns the minimum PIN length. - #[cfg(feature = "with_ctap2_1")] pub fn min_pin_length(&self) -> Result { match self.store.find(key::MIN_PIN_LENGTH)? { None => Ok(DEFAULT_MIN_PIN_LENGTH), @@ -358,14 +352,12 @@ impl PersistentStore { } /// Sets the minimum PIN length. - #[cfg(feature = "with_ctap2_1")] pub fn set_min_pin_length(&mut self, min_pin_length: u8) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) } /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. - #[cfg(feature = "with_ctap2_1")] pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { let rp_ids = self .store @@ -374,11 +366,10 @@ impl PersistentStore { _deserialize_min_pin_length_rp_ids(&value) }); debug_assert!(rp_ids.is_some()); - Ok(rp_ids.unwrap_or(vec![])) + Ok(rp_ids.unwrap_or_default()) } /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. - #[cfg(feature = "with_ctap2_1")] pub fn _set_min_pin_length_rp_ids( &mut self, min_pin_length_rp_ids: Vec, @@ -582,7 +573,6 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result } /// Deserializes a list of RP IDs from storage representation. -#[cfg(feature = "with_ctap2_1")] fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; extract_array(cbor) @@ -594,7 +584,6 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { } /// Serializes a list of RP IDs to storage representation. -#[cfg(feature = "with_ctap2_1")] fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(cbor_array_vec!(rp_ids), &mut data) { @@ -988,7 +977,6 @@ mod test { assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_min_pin_length() { let mut rng = ThreadRng256 {}; @@ -1011,7 +999,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_min_pin_length_rp_ids() { let mut rng = ThreadRng256 {}; @@ -1080,7 +1067,6 @@ mod test { assert_eq!(credential, reconstructed); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_serialize_deserialize_min_pin_length_rp_ids() { let rp_ids = vec![String::from("example.com")]; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 5c5b20e..ec39efa 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -92,13 +92,11 @@ make_partition! { CRED_RANDOM_SECRET = 2041; /// List of RP IDs allowed to read the minimum PIN length. - #[cfg(feature = "with_ctap2_1")] _MIN_PIN_LENGTH_RP_IDS = 2042; /// The minimum PIN length. /// /// If the entry is absent, the minimum PIN length is `DEFAULT_MIN_PIN_LENGTH`. - #[cfg(feature = "with_ctap2_1")] MIN_PIN_LENGTH = 2043; /// The number of PIN retries. From da03f77a32e059491ee8e1e2b4a3a0b027e8fa45 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 8 Jan 2021 13:13:52 +0100 Subject: [PATCH 08/86] small readbility fix for variable assignment with cfg --- src/ctap/mod.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4168f8f..fe60e80 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1016,14 +1016,11 @@ mod test { let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let mut expected_response = vec![0x00, 0xAA, 0x01]; - // The difference here is a longer array of supported versions. - let mut version_count = 0; - // CTAP 2.0 and 2.1 are always supported - version_count += 2; + // The version array differs with CTAP1, always including 2.0 and 2.1. + #[cfg(not(feature = "with_ctap1"))] + let version_count = 2; #[cfg(feature = "with_ctap1")] - { - version_count += 1; - } + let version_count = 3; expected_response.push(0x80 + version_count); #[cfg(feature = "with_ctap1")] expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); From f4eb6c938e5105c9c0039f160146d21dfe18a101 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 7 Jan 2021 18:17:21 +0100 Subject: [PATCH 09/86] adds the config command --- README.md | 15 +- src/ctap/command.rs | 80 +++++--- src/ctap/config_command.rs | 269 +++++++++++++++++++++++++++ src/ctap/data_formats.rs | 160 +++++++++++++++- src/ctap/mod.rs | 361 +++++++++++++++++++++--------------- src/ctap/pin_protocol_v1.rs | 114 ++---------- src/ctap/response.rs | 9 + src/ctap/storage.rs | 114 ++++++++---- src/ctap/storage/key.rs | 9 +- 9 files changed, 796 insertions(+), 335 deletions(-) create mode 100644 src/ctap/config_command.rs diff --git a/README.md b/README.md index decee76..48f6c6e 100644 --- a/README.md +++ b/README.md @@ -93,32 +93,37 @@ a few things you can personalize: 1. If you have multiple buttons, choose the buttons responsible for user presence in `main.rs`. -2. Decide whether you want to use batch attestation. There is a boolean flag in +1. Decide whether you want to use batch attestation. There is a boolean flag in `ctap/mod.rs`. It is mandatory for U2F, and you can create your own self-signed certificate. The flag is used for FIDO2 and has some privacy implications. Please check [WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more information. -3. Decide whether you want to use signature counters. Currently, only global +1. Decide whether you want to use signature counters. Currently, only global signature counters are implemented, as they are the default option for U2F. The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy preserving solution is individual or no signature counters. Again, please check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for documentation. -4. Depending on your available flash storage, choose an appropriate maximum +1. Depending on your available flash storage, choose an appropriate maximum number of supported residential keys and number of pages in `ctap/storage.rs`. -5. Change the default level for the credProtect extension in `ctap/mod.rs`. +1. Change the default level for the credProtect extension in `ctap/mod.rs`. When changing the default, resident credentials become undiscoverable without user verification. This helps privacy, but can make usage less comfortable for credentials that need less protection. -6. Increase the default minimum length for PINs in `ctap/storage.rs`. +1. Increase the default minimum length for PINs in `ctap/storage.rs`. The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer PINs can help establish trust between users and relying parties. It makes user verification harder to break, but less convenient. NIST recommends at least 6-digit PINs in section 5.1.9.1: https://pages.nist.gov/800-63-3/sp800-63b.html You can add relying parties to the list of readers of the minimum PIN length. +1. In an enterprise setting, you can adapt `DEFAULT_MIN_PIN_LENGTH_RP_IDS` and + `MAX_RP_IDS_LENGTH` for tuning the `minPinLength` extension. The former + allows some relying parties to read the minimum PIN length by default. The + latter allows storing more relying parties that may check the minimum PIN + length. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 0a86093..6b6ab54 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -14,10 +14,10 @@ use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, - extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, - GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, + CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -42,6 +42,7 @@ pub enum Command { AuthenticatorReset, AuthenticatorGetNextAssertion, AuthenticatorSelection, + AuthenticatorConfig(AuthenticatorConfigParameters), // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), @@ -114,6 +115,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_CONFIG => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorConfig( + AuthenticatorConfigParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_VENDOR_CONFIGURE => { let decoded_cbor = cbor::read(&bytes[1..])?; Ok(Command::AuthenticatorVendorConfigure( @@ -290,8 +297,6 @@ pub struct AuthenticatorClientPinParameters { pub pin_auth: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, - pub min_pin_length: Option, - pub min_pin_length_rp_ids: Option>, pub permissions: Option, pub permissions_rp_id: Option, } @@ -308,8 +313,6 @@ impl TryFrom for AuthenticatorClientPinParameters { 4 => pin_auth, 5 => new_pin_enc, 6 => pin_hash_enc, - 7 => min_pin_length, - 8 => min_pin_length_rp_ids, 9 => permissions, 10 => permissions_rp_id, } = extract_map(cbor_value)?; @@ -321,21 +324,6 @@ impl TryFrom for AuthenticatorClientPinParameters { 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()?; - let min_pin_length = min_pin_length - .map(extract_unsigned) - .transpose()? - .map(u8::try_from) - .transpose() - .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - let min_pin_length_rp_ids = match min_pin_length_rp_ids { - Some(entry) => Some( - extract_array(entry)? - .into_iter() - .map(extract_text_string) - .collect::, Ctap2StatusCode>>()?, - ), - None => None, - }; // We expect a bit field of 8 bits, and drop everything else. // This means we ignore extensions in future versions. let permissions = permissions @@ -351,14 +339,52 @@ impl TryFrom for AuthenticatorClientPinParameters { pin_auth, new_pin_enc, pin_hash_enc, - min_pin_length, - min_pin_length_rp_ids, permissions, permissions_rp_id, }) } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorConfigParameters { + pub sub_command: ConfigSubCommand, + pub sub_command_params: Option, + pub pin_uv_auth_param: Option>, + pub pin_uv_auth_protocol: Option, +} + +impl TryFrom for AuthenticatorConfigParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_uv_auth_param, + 0x04 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } + + let sub_command = ConfigSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = match sub_command { + ConfigSubCommand::SetMinPinLength => Some(ConfigSubCommandParams::SetMinPinLength( + SetMinPinLengthParams::try_from(ok_or_missing(sub_command_params)?)?, + )), + _ => None, + }; + 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(AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorAttestationMaterial { pub certificate: Vec, @@ -541,8 +567,6 @@ mod test { 4 => vec! [0xBB], 5 => vec! [0xCC], 6 => vec! [0xDD], - 7 => 4, - 8 => cbor_array!["example.com"], 9 => 0x03, 10 => "example.com", }; @@ -556,8 +580,6 @@ mod test { pin_auth: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), - min_pin_length: Some(4), - min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), permissions: Some(0x03), permissions_rp_id: Some("example.com".to_string()), }; diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs new file mode 100644 index 0000000..873a9b1 --- /dev/null +++ b/src/ctap/config_command.rs @@ -0,0 +1,269 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::check_pin_uv_auth_protocol; +use super::command::AuthenticatorConfigParameters; +use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; +use super::pin_protocol_v1::PinProtocolV1; +use super::response::ResponseData; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use alloc::vec; + +fn process_set_min_pin_length( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + params: SetMinPinLengthParams, +) -> Result { + let SetMinPinLengthParams { + new_min_pin_length, + min_pin_length_rp_ids, + force_change_pin, + } = params; + let store_min_pin_length = persistent_store.min_pin_length()?; + let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length); + if new_min_pin_length < store_min_pin_length { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); + } + let mut force_change_pin = force_change_pin.unwrap_or(false); + if force_change_pin && persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } + if let Some(old_length) = persistent_store.pin_code_point_length()? { + force_change_pin |= new_min_pin_length > old_length; + } + pin_protocol_v1.force_pin_change |= force_change_pin; + // TODO(kaczmarczyck) actually force a PIN change + persistent_store.set_min_pin_length(new_min_pin_length)?; + if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { + persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?; + } + Ok(ResponseData::AuthenticatorConfig) +} + +pub fn process_config( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + params: AuthenticatorConfigParameters, +) -> Result { + let AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + } = params; + + if persistent_store.pin_hash()?.is_some() { + // TODO(kaczmarczyck) The error code is specified inconsistently with other commands. + check_pin_uv_auth_protocol(pin_uv_auth_protocol) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let mut config_data = vec![0xFF; 32]; + config_data.extend(&[0x0D, sub_command as u8]); + if let Some(sub_command_params) = sub_command_params.clone() { + if !cbor::write(sub_command_params.into(), &mut config_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + if !pin_protocol_v1.verify_pin_auth_token(&config_data, &auth_param) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + } + + match sub_command { + ConfigSubCommand::SetMinPinLength => { + if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { + process_set_min_pin_length(persistent_store, pin_protocol_v1, params) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + } + } + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } +} + +#[cfg(test)] +mod test { + use super::super::command::AuthenticatorConfigParameters; + use super::*; + use crypto::rng256::ThreadRng256; + + fn create_min_pin_config_params( + min_pin_length: u8, + min_pin_length_rp_ids: Option>, + ) -> AuthenticatorConfigParameters { + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(min_pin_length), + min_pin_length_rp_ids, + force_change_pin: None, + }; + AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::SetMinPinLength, + sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength( + set_min_pin_length_params, + )), + pin_uv_auth_param: None, + pin_uv_auth_protocol: Some(1), + } + } + + #[test] + fn test_process_set_min_pin_length() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + // First, increase minimum PIN length from 4 to 6 without PIN auth. + let min_pin_length = 6; + let config_params = create_min_pin_config_params(min_pin_length, None); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + + // Second, increase minimum PIN length from 6 to 8 with PIN auth. + // The stored PIN or its length don't matter since we control the token. + persistent_store.set_pin(&[0x88; 16], 8).unwrap(); + let min_pin_length = 8; + let mut config_params = create_min_pin_config_params(min_pin_length, None); + let pin_auth = vec![ + 0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29, + 0xB2, 0xDE, + ]; + config_params.pin_uv_auth_param = Some(pin_auth); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + + // Third, decreasing the minimum PIN length from 8 to 7 fails. + let mut config_params = create_min_pin_config_params(7, None); + let pin_auth = vec![ + 0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8, + 0xA7, 0x71, + ]; + config_params.pin_uv_auth_param = Some(pin_auth); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) + ); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + } + + #[test] + fn test_process_set_min_pin_length_rp_ids() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + // First, set RP IDs without PIN auth. + let min_pin_length = 6; + let min_pin_length_rp_ids = vec!["example.com".to_string()]; + let config_params = + create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone())); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids) + ); + + // Second, change the RP IDs with PIN auth. + let min_pin_length = 8; + let min_pin_length_rp_ids = vec!["another.example.com".to_string()]; + // The stored PIN or its length don't matter since we control the token. + persistent_store.set_pin(&[0x88; 16], 8).unwrap(); + let mut config_params = + create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone())); + let pin_auth = vec![ + 0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D, + 0xD6, 0xDA, + ]; + config_params.pin_uv_auth_param = Some(pin_auth.clone()); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids.clone()) + ); + + // Third, changing RP IDs with bad PIN auth fails. + // One PIN auth shouldn't work for different lengths. + let mut config_params = + create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone())); + config_params.pin_uv_auth_param = Some(pin_auth.clone()); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids.clone()) + ); + + // Forth, changing RP IDs with bad PIN auth fails. + // One PIN auth shouldn't work for different RP IDs. + let mut config_params = create_min_pin_config_params( + min_pin_length, + Some(vec!["counter.example.com".to_string()]), + ); + config_params.pin_uv_auth_param = Some(pin_auth); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids) + ); + } + + #[test] + fn test_process_config_vendor_prototype() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::VendorPrototype, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 8081567..9cf149f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -262,6 +262,7 @@ impl From for cbor::Value { pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, + pub min_pin_length: bool, } impl TryFrom for MakeCredentialExtensions { @@ -272,6 +273,7 @@ impl TryFrom for MakeCredentialExtensions { let { "credProtect" => cred_protect, "hmac-secret" => hmac_secret, + "minPinLength" => min_pin_length, } = extract_map(cbor_value)?; } @@ -279,9 +281,11 @@ impl TryFrom for MakeCredentialExtensions { let cred_protect = cred_protect .map(CredentialProtectionPolicy::try_from) .transpose()?; + let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; Ok(Self { hmac_secret, cred_protect, + min_pin_length, }) } } @@ -706,7 +710,6 @@ pub enum ClientPinSubCommand { GetPinToken = 0x05, GetPinUvAuthTokenUsingUvWithPermissions = 0x06, GetUvRetries = 0x07, - SetMinPinLength = 0x08, GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } @@ -729,13 +732,114 @@ impl TryFrom for ClientPinSubCommand { 0x05 => Ok(ClientPinSubCommand::GetPinToken), 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), 0x07 => Ok(ClientPinSubCommand::GetUvRetries), - 0x08 => Ok(ClientPinSubCommand::SetMinPinLength), 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), } } } +#[derive(Clone, Copy)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum ConfigSubCommand { + EnableEnterpriseAttestation = 0x01, + ToggleAlwaysUv = 0x02, + SetMinPinLength = 0x03, + VendorPrototype = 0xFF, +} + +impl From for cbor::Value { + fn from(subcommand: ConfigSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for ConfigSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(ConfigSubCommand::EnableEnterpriseAttestation), + 0x02 => Ok(ConfigSubCommand::ToggleAlwaysUv), + 0x03 => Ok(ConfigSubCommand::SetMinPinLength), + 0xFF => Ok(ConfigSubCommand::VendorPrototype), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub enum ConfigSubCommandParams { + SetMinPinLength(SetMinPinLengthParams), +} + +impl From for cbor::Value { + fn from(params: ConfigSubCommandParams) -> Self { + match params { + ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params) => { + set_min_pin_length_params.into() + } + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct SetMinPinLengthParams { + pub new_min_pin_length: Option, + pub min_pin_length_rp_ids: Option>, + pub force_change_pin: Option, +} + +impl TryFrom for SetMinPinLengthParams { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => new_min_pin_length, + 0x02 => min_pin_length_rp_ids, + 0x03 => force_change_pin, + } = extract_map(cbor_value)?; + } + + let new_min_pin_length = new_min_pin_length + .map(extract_unsigned) + .transpose()? + .map(u8::try_from) + .transpose() + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; + let min_pin_length_rp_ids = match min_pin_length_rp_ids { + Some(entry) => Some( + extract_array(entry)? + .into_iter() + .map(extract_text_string) + .collect::, Ctap2StatusCode>>()?, + ), + None => None, + }; + let force_change_pin = force_change_pin.map(extract_bool).transpose()?; + + Ok(Self { + new_min_pin_length, + min_pin_length_rp_ids, + force_change_pin, + }) + } +} + +impl From for cbor::Value { + fn from(params: SetMinPinLengthParams) -> Self { + cbor_map_options! { + 0x01 => params.new_min_pin_length.map(|u| u as u64), + 0x02 => params.min_pin_length_rp_ids.map(|vec| cbor_array_vec!(vec)), + 0x03 => params.force_change_pin, + } + } +} + pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), @@ -1240,11 +1344,13 @@ mod test { let cbor_extensions = cbor_map! { "hmac-secret" => true, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, + "minPinLength" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), + min_pin_length: true, }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1347,6 +1453,56 @@ mod test { } } + #[test] + fn test_from_into_config_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = ConfigSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = ConfigSubCommand::EnableEnterpriseAttestation; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in ConfigSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = ConfigSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_set_min_pin_length_params() { + let params = SetMinPinLengthParams { + new_min_pin_length: Some(6), + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + force_change_pin: Some(true), + }; + let cbor_params = cbor_map! { + 0x01 => 6, + 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(params.clone()), cbor_params); + let reconstructed_params = SetMinPinLengthParams::try_from(cbor_params); + assert_eq!(reconstructed_params, Ok(params)); + } + + #[test] + fn test_from_config_sub_command_params() { + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(6), + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + force_change_pin: Some(true), + }; + let config_sub_command_params = + ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params); + let cbor_params = cbor_map! { + 0x01 => 6, + 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); + } + #[test] fn test_credential_source_cbor_round_trip() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index fe60e80..233e5b7 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -14,6 +14,7 @@ pub mod apdu; pub mod command; +mod config_command; #[cfg(feature = "with_ctap1")] mod ctap1; pub mod data_formats; @@ -30,6 +31,7 @@ use self::command::{ AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, MAX_CREDENTIAL_COUNT_IN_LIST, }; +use self::config_command::process_config; use self::data_formats::{ AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, @@ -106,6 +108,9 @@ pub const U2F_VERSION_STRING: &str = "U2F_V2"; // TODO(#106) change to final string when ready pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; +// This is the currently supported PIN protocol version. +const PIN_PROTOCOL_VERSION: u64 = 1; + // We currently only support one algorithm for signatures: ES256. // This algorithm is requested in MakeCredential and advertized in GetInfo. pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { @@ -117,6 +122,17 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa // - Some(CredentialProtectionPolicy::UserVerificationRequired) const DEFAULT_CRED_PROTECT: Option = None; +// Checks the PIN protocol parameter against all supported versions. +pub fn check_pin_uv_auth_protocol( + pin_uv_auth_protocol: Option, +) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + Some(PIN_PROTOCOL_VERSION) => Ok(()), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } +} + // This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110 // (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding. // We change the return value, since we don't need the bool. @@ -172,8 +188,6 @@ where R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>, { - pub const PIN_PROTOCOL_VERSION: u64 = 1; - pub fn new( rng: &'a mut R, check_user_presence: CheckUserPresence, @@ -351,6 +365,11 @@ where Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorSelection => self.process_selection(cid), + Command::AuthenticatorConfig(params) => process_config( + &mut self.persistent_store, + &mut self.pin_protocol_v1, + params, + ), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands Command::AuthenticatorVendorConfigure(params) => { @@ -394,11 +413,7 @@ where } } - match pin_uv_auth_protocol { - Some(CtapState::::PIN_PROTOCOL_VERSION) => Ok(()), - Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - None => Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } + check_pin_uv_auth_protocol(pin_uv_auth_protocol) } else { Ok(()) } @@ -427,22 +442,29 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions { - let mut cred_protect = extensions.cred_protect; - if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - < DEFAULT_CRED_PROTECT - .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - { - cred_protect = DEFAULT_CRED_PROTECT; - } - (extensions.hmac_secret, cred_protect) - } else { - (false, DEFAULT_CRED_PROTECT) - }; - - let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); - let rp_id = rp.rp_id; + let (use_hmac_extension, cred_protect_policy, min_pin_length) = + if let Some(extensions) = extensions { + let mut cred_protect = extensions.cred_protect; + if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT + .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect = DEFAULT_CRED_PROTECT; + } + let min_pin_length = extensions.min_pin_length + && self + .persistent_store + .min_pin_length_rp_ids()? + .contains(&rp_id); + (extensions.hmac_secret, cred_protect, min_pin_length) + } else { + (false, DEFAULT_CRED_PROTECT, false) + }; + + let has_extension_output = + use_hmac_extension || cred_protect_policy.is_some() || min_pin_length; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { for cred_desc in exclude_list { @@ -541,9 +563,15 @@ where auth_data.extend(cose_key); if has_extension_output { let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let min_pin_length_output = if min_pin_length { + Some(self.persistent_store.min_pin_length()? as u64) + } else { + None + }; let extensions_output = cbor_map_options! { "hmac-secret" => hmac_secret_output, "credProtect" => cred_protect_policy, + "minPinLength" => min_pin_length_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -832,6 +860,7 @@ where String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("setMinPINLength"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -840,13 +869,15 @@ where String::from(FIDO2_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING), ], - extensions: Some(vec![String::from("hmac-secret")]), + extensions: Some(vec![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), - pin_protocols: Some(vec![ - CtapState::::PIN_PROTOCOL_VERSION, - ]), + pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), // #TODO(106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), @@ -1008,6 +1039,49 @@ mod test { // ID is irrelevant, so we pass this (dummy but valid) value. const DUMMY_CHANNEL_ID: ChannelID = [0x12, 0x34, 0x56, 0x78]; + fn check_make_response( + make_credential_response: Result, + flags: u8, + expected_aaguid: &[u8], + expected_credential_id_size: u8, + expected_extension_cbor: &[u8], + ) { + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let AuthenticatorMakeCredentialResponse { + fmt, + auth_data, + att_stmt, + } = make_credential_response; + // The expected response is split to only assert the non-random parts. + assert_eq!(fmt, "packed"); + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, + ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); + expected_auth_data.extend(expected_aaguid); + expected_auth_data.extend(&[0x00, expected_credential_id_size]); + assert_eq!( + auth_data[0..expected_auth_data.len()], + expected_auth_data[..] + ); + /*assert_eq!( + &auth_data[expected_auth_data.len() + ..expected_auth_data.len() + expected_attested_cred_data.len()], + expected_attested_cred_data + );*/ + assert_eq!( + &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], + expected_extension_cbor + ); + assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_get_info() { let mut rng = ThreadRng256 {}; @@ -1027,19 +1101,22 @@ mod test { expected_response.extend( [ 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, - 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61, + 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, + 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, + 0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50, ] .iter(), ); 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, - 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, - 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, - 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, + 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, + 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, + 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, + 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, + 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, + 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, ] .iter(), ); @@ -1098,6 +1175,7 @@ mod test { let extensions = Some(MakeCredentialExtensions { hmac_secret: false, cred_protect: Some(policy), + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1114,31 +1192,13 @@ mod test { let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - 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()], - expected_auth_data[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); } #[test] @@ -1152,31 +1212,13 @@ mod test { let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + CREDENTIAL_ID_SIZE as u8, + &[], + ); } #[test] @@ -1294,6 +1336,7 @@ mod test { let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1301,39 +1344,16 @@ mod test { let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - let expected_extension_cbor = vec![ - 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0xF5, - ]; - assert_eq!( - auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], - expected_extension_cbor[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + let expected_extension_cbor = [ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + CREDENTIAL_ID_SIZE as u8, + &expected_extension_cbor, + ); } #[test] @@ -1345,45 +1365,80 @@ mod test { let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - 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()], - expected_auth_data[..] - ); - let expected_extension_cbor = vec![ - 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0xF5, - ]; - assert_eq!( - auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], - expected_extension_cbor[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + let expected_extension_cbor = [ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + } + + #[test] + fn test_process_make_credential_min_pin_length() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // First part: The extension is ignored, since the RP ID is not on the list. + let extensions = Some(MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: true, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let mut expected_attested_cred_data = + ctap_state.persistent_store.aaguid().unwrap().to_vec(); + expected_attested_cred_data.extend(&[0x00, 0x20]); + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); + + // Second part: The extension is used. + assert_eq!( + ctap_state + .persistent_store + .set_min_pin_length_rp_ids(vec!["example.com".to_string()]), + Ok(()) + ); + + let extensions = Some(MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: true, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, + 0x04, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); } #[test] @@ -1502,6 +1557,7 @@ mod test { let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1569,6 +1625,7 @@ mod test { let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; @@ -1761,10 +1818,8 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); - ctap_state - .persistent_store - .set_pin_hash(&[0u8; 16]) - .unwrap(); + // The PIN length is outside of the test scope and most likely incorrect. + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79, 0x90, 0x8D, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index b8aeb21..d4f148d 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -17,6 +17,7 @@ use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretIn use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use alloc::str; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; @@ -141,13 +142,14 @@ fn check_and_store_new_pin( .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; let min_pin_length = persistent_store.min_pin_length()? as usize; - if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { - // TODO(kaczmarczyck) check 4 code point minimum instead + let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count(); + if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - persistent_store.set_pin_hash(&pin_hash)?; + // The PIN length is always < 64. + persistent_store.set_pin(&pin_hash, pin_length as u8)?; Ok(()) } @@ -170,6 +172,7 @@ pub struct PinProtocolV1 { consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, + pub force_pin_change: bool, } impl PinProtocolV1 { @@ -182,6 +185,7 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, + force_pin_change: false, } } @@ -363,54 +367,6 @@ impl PinProtocolV1 { Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } - fn process_set_min_pin_length( - &mut self, - persistent_store: &mut PersistentStore, - min_pin_length: u8, - min_pin_length_rp_ids: Option>, - pin_auth: Option>, - ) -> Result<(), Ctap2StatusCode> { - if min_pin_length_rp_ids.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - if persistent_store.pin_hash()?.is_some() { - match pin_auth { - Some(pin_auth) => { - if self.consecutive_pin_mismatches >= 3 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); - } - // TODO(kaczmarczyck) Values are taken from the (not yet public) new revision - // of CTAP 2.1. The code should link the specification when published. - // From CTAP2.1: "If request contains pinUvAuthParam, the Authenticator calls - // verify(pinUvAuthToken, 32×0xff || 0x0608 || uint32LittleEndian(minPINLength) - // || minPinLengthRPIDs, pinUvAuthParam)" - let mut message = vec![0xFF; 32]; - message.extend(&[0x06, 0x08]); - message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]); - // TODO(kaczmarczyck) commented code is useful for the extension - // https://github.com/google/OpenSK/issues/129 - // if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) { - // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - // } - if !verify_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - }; - } - if min_pin_length < persistent_store.min_pin_length()? { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } - persistent_store.set_min_pin_length(min_pin_length)?; - // TODO(kaczmarczyck) commented code is useful for the extension - // https://github.com/google/OpenSK/issues/129 - // if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { - // persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?; - // } - Ok(()) - } - fn process_get_pin_uv_auth_token_using_pin_with_permissions( &mut self, rng: &mut impl Rng256, @@ -450,8 +406,6 @@ impl PinProtocolV1 { pin_auth, new_pin_enc, pin_hash_enc, - min_pin_length, - min_pin_length_rp_ids, permissions, permissions_rp_id, } = client_pin_params; @@ -499,15 +453,6 @@ impl PinProtocolV1 { )?, ), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), - ClientPinSubCommand::SetMinPinLength => { - self.process_set_min_pin_length( - persistent_store, - min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - min_pin_length_rp_ids, - pin_auth, - )?; - None - } ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( rng, @@ -577,7 +522,7 @@ impl PinProtocolV1 { #[cfg(test)] pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, - pin_uv_auth_token: [u8; 32], + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], ) -> PinProtocolV1 { PinProtocolV1 { key_agreement_key, @@ -585,6 +530,7 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, + force_pin_change: false, } } } @@ -600,7 +546,7 @@ mod test { pin[..4].copy_from_slice(b"1234"); let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - persistent_store.set_pin_hash(&pin_hash).unwrap(); + persistent_store.set_pin(&pin_hash, 4).unwrap(); } // Encrypts the message with a zero IV and key derived from shared_secret. @@ -662,7 +608,7 @@ mod test { 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0xC4, 0x12, ]; - persistent_store.set_pin_hash(&pin_hash).unwrap(); + persistent_store.set_pin(&pin_hash, 4).unwrap(); let shared_secret = [0x88; 32]; let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); @@ -935,40 +881,6 @@ mod test { ); } - #[test] - fn test_process_set_min_pin_length() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let min_pin_length = 8; - pin_protocol_v1.pin_uv_auth_token = [0x55; PIN_TOKEN_LENGTH]; - let pin_auth = vec![ - 0x94, 0x86, 0xEF, 0x4C, 0xB3, 0x84, 0x2C, 0x85, 0x72, 0x02, 0xBF, 0xE4, 0x36, 0x22, - 0xFE, 0xC9, - ]; - // TODO(kaczmarczyck) implement test for the min PIN length extension - // https://github.com/google/OpenSK/issues/129 - let response = pin_protocol_v1.process_set_min_pin_length( - &mut persistent_store, - min_pin_length, - None, - Some(pin_auth.clone()), - ); - assert_eq!(response, Ok(())); - assert_eq!(persistent_store.min_pin_length().unwrap(), min_pin_length); - let response = pin_protocol_v1.process_set_min_pin_length( - &mut persistent_store, - 7, - None, - Some(pin_auth), - ); - assert_eq!( - response, - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) - ); - assert_eq!(persistent_store.min_pin_length().unwrap(), min_pin_length); - } - #[test] fn test_process() { let mut rng = ThreadRng256 {}; @@ -981,8 +893,6 @@ mod test { pin_auth: None, new_pin_enc: None, pin_hash_enc: None, - min_pin_length: None, - min_pin_length_rp_ids: None, permissions: None, permissions_rp_id: None, }; @@ -997,8 +907,6 @@ mod test { pin_auth: None, new_pin_enc: None, pin_hash_enc: None, - min_pin_length: None, - min_pin_length_rp_ids: None, permissions: None, permissions_rp_id: None, }; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 390b0cb..0fa8e1e 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -31,6 +31,8 @@ pub enum ResponseData { AuthenticatorClientPin(Option), AuthenticatorReset, AuthenticatorSelection, + // TODO(kaczmarczyck) dummy, extend + AuthenticatorConfig, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -45,6 +47,7 @@ impl From for Option { ResponseData::AuthenticatorClientPin(None) => None, ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorConfig => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } } @@ -368,6 +371,12 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_config_into_cbor() { + let response_cbor: Option = ResponseData::AuthenticatorConfig.into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_vendor_response_into_cbor() { let response_cbor: Option = diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 28c1599..bc5ef95 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -53,11 +53,10 @@ const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -// TODO(kaczmarczyck) use this for the minPinLength extension -// https://github.com/google/OpenSK/issues/129 -const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); -// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. -const _MAX_RP_IDS_LENGTH: usize = 8; +const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); +// This constant is an attempt to limit storage requirements. If you don't set it to 0, +// the stored strings can still be unbounded, but that is true for all RP IDs. +const MAX_RP_IDS_LENGTH: usize = 8; /// Wrapper for master keys. pub struct MasterKeys { @@ -68,6 +67,15 @@ pub struct MasterKeys { pub hmac: [u8; 32], } +/// Wrapper for PIN properties. +struct PinProperties { + /// 16 byte prefix of SHA256 of the currently set PIN. + hash: [u8; PIN_AUTH_LENGTH], + + /// Length of the current PIN in code points. + code_point_length: u8, +} + /// CTAP persistent storage. pub struct PersistentStore { store: persistent_store::Store, @@ -296,26 +304,44 @@ impl PersistentStore { Ok(*array_ref![cred_random_secret, offset, 32]) } - /// Returns the PIN hash if defined. - pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { - let pin_hash = match self.store.find(key::PIN_HASH)? { + /// Reads the PIN properties and wraps them into PinProperties. + fn pin_properties(&self) -> Result, Ctap2StatusCode> { + let pin_properties = match self.store.find(key::PIN_PROPERTIES)? { None => return Ok(None), - Some(pin_hash) => pin_hash, + Some(pin_properties) => pin_properties, }; - if pin_hash.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + const PROPERTIES_LENGTH: usize = PIN_AUTH_LENGTH + 1; + match pin_properties.len() { + PROPERTIES_LENGTH => Ok(Some(PinProperties { + hash: *array_ref![pin_properties, 1, PIN_AUTH_LENGTH], + code_point_length: pin_properties[0], + })), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } - Ok(Some(*array_ref![pin_hash, 0, PIN_AUTH_LENGTH])) } - /// Sets the PIN hash. + /// Returns the PIN hash if defined. + pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { + Ok(self.pin_properties()?.map(|p| p.hash)) + } + + /// Returns the length of the currently set PIN if defined. + pub fn pin_code_point_length(&self) -> Result, Ctap2StatusCode> { + Ok(self.pin_properties()?.map(|p| p.code_point_length)) + } + + /// Sets the PIN hash and length. /// /// If it was already defined, it is updated. - pub fn set_pin_hash( + pub fn set_pin( &mut self, pin_hash: &[u8; PIN_AUTH_LENGTH], + pin_code_point_length: u8, ) -> Result<(), Ctap2StatusCode> { - Ok(self.store.insert(key::PIN_HASH, pin_hash)?) + let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; + pin_properties[0] = pin_code_point_length; + pin_properties[1..].clone_from_slice(pin_hash); + Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?) } /// Returns the number of remaining PIN retries. @@ -358,34 +384,34 @@ impl PersistentStore { /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. - pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { + pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { let rp_ids = self .store - .find(key::_MIN_PIN_LENGTH_RP_IDS)? - .map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { - _deserialize_min_pin_length_rp_ids(&value) + .find(key::MIN_PIN_LENGTH_RP_IDS)? + .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { + deserialize_min_pin_length_rp_ids(&value) }); debug_assert!(rp_ids.is_some()); Ok(rp_ids.unwrap_or_default()) } /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. - pub fn _set_min_pin_length_rp_ids( + pub fn set_min_pin_length_rp_ids( &mut self, min_pin_length_rp_ids: Vec, ) -> Result<(), Ctap2StatusCode> { let mut min_pin_length_rp_ids = min_pin_length_rp_ids; - for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } } - if min_pin_length_rp_ids.len() > _MAX_RP_IDS_LENGTH { + if min_pin_length_rp_ids.len() > MAX_RP_IDS_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } Ok(self.store.insert( - key::_MIN_PIN_LENGTH_RP_IDS, - &_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, + key::MIN_PIN_LENGTH_RP_IDS, + &serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, )?) } @@ -573,7 +599,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result } /// Deserializes a list of RP IDs from storage representation. -fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { +fn deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; extract_array(cbor) .ok()? @@ -584,7 +610,7 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { } /// Serializes a list of RP IDs to storage representation. -fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { +fn serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(cbor_array_vec!(rp_ids), &mut data) { Ok(data) @@ -891,28 +917,38 @@ mod test { } #[test] - fn test_pin_hash() { + fn test_pin_hash_and_length() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // Pin hash is initially not set. assert!(persistent_store.pin_hash().unwrap().is_none()); + assert!(persistent_store.pin_code_point_length().unwrap().is_none()); - // Setting the pin hash sets the pin hash. + // Setting the pin sets the pin hash. let random_data = rng.gen_uniform_u8x32(); assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH); let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH); let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH); - persistent_store.set_pin_hash(&pin_hash_1).unwrap(); + let pin_length_1 = 4; + let pin_length_2 = 63; + persistent_store.set_pin(&pin_hash_1, pin_length_1).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); - assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); - persistent_store.set_pin_hash(&pin_hash_2).unwrap(); - assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); + assert_eq!( + persistent_store.pin_code_point_length().unwrap(), + Some(pin_length_1) + ); + persistent_store.set_pin(&pin_hash_2, pin_length_2).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); + assert_eq!( + persistent_store.pin_code_point_length().unwrap(), + Some(pin_length_2) + ); // Resetting the storage resets the pin hash. persistent_store.reset(&mut rng).unwrap(); assert!(persistent_store.pin_hash().unwrap().is_none()); + assert!(persistent_store.pin_code_point_length().unwrap().is_none()); } #[test] @@ -1006,22 +1042,22 @@ mod test { // The minimum PIN length RP IDs are initially at the default. assert_eq!( - persistent_store._min_pin_length_rp_ids().unwrap(), - _DEFAULT_MIN_PIN_LENGTH_RP_IDS + persistent_store.min_pin_length_rp_ids().unwrap(), + DEFAULT_MIN_PIN_LENGTH_RP_IDS ); // Changes by the setter are reflected by the getter. let mut rp_ids = vec![String::from("example.com")]; assert_eq!( - persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()), + persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } } - assert_eq!(persistent_store._min_pin_length_rp_ids().unwrap(), rp_ids); + assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids); } #[test] @@ -1070,8 +1106,8 @@ mod test { #[test] fn test_serialize_deserialize_min_pin_length_rp_ids() { let rp_ids = vec![String::from("example.com")]; - let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); - let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap(); + let serialized = serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); + let reconstructed = deserialize_min_pin_length_rp_ids(&serialized).unwrap(); assert_eq!(rp_ids, reconstructed); } } diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index ec39efa..dfe44fc 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -92,7 +92,7 @@ make_partition! { CRED_RANDOM_SECRET = 2041; /// List of RP IDs allowed to read the minimum PIN length. - _MIN_PIN_LENGTH_RP_IDS = 2042; + MIN_PIN_LENGTH_RP_IDS = 2042; /// The minimum PIN length. /// @@ -104,10 +104,11 @@ make_partition! { /// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`. PIN_RETRIES = 2044; - /// The PIN hash. + /// The PIN hash and length. /// - /// If the entry is absent, there is no PIN set. - PIN_HASH = 2045; + /// If the entry is absent, there is no PIN set. The first byte represents + /// the length, the following are an array with the hash. + PIN_PROPERTIES = 2045; /// The encryption and hmac keys. /// From ec259d8428fa0536abcd6c2dcdab11b2ef9a3b64 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 7 Jan 2021 18:50:34 +0100 Subject: [PATCH 10/86] adds comments to new config command file --- src/ctap/config_command.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 873a9b1..f270513 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -21,6 +21,7 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use alloc::vec; +/// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, pin_protocol_v1: &mut PinProtocolV1, @@ -52,6 +53,7 @@ fn process_set_min_pin_length( Ok(ResponseData::AuthenticatorConfig) } +/// Processes the AuthenticatorConfig command. pub fn process_config( persistent_store: &mut PersistentStore, pin_protocol_v1: &mut PinProtocolV1, From 6f9f833c0b6c6b94cc1a3d3ff75769d130ad0288 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 8 Jan 2021 15:42:35 +0100 Subject: [PATCH 11/86] moves COSE related conversion from crypto to data_formats --- libraries/crypto/src/ecdh.rs | 8 +++- libraries/crypto/src/ecdsa.rs | 59 ++++++++++----------------- src/ctap/data_formats.rs | 75 +++++++++++++++++++++++++++-------- src/ctap/mod.rs | 10 ++--- 4 files changed, 88 insertions(+), 64 deletions(-) diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index c735d11..9645f66 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -62,8 +62,10 @@ impl SecKey { // - https://www.secg.org/sec1-v2.pdf } - // DH key agreement method defined in the FIDO2 specification, Section 5.5.4. "Getting - // sharedSecret from Authenticator" + /// Creates a shared key using the Diffie Hellman key agreement. + /// + /// The key agreement is defined in the FIDO2 specification, + /// Section 6.5.5.4. "Obtaining the Shared Secret" pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] { let p = self.exchange_raw(other); let mut x: [u8; 32] = [Default::default(); 32]; @@ -83,11 +85,13 @@ impl PubKey { self.p.to_bytes_uncompressed(bytes); } + /// Creates a new PubKey from their coordinates. pub fn from_coordinates(x: &[u8; NBYTES], y: &[u8; NBYTES]) -> Option { PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y)) .map(|p| PubKey { p }) } + /// Writes the coordinates into the passed in arrays. pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) { self.p.getx().to_int().to_bin(x); self.p.gety().to_int().to_bin(y); diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index 52949e3..8fef458 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -21,12 +21,15 @@ use super::rng256::Rng256; use super::{Hash256, HashBlockSize64Bytes}; use alloc::vec; use alloc::vec::Vec; +#[cfg(test)] +use arrayref::array_mut_ref; #[cfg(feature = "std")] use arrayref::array_ref; -use arrayref::{array_mut_ref, mut_array_refs}; -use cbor::{cbor_bytes, cbor_map_options}; +use arrayref::mut_array_refs; use core::marker::PhantomData; +pub const NBYTES: usize = int256::NBYTES; + #[derive(Clone, PartialEq)] #[cfg_attr(feature = "derive_debug", derive(Debug))] pub struct SecKey { @@ -38,6 +41,7 @@ pub struct Signature { s: NonZeroExponentP256, } +#[cfg_attr(feature = "derive_debug", derive(Clone))] pub struct PubKey { p: PointP256, } @@ -58,10 +62,11 @@ impl SecKey { } } - // ECDSA signature based on a RNG to generate a suitable randomization parameter. - // Under the hood, rejection sampling is used to make sure that the randomization parameter is - // uniformly distributed. - // The provided RNG must be cryptographically secure; otherwise this method is insecure. + /// Creates an ECDSA signature based on a RNG. + /// + /// Under the hood, rejection sampling is used to make sure that the + /// randomization parameter is uniformly distributed. The provided RNG must + /// be cryptographically secure; otherwise this method is insecure. pub fn sign_rng(&self, msg: &[u8], rng: &mut R) -> Signature where H: Hash256, @@ -77,8 +82,7 @@ impl SecKey { } } - // Deterministic ECDSA signature based on RFC 6979 to generate a suitable randomization - // parameter. + /// Creates a deterministic ECDSA signature based on RFC 6979. pub fn sign_rfc6979(&self, msg: &[u8]) -> Signature where H: Hash256 + HashBlockSize64Bytes, @@ -101,8 +105,10 @@ impl SecKey { } } - // Try signing a curve element given a randomization parameter k. If no signature can be - // obtained from this k, None is returned and the caller should try again with another value. + /// Try signing a curve element given a randomization parameter k. + /// + /// If no signature can be obtained from this k, None is returned and the + /// caller should try again with another value. fn try_sign(&self, k: &NonZeroExponentP256, msg: &ExponentP256) -> Option { let r = ExponentP256::modn(PointP256::base_point_mul(k.as_exponent()).getx().to_int()); // The branching here is fine because all this reveals is that k generated an unsuitable r. @@ -242,35 +248,10 @@ impl PubKey { representation } - // Encodes the key according to CBOR Object Signing and Encryption, defined in RFC 8152. - pub fn to_cose_key(&self) -> Option> { - const EC2_KEY_TYPE: i64 = 2; - const P_256_CURVE: i64 = 1; - let mut x_bytes = vec![0; int256::NBYTES]; - self.p - .getx() - .to_int() - .to_bin(array_mut_ref![x_bytes.as_mut_slice(), 0, int256::NBYTES]); - let x_byte_cbor: cbor::Value = cbor_bytes!(x_bytes); - let mut y_bytes = vec![0; int256::NBYTES]; - self.p - .gety() - .to_int() - .to_bin(array_mut_ref![y_bytes.as_mut_slice(), 0, int256::NBYTES]); - let y_byte_cbor: cbor::Value = cbor_bytes!(y_bytes); - let cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => PubKey::ES256_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - let mut encoded_key = Vec::new(); - if cbor::write(cbor_value, &mut encoded_key) { - Some(encoded_key) - } else { - None - } + /// Writes the coordinates into the passed in arrays. + pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) { + self.p.getx().to_int().to_bin(x); + self.p.gety().to_int().to_bin(y); } #[cfg(feature = "std")] diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 8081567..ac1fb73 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -18,7 +18,7 @@ use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; -use core::convert::TryFrom; +use core::convert::{TryFrom, TryInto}; use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; @@ -631,26 +631,39 @@ const ES256_ALGORITHM: i64 = -7; const EC2_KEY_TYPE: i64 = 2; const P_256_CURVE: i64 = 1; +impl TryFrom for CoseKey { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + if let cbor::Value::Map(cose_map) = cbor_value { + Ok(CoseKey(cose_map)) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + } +} + +fn cose_key_from_bytes(x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES]) -> CoseKey { + let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); + let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); + // TODO(kaczmarczyck) do not write optional parameters, spec is unclear + let cose_cbor_value = cbor_map_options! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + -2 => x_byte_cbor, + -3 => y_byte_cbor, + }; + // Unwrap is safe here since we know it's a map. + cose_cbor_value.try_into().unwrap() +} + impl From for CoseKey { fn from(pk: ecdh::PubKey) -> Self { let mut x_bytes = [0; ecdh::NBYTES]; let mut y_bytes = [0; ecdh::NBYTES]; pk.to_coordinates(&mut x_bytes, &mut y_bytes); - let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); - let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); - // TODO(kaczmarczyck) do not write optional parameters, spec is unclear - let cose_cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - if let cbor::Value::Map(cose_map) = cose_cbor_value { - CoseKey(cose_map) - } else { - unreachable!(); - } + cose_key_from_bytes(x_bytes, y_bytes) } } @@ -696,6 +709,15 @@ impl TryFrom for ecdh::PubKey { } } +impl From for CoseKey { + fn from(pk: ecdsa::PubKey) -> Self { + let mut x_bytes = [0; ecdh::NBYTES]; + let mut y_bytes = [0; ecdh::NBYTES]; + pk.to_coordinates(&mut x_bytes, &mut y_bytes); + cose_key_from_bytes(x_bytes, y_bytes) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { @@ -1322,7 +1344,7 @@ mod test { } #[test] - fn test_from_into_cose_key() { + fn test_from_into_cose_key_ecdh() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1331,6 +1353,25 @@ mod test { assert_eq!(created_pk, Ok(pk)); } + #[test] + fn test_into_cose_key_ecdsa() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdsa::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cose_map = cose_key.0; + let template = cbor_map! { + 1 => 0, + 3 => 0, + -1 => 0, + -2 => 0, + -3 => 0, + }; + for key in CoseKey::try_from(template).unwrap().0.keys() { + assert!(cose_map.contains_key(key)); + } + } + #[test] fn test_from_into_client_pin_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index fe60e80..f9229ac 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -31,7 +31,7 @@ use self::command::{ MAX_CREDENTIAL_COUNT_IN_LIST, }; use self::data_formats::{ - AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, @@ -534,11 +534,9 @@ where } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); - let cose_key = match pk.to_cose_key() { - Some(cose_key) => cose_key, - None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), - }; - auth_data.extend(cose_key); + if !cbor::write(cbor::Value::Map(CoseKey::from(pk).0), &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } if has_extension_output { let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; let extensions_output = cbor_map_options! { From 18ebeebb3e4ddb3e376bc2ff56de5fc1042e5837 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 11 Jan 2021 11:51:01 +0100 Subject: [PATCH 12/86] adds storage changes for credential management --- src/ctap/mod.rs | 12 +++- src/ctap/response.rs | 19 ++++- src/ctap/storage.rs | 164 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 163 insertions(+), 32 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index fe60e80..4e93210 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -848,13 +848,19 @@ where CtapState::::PIN_PROTOCOL_VERSION, ]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // #TODO(106) update with version 2.1 of HMAC-secret + // TODO(#106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, + max_cred_blob_length: None, + // TODO(kaczmarczyck) update when extension is implemented + max_rp_ids_for_set_min_pin_length: None, + remaining_discoverable_credentials: Some( + self.persistent_store.remaining_credentials()? as u64, + ), }, )) } @@ -1015,7 +1021,7 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAA, 0x01]; + let mut expected_response = vec![0x00, 0xAB, 0x01]; // The version array differs with CTAP1, always including 2.0 and 2.1. #[cfg(not(feature = "with_ctap1"))] let version_count = 2; @@ -1039,7 +1045,7 @@ mod test { 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, - 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, + 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, ] .iter(), ); diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 390b0cb..3ebab3b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -107,7 +107,6 @@ impl From for cbor::Value { #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorGetInfoResponse { - // TODO(kaczmarczyck) add maxAuthenticatorConfigLength and defaultCredProtect pub versions: Vec, pub extensions: Option>, pub aaguid: [u8; 16], @@ -121,6 +120,9 @@ pub struct AuthenticatorGetInfoResponse { pub default_cred_protect: Option, pub min_pin_length: u8, pub firmware_version: Option, + pub max_cred_blob_length: Option, + pub max_rp_ids_for_set_min_pin_length: Option, + pub remaining_discoverable_credentials: Option, } impl From for cbor::Value { @@ -139,6 +141,9 @@ impl From for cbor::Value { default_cred_protect, min_pin_length, firmware_version, + max_cred_blob_length, + max_rp_ids_for_set_min_pin_length, + remaining_discoverable_credentials, } = get_info_response; let options_cbor: Option = options.map(|options| { @@ -163,6 +168,9 @@ impl From for cbor::Value { 0x0C => default_cred_protect.map(|p| p as u64), 0x0D => min_pin_length as u64, 0x0E => firmware_version, + 0x0F => max_cred_blob_length, + 0x10 => max_rp_ids_for_set_min_pin_length, + 0x14 => remaining_discoverable_credentials, } } } @@ -285,6 +293,9 @@ mod test { default_cred_protect: None, min_pin_length: 4, firmware_version: None, + max_cred_blob_length: None, + max_rp_ids_for_set_min_pin_length: None, + remaining_discoverable_credentials: None, }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); @@ -314,6 +325,9 @@ mod test { default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: 4, firmware_version: Some(0), + max_cred_blob_length: Some(1024), + max_rp_ids_for_set_min_pin_length: Some(8), + remaining_discoverable_credentials: Some(150), }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); @@ -331,6 +345,9 @@ mod test { 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, 0x0D => 4, 0x0E => 0, + 0x0F => 1024, + 0x10 => 8, + 0x14 => 150, }; assert_eq!(response_cbor, Some(expected_cbor)); } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 28c1599..76c2fd6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -16,6 +16,7 @@ mod key; use crate::ctap::data_formats::{ extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, }; use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; @@ -116,6 +117,29 @@ impl PersistentStore { Ok(()) } + /// Finds the key and value for a given credential ID. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + fn find_credential_item( + &self, + credential_id: &[u8], + ) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> { + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + let mut credentials: Vec<(usize, PublicKeyCredentialSource)> = iter + .filter(|(_, credential)| credential.credential_id == credential_id) + .collect(); + iter_result?; + if credentials.len() > 1 { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + } + /// Returns the first matching credential. /// /// Returns `None` if no credentials are matched or if `check_cred_protect` is set and the first @@ -126,22 +150,17 @@ impl PersistentStore { credential_id: &[u8], check_cred_protect: bool, ) -> Result, Ctap2StatusCode> { - let mut iter_result = Ok(()); - let iter = self.iter_credentials(&mut iter_result)?; - // We don't check whether there is more than one matching credential to be able to exit - // early. - let result = iter.map(|(_, credential)| credential).find(|credential| { - credential.rp_id == rp_id && credential.credential_id == credential_id - }); - iter_result?; - if let Some(cred) = &result { - let user_verification_required = cred.cred_protect_policy - == Some(CredentialProtectionPolicy::UserVerificationRequired); - if check_cred_protect && user_verification_required { - return Ok(None); - } + let credential = match self.find_credential_item(credential_id) { + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) => return Ok(None), + Err(e) => return Err(e), + Ok(credential) => credential.1, + }; + let is_protected = credential.cred_protect_policy + == Some(CredentialProtectionPolicy::UserVerificationRequired); + if credential.rp_id != rp_id || (check_cred_protect && is_protected) { + return Ok(None); } - Ok(result) + Ok(Some(credential)) } /// Stores or updates a credential. @@ -196,6 +215,34 @@ impl PersistentStore { Ok(()) } + /// Deletes a credential. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { + let (key, _) = self.find_credential_item(credential_id)?; + Ok(self.store.remove(key)?) + } + + /// Updates a credential's user information. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn _update_credential( + &mut self, + credential_id: &[u8], + user: PublicKeyCredentialUserEntity, + ) -> Result<(), Ctap2StatusCode> { + let (key, mut credential) = self.find_credential_item(credential_id)?; + credential.user_name = user.user_name; + credential.user_display_name = user.user_display_name; + credential.user_icon = user.user_icon; + let value = serialize_credential(credential)?; + Ok(self.store.insert(key, &value)?) + } + /// Returns the list of matching credentials. /// /// Does not return credentials that are not discoverable if `check_cred_protect` is set. @@ -221,7 +268,6 @@ impl PersistentStore { } /// Returns the number of credentials. - #[cfg(test)] pub fn count_credentials(&self) -> Result { let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; @@ -230,10 +276,17 @@ impl PersistentStore { Ok(result) } + /// Returns the estimated number of credentials that can still be stored. + pub fn remaining_credentials(&self) -> Result { + MAX_SUPPORTED_RESIDENTIAL_KEYS + .checked_sub(self.count_credentials()?) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + /// Iterates through the credentials. /// /// If an error is encountered during iteration, it is written to `result`. - fn iter_credentials<'a>( + pub fn iter_credentials<'a>( &'a self, result: &'a mut Result<(), Ctap2StatusCode>, ) -> Result, Ctap2StatusCode> { @@ -494,7 +547,7 @@ impl From for Ctap2StatusCode { } /// Iterator for credentials. -struct IterCredentials<'a> { +pub struct IterCredentials<'a> { /// The store being iterated. store: &'a persistent_store::Store, @@ -629,6 +682,66 @@ mod test { assert!(persistent_store.count_credentials().unwrap() > 0); } + #[test] + fn test_delete_credential() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!(persistent_store.count_credentials().unwrap(), 0); + + let mut credential_ids = vec![]; + for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + let user_handle = i.to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); + credential_ids.push(credential_source.credential_id.clone()); + assert!(persistent_store.store_credential(credential_source).is_ok()); + assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); + } + let mut count = persistent_store.count_credentials().unwrap(); + for credential_id in credential_ids { + assert!(persistent_store._delete_credential(&credential_id).is_ok()); + count -= 1; + assert_eq!(persistent_store.count_credentials().unwrap(), count); + } + } + + #[test] + fn test_update_credential() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let user = PublicKeyCredentialUserEntity { + // User ID is ignored. + user_id: vec![0x00], + user_name: Some("name".to_string()), + user_display_name: Some("display_name".to_string()), + user_icon: Some("icon".to_string()), + }; + assert_eq!( + persistent_store._update_credential(&[0x1D], user.clone()), + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + ); + + let credential_source = create_credential_source(&mut rng, "example.com", vec![0x1D]); + let credential_id = credential_source.credential_id.clone(); + assert!(persistent_store.store_credential(credential_source).is_ok()); + let stored_credential = persistent_store + .find_credential("example.com", &credential_id, false) + .unwrap() + .unwrap(); + assert_eq!(stored_credential.user_name, None); + assert_eq!(stored_credential.user_display_name, None); + assert_eq!(stored_credential.user_icon, None); + assert!(persistent_store + ._update_credential(&credential_id, user.clone()) + .is_ok()); + let stored_credential = persistent_store + .find_credential("example.com", &credential_id, false) + .unwrap() + .unwrap(); + assert_eq!(stored_credential.user_name, user.user_name); + assert_eq!(stored_credential.user_display_name, user.user_display_name); + assert_eq!(stored_credential.user_icon, user.user_icon); + } + #[test] fn test_credential_order() { let mut rng = ThreadRng256 {}; @@ -645,17 +758,14 @@ mod test { } #[test] - #[allow(clippy::assertions_on_constants)] fn test_fill_store() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); - // To make this test work for bigger storages, implement better int -> Vec conversion. - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { - let credential_source = - create_credential_source(&mut rng, "example.com", vec![i as u8]); + let user_handle = i.to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } @@ -675,7 +785,6 @@ mod test { } #[test] - #[allow(clippy::assertions_on_constants)] fn test_overwrite() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -699,11 +808,10 @@ mod test { &[expected_credential] ); - // To make this test work for bigger storages, implement better int -> Vec conversion. - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); + let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { - let credential_source = - create_credential_source(&mut rng, "example.com", vec![i as u8]); + let user_handle = i.to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } From 4cee0c4c656374664426a8c7ed5d8221ceb9b983 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 11 Jan 2021 14:31:13 +0100 Subject: [PATCH 13/86] only keeps keys instead of credentials as state --- src/ctap/mod.rs | 76 ++++++++++++++++++++++++++------------------- src/ctap/storage.rs | 64 ++++++++++++++++++++++++++------------ 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4e93210..5072a47 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -142,7 +142,7 @@ struct AssertionInput { struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. - next_credentials: Vec, + next_credential_keys: Vec, } enum StatefulCommand { @@ -606,7 +606,7 @@ where // and returns the correct Get(Next)Assertion response. fn assertion_response( &mut self, - credential: PublicKeyCredentialSource, + mut credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, ) -> Result { @@ -642,6 +642,12 @@ where key_id: credential.credential_id, transports: None, // You can set USB as a hint here. }; + // Remove user identifiable information without uv. + if !has_uv { + credential.user_name = None; + credential.user_display_name = None; + credential.user_icon = None; + } let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle, @@ -749,26 +755,23 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut applicable_credentials = if let Some(allow_list) = allow_list { - if let Some(credential) = - self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? - { - vec![credential] - } else { - vec![] - } + let (credential, next_credential_keys) = if let Some(allow_list) = allow_list { + ( + self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)?, + vec![], + ) } else { - self.persistent_store.filter_credential(&rp_id, !has_uv)? + let mut stored_credentials = + self.persistent_store.filter_credentials(&rp_id, !has_uv)?; + stored_credentials.sort_unstable_by_key(|c| c.1); + let mut stored_credentials: Vec = + stored_credentials.into_iter().map(|c| c.0).collect(); + let credential = stored_credentials + .pop() + .map(|key| self.persistent_store.get_credential(key)) + .transpose()?; + (credential, stored_credentials) }; - // Remove user identifiable information without uv. - if !has_uv { - for credential in &mut applicable_credentials { - credential.user_name = None; - credential.user_display_name = None; - credential.user_icon = None; - } - } - applicable_credentials.sort_unstable_by_key(|c| c.creation_order); // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. // For CTAP 2.1, it was moved to a later protocol step. @@ -776,9 +779,7 @@ where (self.check_user_presence)(cid)?; } - let credential = applicable_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.increment_global_signature_counter()?; @@ -788,15 +789,15 @@ where hmac_secret_input, has_uv, }; - let number_of_credentials = if applicable_credentials.is_empty() { + let number_of_credentials = if next_credential_keys.is_empty() { None } else { - let number_of_credentials = Some(applicable_credentials.len() + 1); + let number_of_credentials = Some(next_credential_keys.len() + 1); self.stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), - next_credentials: applicable_credentials, + next_credential_keys, })); number_of_credentials }; @@ -812,10 +813,11 @@ where if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.stateful_command_type { - let credential = assertion_state - .next_credentials + let credential_key = assertion_state + .next_credential_keys .pop() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = self.persistent_store.get_credential(credential_key)?; (assertion_state.assertion_input.clone(), credential) } else { return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); @@ -1250,11 +1252,16 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let stored_credential = ctap_state + let credential_key = ctap_state .persistent_store - .filter_credential("example.com", false) + .filter_credentials("example.com", false) .unwrap() .pop() + .unwrap() + .0; + let stored_credential = ctap_state + .persistent_store + .get_credential(credential_key) .unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); @@ -1275,11 +1282,16 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let stored_credential = ctap_state + let credential_key = ctap_state .persistent_store - .filter_credential("example.com", false) + .filter_credentials("example.com", false) .unwrap() .pop() + .unwrap() + .0; + let stored_credential = ctap_state + .persistent_store + .get_credential(credential_key) .unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 76c2fd6..343751a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -117,6 +117,24 @@ impl PersistentStore { Ok(()) } + /// Returns the credential at the given key. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_VENDOR_INTERNAL_ERROR` if the key does not hold a valid credential. + pub fn get_credential(&self, key: usize) -> Result { + let min_key = key::CREDENTIALS.start; + if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENTIAL_KEYS { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let credential_entry = self + .store + .find(key)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + deserialize_credential(&credential_entry) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + /// Finds the key and value for a given credential ID. /// /// # Errors @@ -246,22 +264,23 @@ impl PersistentStore { /// Returns the list of matching credentials. /// /// Does not return credentials that are not discoverable if `check_cred_protect` is set. - pub fn filter_credential( + pub fn filter_credentials( &self, rp_id: &str, check_cred_protect: bool, - ) -> Result, Ctap2StatusCode> { + ) -> Result, Ctap2StatusCode> { let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; let result = iter - .filter_map(|(_, credential)| { - if credential.rp_id == rp_id { - Some(credential) + .filter_map(|(key, credential)| { + if credential.rp_id == rp_id + && (!check_cred_protect || credential.is_discoverable()) + { + Some((key, credential.creation_order)) } else { None } }) - .filter(|cred| !check_cred_protect || cred.is_discoverable()) .collect(); iter_result?; Ok(result) @@ -801,12 +820,13 @@ mod test { .store_credential(credential_source1) .is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), 1); - assert_eq!( - &persistent_store - .filter_credential("example.com", false) - .unwrap(), - &[expected_credential] - ); + let filtered_credentials = persistent_store + .filter_credentials("example.com", false) + .unwrap(); + let retrieved_credential_source = persistent_store + .get_credential(filtered_credentials[0].0) + .unwrap(); + assert_eq!(retrieved_credential_source, expected_credential); let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { @@ -831,7 +851,7 @@ mod test { } #[test] - fn test_filter() { + fn test_filter_get_credentials() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); @@ -852,14 +872,20 @@ mod test { .is_ok()); let filtered_credentials = persistent_store - .filter_credential("example.com", false) + .filter_credentials("example.com", false) .unwrap(); assert_eq!(filtered_credentials.len(), 2); + let retrieved_credential0 = persistent_store + .get_credential(filtered_credentials[0].0) + .unwrap(); + let retrieved_credential1 = persistent_store + .get_credential(filtered_credentials[1].0) + .unwrap(); assert!( - (filtered_credentials[0].credential_id == id0 - && filtered_credentials[1].credential_id == id1) - || (filtered_credentials[1].credential_id == id0 - && filtered_credentials[0].credential_id == id1) + (retrieved_credential0.credential_id == id0 + && retrieved_credential1.credential_id == id1) + || (retrieved_credential1.credential_id == id0 + && retrieved_credential0.credential_id == id1) ); } @@ -886,7 +912,7 @@ mod test { assert!(persistent_store.store_credential(credential).is_ok()); let no_credential = persistent_store - .filter_credential("example.com", true) + .filter_credentials("example.com", true) .unwrap(); assert_eq!(no_credential, vec![]); } From 27a7108328fcea9bba7bfe1b1411167e9a7551ef Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 07:01:25 +0100 Subject: [PATCH 14/86] moves filter_credentials to call side --- src/ctap/mod.rs | 52 +++++++++++--------- src/ctap/storage.rs | 112 ++++++++------------------------------------ 2 files changed, 49 insertions(+), 115 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 5072a47..2047500 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -761,11 +761,23 @@ where vec![], ) } else { - let mut stored_credentials = - self.persistent_store.filter_credentials(&rp_id, !has_uv)?; - stored_credentials.sort_unstable_by_key(|c| c.1); - let mut stored_credentials: Vec = - stored_credentials.into_iter().map(|c| c.0).collect(); + let mut iter_result = Ok(()); + let iter = self.persistent_store.iter_credentials(&mut iter_result)?; + let mut stored_credentials: Vec<(usize, u64)> = iter + .filter_map(|(key, credential)| { + if credential.rp_id == rp_id && (has_uv || credential.is_discoverable()) { + Some((key, credential.creation_order)) + } else { + None + } + }) + .collect(); + iter_result?; + stored_credentials.sort_unstable_by_key(|&(_key, order)| order); + let mut stored_credentials: Vec = stored_credentials + .into_iter() + .map(|(key, _order)| key) + .collect(); let credential = stored_credentials .pop() .map(|key| self.persistent_store.get_credential(key)) @@ -1252,17 +1264,14 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let credential_key = ctap_state + let mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credentials("example.com", false) - .unwrap() - .pop() - .unwrap() - .0; - let stored_credential = ctap_state - .persistent_store - .get_credential(credential_key) + .iter_credentials(&mut iter_result) .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); @@ -1282,17 +1291,14 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let credential_key = ctap_state + let mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credentials("example.com", false) - .unwrap() - .pop() - .unwrap() - .0; - let stored_credential = ctap_state - .persistent_store - .get_credential(credential_key) + .iter_credentials(&mut iter_result) .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 343751a..8df987d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -171,7 +171,7 @@ impl PersistentStore { let credential = match self.find_credential_item(credential_id) { Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) => return Ok(None), Err(e) => return Err(e), - Ok(credential) => credential.1, + Ok((_key, credential)) => credential, }; let is_protected = credential.cred_protect_policy == Some(CredentialProtectionPolicy::UserVerificationRequired); @@ -261,31 +261,6 @@ impl PersistentStore { Ok(self.store.insert(key, &value)?) } - /// Returns the list of matching credentials. - /// - /// Does not return credentials that are not discoverable if `check_cred_protect` is set. - pub fn filter_credentials( - &self, - rp_id: &str, - check_cred_protect: bool, - ) -> Result, Ctap2StatusCode> { - let mut iter_result = Ok(()); - let iter = self.iter_credentials(&mut iter_result)?; - let result = iter - .filter_map(|(key, credential)| { - if credential.rp_id == rp_id - && (!check_cred_protect || credential.is_discoverable()) - { - Some((key, credential.creation_order)) - } else { - None - } - }) - .collect(); - iter_result?; - Ok(result) - } - /// Returns the number of credentials. pub fn count_credentials(&self) -> Result { let mut iter_result = Ok(()); @@ -811,7 +786,8 @@ mod test { // These should have different IDs. let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x00]); - let expected_credential = credential_source1.clone(); + let credential_id0 = credential_source0.credential_id.clone(); + let credential_id1 = credential_source1.credential_id.clone(); assert!(persistent_store .store_credential(credential_source0) @@ -820,13 +796,14 @@ mod test { .store_credential(credential_source1) .is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), 1); - let filtered_credentials = persistent_store - .filter_credentials("example.com", false) - .unwrap(); - let retrieved_credential_source = persistent_store - .get_credential(filtered_credentials[0].0) - .unwrap(); - assert_eq!(retrieved_credential_source, expected_credential); + assert!(persistent_store + .find_credential("example.com", &credential_id0, false) + .unwrap() + .is_none()); + assert!(persistent_store + .find_credential("example.com", &credential_id1, false) + .unwrap() + .is_some()); let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { @@ -851,70 +828,21 @@ mod test { } #[test] - fn test_filter_get_credentials() { + fn test_get_credential() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - assert_eq!(persistent_store.count_credentials().unwrap(), 0); let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]); let credential_source2 = create_credential_source(&mut rng, "another.example.com", vec![0x02]); - let id0 = credential_source0.credential_id.clone(); - let id1 = credential_source1.credential_id.clone(); - assert!(persistent_store - .store_credential(credential_source0) - .is_ok()); - assert!(persistent_store - .store_credential(credential_source1) - .is_ok()); - assert!(persistent_store - .store_credential(credential_source2) - .is_ok()); - - let filtered_credentials = persistent_store - .filter_credentials("example.com", false) - .unwrap(); - assert_eq!(filtered_credentials.len(), 2); - let retrieved_credential0 = persistent_store - .get_credential(filtered_credentials[0].0) - .unwrap(); - let retrieved_credential1 = persistent_store - .get_credential(filtered_credentials[1].0) - .unwrap(); - assert!( - (retrieved_credential0.credential_id == id0 - && retrieved_credential1.credential_id == id1) - || (retrieved_credential1.credential_id == id0 - && retrieved_credential0.credential_id == id1) - ); - } - - #[test] - fn test_filter_with_cred_protect() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - assert_eq!(persistent_store.count_credentials().unwrap(), 0); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let credential = PublicKeyCredentialSource { - key_type: PublicKeyCredentialType::PublicKey, - credential_id: rng.gen_uniform_u8x32().to_vec(), - private_key, - rp_id: String::from("example.com"), - user_handle: vec![0x00], - user_display_name: None, - cred_protect_policy: Some( - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - ), - creation_order: 0, - user_name: None, - user_icon: None, - }; - assert!(persistent_store.store_credential(credential).is_ok()); - - let no_credential = persistent_store - .filter_credentials("example.com", true) - .unwrap(); - assert_eq!(no_credential, vec![]); + let credential_sources = vec![credential_source0, credential_source1, credential_source2]; + for credential_source in credential_sources.into_iter() { + let cred_id = credential_source.credential_id.clone(); + assert!(persistent_store.store_credential(credential_source).is_ok()); + let (key, _) = persistent_store.find_credential_item(&cred_id).unwrap(); + let cred = persistent_store.get_credential(key).unwrap(); + assert_eq!(&cred_id, &cred.credential_id); + } } #[test] From 2776bd9b8ec7af7f486b4db2cb1100625a810e28 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 15:11:20 +0100 Subject: [PATCH 15/86] new CoseKey data format --- libraries/crypto/src/ecdh.rs | 2 +- libraries/crypto/src/ecdsa.rs | 1 - src/ctap/command.rs | 13 +- src/ctap/data_formats.rs | 275 +++++++++++++++++++++++----------- src/ctap/mod.rs | 2 +- src/ctap/response.rs | 2 +- 6 files changed, 196 insertions(+), 99 deletions(-) diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index 9645f66..a1e3736 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -85,7 +85,7 @@ impl PubKey { self.p.to_bytes_uncompressed(bytes); } - /// Creates a new PubKey from their coordinates. + /// Creates a new PubKey from its coordinates on the elliptic curve. pub fn from_coordinates(x: &[u8; NBYTES], y: &[u8; NBYTES]) -> Option { PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y)) .map(|p| PubKey { p }) diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index 8fef458..b6a1708 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -220,7 +220,6 @@ impl Signature { } impl PubKey { - pub const ES256_ALGORITHM: i64 = -7; #[cfg(feature = "with_ctap1")] const UNCOMPRESSED_LENGTH: usize = 1 + 2 * int256::NBYTES; diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 0a86093..6f0aa72 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -317,7 +317,7 @@ impl TryFrom for AuthenticatorClientPinParameters { 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(CoseKey); + let key_agreement = key_agreement.map(CoseKey::try_from).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()?; @@ -423,8 +423,8 @@ mod test { }; use super::super::ES256_CRED_PARAM; use super::*; - use alloc::collections::BTreeMap; use cbor::{cbor_array, cbor_map}; + use crypto::rng256::ThreadRng256; #[test] fn test_from_cbor_make_credential_parameters() { @@ -534,10 +534,15 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cbor_value = cbor_map! { 1 => 1, 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor_map!{}, + 3 => cbor::Value::from(cose_key.clone()), 4 => vec! [0xBB], 5 => vec! [0xCC], 6 => vec! [0xDD], @@ -552,7 +557,7 @@ mod test { let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { pin_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: Some(CoseKey(BTreeMap::new())), + key_agreement: Some(cose_key), pin_auth: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ac1fb73..87bc6b4 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -17,12 +17,23 @@ use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; -use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; -use core::convert::{TryFrom, TryInto}; +use cbor::{cbor_array_vec, cbor_map, cbor_map_options, destructure_cbor_map}; +use core::convert::TryFrom; use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; +// This is the algorithm specifier that is supposed to be used in a COSE key +// map in ECDH. CTAP requests -25 which represents ECDH-ES + HKDF-256 here: +// https://www.iana.org/assignments/cose/cose.xhtml#algorithms +const ECDH_ALGORITHM: i64 = -25; +// This is the identifier used for ECDSA and ECDH in OpenSSH. +const ES256_ALGORITHM: i64 = -7; +// The COSE key parameter behind map key 1. +const EC2_KEY_TYPE: i64 = 2; +// The COSE key parameter behind map key -1. +const P_256_CURVE: i64 = 1; + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialRpEntity { @@ -322,17 +333,17 @@ impl TryFrom for GetAssertionHmacSecretInput { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => cose_key, + 1 => key_agreement, 2 => salt_enc, 3 => salt_auth, } = extract_map(cbor_value)?; } - let cose_key = extract_map(ok_or_missing(cose_key)?)?; + let key_agreement = CoseKey::try_from(ok_or_missing(key_agreement)?)?; 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), + key_agreement, salt_enc, salt_auth, }) @@ -432,7 +443,7 @@ impl From for cbor::Value { #[derive(PartialEq)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub enum SignatureAlgorithm { - ES256 = ecdsa::PubKey::ES256_ALGORITHM as isize, + ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. // Unknown types should be ignored, instead of returning errors. Unknown = 0, @@ -449,7 +460,7 @@ impl TryFrom for SignatureAlgorithm { fn try_from(cbor_value: cbor::Value) -> Result { match extract_integer(cbor_value)? { - ecdsa::PubKey::ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), + ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), _ => Ok(SignatureAlgorithm::Unknown), } } @@ -614,85 +625,31 @@ impl PublicKeyCredentialSource { } } -// TODO(kaczmarczyck) we could decide to split this data type up -// It depends on the algorithm though, I think. -// So before creating a mess, this is my workaround. +// The COSE key is used for both ECDH and ECDSA public keys for transmission. #[derive(Clone)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] -pub struct CoseKey(pub BTreeMap); - -// This is the algorithm specifier that is supposed to be used in a COSE key -// map. The CTAP specification says -25 which represents ECDH-ES + HKDF-256 -// here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms -// In fact, this is just used for compatibility with older specification versions. -const ECDH_ALGORITHM: i64 = -25; -// This is the identifier used by OpenSSH. To be compatible, we accept both. -const ES256_ALGORITHM: i64 = -7; -const EC2_KEY_TYPE: i64 = 2; -const P_256_CURVE: i64 = 1; +pub struct CoseKey { + x_bytes: [u8; ecdh::NBYTES], + y_bytes: [u8; ecdh::NBYTES], + algorithm: i64, +} +// This conversion accepts both ECDH and ECDSA. impl TryFrom for CoseKey { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - if let cbor::Value::Map(cose_map) = cbor_value { - Ok(CoseKey(cose_map)) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) - } - } -} - -fn cose_key_from_bytes(x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES]) -> CoseKey { - let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); - let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); - // TODO(kaczmarczyck) do not write optional parameters, spec is unclear - let cose_cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - // Unwrap is safe here since we know it's a map. - cose_cbor_value.try_into().unwrap() -} - -impl From for CoseKey { - fn from(pk: ecdh::PubKey) -> Self { - let mut x_bytes = [0; ecdh::NBYTES]; - let mut y_bytes = [0; ecdh::NBYTES]; - pk.to_coordinates(&mut x_bytes, &mut y_bytes); - cose_key_from_bytes(x_bytes, y_bytes) - } -} - -impl TryFrom for ecdh::PubKey { - type Error = Ctap2StatusCode; - - fn try_from(cose_key: CoseKey) -> Result { destructure_cbor_map! { let { + // This is sorted correctly, negative encoding is bigger. 1 => key_type, 3 => algorithm, -1 => curve, -2 => x_bytes, -3 => y_bytes, - } = cose_key.0; + } = extract_map(cbor_value)?; } - 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(algorithm)?)?; - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } - 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(x_bytes)?)?; if x_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -701,11 +658,55 @@ impl TryFrom for ecdh::PubKey { if y_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } + let curve = extract_integer(ok_or_missing(curve)?)?; + if curve != P_256_CURVE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + 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(algorithm)?)?; + if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } - 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) - .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + Ok(CoseKey { + x_bytes: *array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES], + y_bytes: *array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES], + algorithm, + }) + } +} + +impl From for cbor::Value { + fn from(cose_key: CoseKey) -> Self { + let CoseKey { + x_bytes, + y_bytes, + algorithm, + } = cose_key; + + cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => algorithm, + -1 => P_256_CURVE, + -2 => x_bytes, + -3 => y_bytes, + } + } +} + +impl From for CoseKey { + fn from(pk: ecdh::PubKey) -> Self { + let mut x_bytes = [0; ecdh::NBYTES]; + let mut y_bytes = [0; ecdh::NBYTES]; + pk.to_coordinates(&mut x_bytes, &mut y_bytes); + CoseKey { + x_bytes, + y_bytes, + algorithm: ECDH_ALGORITHM, + } } } @@ -714,7 +715,32 @@ impl From for CoseKey { let mut x_bytes = [0; ecdh::NBYTES]; let mut y_bytes = [0; ecdh::NBYTES]; pk.to_coordinates(&mut x_bytes, &mut y_bytes); - cose_key_from_bytes(x_bytes, y_bytes) + CoseKey { + x_bytes, + y_bytes, + algorithm: ES256_ALGORITHM, + } + } +} + +impl TryFrom for ecdh::PubKey { + type Error = Ctap2StatusCode; + + fn try_from(cose_key: CoseKey) -> Result { + let CoseKey { + x_bytes, + y_bytes, + algorithm, + } = cose_key; + + // Since algorithm can be used for different COSE key types, we check + // whether the current type is correct for ECDH. For an OpenSSH bugfix, + // the algorithm ES256_ALGORITHM is allowed here too. + if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes) + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) } } @@ -827,8 +853,8 @@ mod test { use super::*; use alloc::collections::BTreeMap; use cbor::{ - cbor_array, cbor_bool, cbor_bytes, cbor_false, cbor_int, cbor_map, cbor_null, cbor_text, - cbor_unsigned, + cbor_array, cbor_bool, cbor_bytes, cbor_bytes_lit, cbor_false, cbor_int, cbor_null, + cbor_text, cbor_unsigned, }; use crypto::rng256::{Rng256, ThreadRng256}; @@ -1151,7 +1177,7 @@ mod test { #[test] fn test_from_into_signature_algorithm() { - let cbor_signature_algorithm: cbor::Value = cbor_int!(ecdsa::PubKey::ES256_ALGORITHM); + let cbor_signature_algorithm: cbor::Value = cbor_int!(ES256_ALGORITHM); let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); let expected_signature_algorithm = SignatureAlgorithm::ES256; assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); @@ -1225,7 +1251,7 @@ mod test { fn test_from_into_public_key_credential_parameter() { let cbor_credential_parameter = cbor_map! { "type" => "public-key", - "alg" => ecdsa::PubKey::ES256_ALGORITHM, + "alg" => ES256_ALGORITHM, }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); @@ -1279,7 +1305,7 @@ mod test { let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { "hmac-secret" => cbor_map! { - 1 => cbor::Value::Map(cose_key.0.clone()), + 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, @@ -1343,6 +1369,83 @@ mod test { assert_eq!(created_cbor, cbor_packed_attestation_statement); } + #[test] + fn test_from_into_cose_key_cbor() { + for algorithm in &[ECDH_ALGORITHM, ES256_ALGORITHM] { + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => algorithm, + -1 => P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + let cose_key = CoseKey::try_from(cbor_value.clone()).unwrap(); + let created_cbor_value = cbor::Value::from(cose_key); + assert_eq!(created_cbor_value, cbor_value); + } + + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + // unknown algorithm + 3 => 0, + -1 => P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + let cbor_value = cbor_map! { + // unknown type + 1 => 0, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + // unknown curve + -1 => 0, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + // wrong length + -2 => [0u8; 31], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + -2 => [0u8; 32], + // wrong length + -3 => [0u8; 33], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_from_into_cose_key_ecdh() { let mut rng = ThreadRng256 {}; @@ -1359,17 +1462,7 @@ mod test { let sk = crypto::ecdsa::SecKey::gensk(&mut rng); let pk = sk.genpk(); let cose_key = CoseKey::from(pk); - let cose_map = cose_key.0; - let template = cbor_map! { - 1 => 0, - 3 => 0, - -1 => 0, - -2 => 0, - -3 => 0, - }; - for key in CoseKey::try_from(template).unwrap().0.keys() { - assert!(cose_map.contains_key(key)); - } + assert_eq!(cose_key.algorithm, ES256_ALGORITHM); } #[test] diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 676f4c2..8895605 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -534,7 +534,7 @@ where } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); - if !cbor::write(cbor::Value::Map(CoseKey::from(pk).0), &mut auth_data) { + if !cbor::write(cbor::Value::from(CoseKey::from(pk)), &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if has_extension_output { diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 3ebab3b..f40dc21 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -192,7 +192,7 @@ impl From for cbor::Value { } = client_pin_response; cbor_map_options! { - 1 => key_agreement.map(|cose_key| cbor_map_btree!(cose_key.0)), + 1 => key_agreement.map(cbor::Value::from), 2 => pin_token, 3 => retries, } From da27848c27c7a56cb6265d61ddc6eda50838f3dd Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 17:17:23 +0100 Subject: [PATCH 16/86] updates license header to 2021 in ctap --- src/ctap/apdu.rs | 2 +- src/ctap/command.rs | 2 +- src/ctap/config_command.rs | 2 +- src/ctap/ctap1.rs | 2 +- src/ctap/data_formats.rs | 2 +- src/ctap/hid/mod.rs | 2 +- src/ctap/hid/receive.rs | 2 +- src/ctap/hid/send.rs | 2 +- src/ctap/key_material.rs | 2 +- src/ctap/mod.rs | 2 +- src/ctap/pin_protocol_v1.rs | 2 +- src/ctap/response.rs | 2 +- src/ctap/status_code.rs | 2 +- src/ctap/storage.rs | 2 +- src/ctap/timed_permission.rs | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index c99bf84..f475308 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2020-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 6b6ab54..dcb96d6 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index f270513..57e2a97 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2020-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 0932e2c..0bf43b5 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9cf149f..05b8374 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 01c0b11..71bd7c8 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/hid/receive.rs b/src/ctap/hid/receive.rs index b522837..8efdb1c 100644 --- a/src/ctap/hid/receive.rs +++ b/src/ctap/hid/receive.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/hid/send.rs b/src/ctap/hid/send.rs index 434d633..22f9c61 100644 --- a/src/ctap/hid/send.rs +++ b/src/ctap/hid/send.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index eec5456..a8ae6da 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 7b5b2de..98ed569 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index d4f148d..e2a84eb 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2020-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/response.rs b/src/ctap/response.rs index df38148..b4d4ed4 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 5a9ec71..a593dad 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0c6d15f..0bc9c5f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/timed_permission.rs b/src/ctap/timed_permission.rs index fcc0ada..868e9da 100644 --- a/src/ctap/timed_permission.rs +++ b/src/ctap/timed_permission.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2 (the "License"); // you may not use this file except in compliance with the License. From c30268a099c53c3f3219070525ba777bbe1c7bf9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 17:57:58 +0100 Subject: [PATCH 17/86] code cleanups and clarifications --- src/ctap/config_command.rs | 9 +++++---- src/ctap/mod.rs | 11 +---------- src/ctap/pin_protocol_v1.rs | 3 --- src/ctap/storage.rs | 5 +++++ 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 57e2a97..e09bab3 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -24,7 +24,6 @@ use alloc::vec; /// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, params: SetMinPinLengthParams, ) -> Result { let SetMinPinLengthParams { @@ -44,8 +43,10 @@ fn process_set_min_pin_length( if let Some(old_length) = persistent_store.pin_code_point_length()? { force_change_pin |= new_min_pin_length > old_length; } - pin_protocol_v1.force_pin_change |= force_change_pin; - // TODO(kaczmarczyck) actually force a PIN change + if force_change_pin { + // TODO(kaczmarczyck) actually force a PIN change in PinProtocolV1 + persistent_store.force_pin_change()?; + } persistent_store.set_min_pin_length(new_min_pin_length)?; if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?; @@ -86,7 +87,7 @@ pub fn process_config( match sub_command { ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { - process_set_min_pin_length(persistent_store, pin_protocol_v1, params) + process_set_min_pin_length(persistent_store, params) } else { Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 98ed569..1d7e286 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -128,8 +128,7 @@ pub fn check_pin_uv_auth_protocol( ) -> Result<(), Ctap2StatusCode> { match pin_uv_auth_protocol { Some(PIN_PROTOCOL_VERSION) => Ok(()), - Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - None => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), } } @@ -1087,11 +1086,6 @@ mod test { auth_data[0..expected_auth_data.len()], expected_auth_data[..] ); - /*assert_eq!( - &auth_data[expected_auth_data.len() - ..expected_auth_data.len() + expected_attested_cred_data.len()], - expected_attested_cred_data - );*/ assert_eq!( &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], expected_extension_cbor @@ -1424,9 +1418,6 @@ mod test { make_credential_params.extensions = extensions; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - let mut expected_attested_cred_data = - ctap_state.persistent_store.aaguid().unwrap().to_vec(); - expected_attested_cred_data.extend(&[0x00, 0x20]); check_make_response( make_credential_response, 0x41, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index e2a84eb..eef0440 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -172,7 +172,6 @@ pub struct PinProtocolV1 { consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, - pub force_pin_change: bool, } impl PinProtocolV1 { @@ -185,7 +184,6 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, - force_pin_change: false, } } @@ -530,7 +528,6 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, - force_pin_change: false, } } } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0bc9c5f..cf9afbc 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -544,6 +544,11 @@ impl PersistentStore { self.init(rng)?; Ok(()) } + + pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { + // TODO(kaczmarczyck) implement storage logic + Ok(()) + } } impl From for Ctap2StatusCode { From 78167282f90937a4656d1e84ddbd3b37e877abc7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 19:11:32 +0100 Subject: [PATCH 18/86] comment for constants --- src/ctap/config_command.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index e09bab3..19d0cc2 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -72,6 +72,7 @@ pub fn process_config( check_pin_uv_auth_protocol(pin_uv_auth_protocol) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + // Constants are taken from the specification, section 6.11, step 4.2. let mut config_data = vec![0xFF; 32]; config_data.extend(&[0x0D, sub_command as u8]); if let Some(sub_command_params) = sub_command_params.clone() { From cc86fc274201143e5b7b213657aeac15a75af605 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 08:52:00 +0100 Subject: [PATCH 19/86] removes unused import --- src/ctap/config_command.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 19d0cc2..726634c 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -99,7 +99,6 @@ pub fn process_config( #[cfg(test)] mod test { - use super::super::command::AuthenticatorConfigParameters; use super::*; use crypto::rng256::ThreadRng256; From a26de3b720e953a2e01371539b3b77c1d7bd3e9b Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 14:00:34 +0100 Subject: [PATCH 20/86] moves constants to CoseKey --- src/ctap/data_formats.rs | 86 +++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 87bc6b4..ffe2336 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -23,16 +23,8 @@ use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; -// This is the algorithm specifier that is supposed to be used in a COSE key -// map in ECDH. CTAP requests -25 which represents ECDH-ES + HKDF-256 here: -// https://www.iana.org/assignments/cose/cose.xhtml#algorithms -const ECDH_ALGORITHM: i64 = -25; -// This is the identifier used for ECDSA and ECDH in OpenSSH. +// Used as the identifier for ECDSA in assertion signatures and COSE. const ES256_ALGORITHM: i64 = -7; -// The COSE key parameter behind map key 1. -const EC2_KEY_TYPE: i64 = 2; -// The COSE key parameter behind map key -1. -const P_256_CURVE: i64 = 1; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] @@ -634,6 +626,17 @@ pub struct CoseKey { algorithm: i64, } +impl CoseKey { + // This is the algorithm specifier for ECDH. + // CTAP requests -25 which represents ECDH-ES + HKDF-256 here: + // https://www.iana.org/assignments/cose/cose.xhtml#algorithms + const ECDH_ALGORITHM: i64 = -25; + // The parameter behind map key 1. + const EC2_KEY_TYPE: i64 = 2; + // The parameter behind map key -1. + const P_256_CURVE: i64 = 1; +} + // This conversion accepts both ECDH and ECDSA. impl TryFrom for CoseKey { type Error = Ctap2StatusCode; @@ -659,15 +662,15 @@ impl TryFrom for CoseKey { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let curve = extract_integer(ok_or_missing(curve)?)?; - if curve != P_256_CURVE { + if curve != CoseKey::P_256_CURVE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } let key_type = extract_integer(ok_or_missing(key_type)?)?; - if key_type != EC2_KEY_TYPE { + if key_type != CoseKey::EC2_KEY_TYPE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } let algorithm = extract_integer(ok_or_missing(algorithm)?)?; - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } @@ -688,9 +691,9 @@ impl From for cbor::Value { } = cose_key; cbor_map! { - 1 => EC2_KEY_TYPE, + 1 => CoseKey::EC2_KEY_TYPE, 3 => algorithm, - -1 => P_256_CURVE, + -1 => CoseKey::P_256_CURVE, -2 => x_bytes, -3 => y_bytes, } @@ -705,7 +708,7 @@ impl From for CoseKey { CoseKey { x_bytes, y_bytes, - algorithm: ECDH_ALGORITHM, + algorithm: CoseKey::ECDH_ALGORITHM, } } } @@ -735,8 +738,8 @@ impl TryFrom for ecdh::PubKey { // Since algorithm can be used for different COSE key types, we check // whether the current type is correct for ECDH. For an OpenSSH bugfix, - // the algorithm ES256_ALGORITHM is allowed here too. - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + // the algorithm ES256_ALGORITHM is allowed here too. See #90. + if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes) @@ -1371,11 +1374,11 @@ mod test { #[test] fn test_from_into_cose_key_cbor() { - for algorithm in &[ECDH_ALGORITHM, ES256_ALGORITHM] { + for algorithm in &[CoseKey::ECDH_ALGORITHM, ES256_ALGORITHM] { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, + 1 => CoseKey::EC2_KEY_TYPE, 3 => algorithm, - -1 => P_256_CURVE, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], -3 => [0u8; 32], }; @@ -1383,12 +1386,15 @@ mod test { let created_cbor_value = cbor::Value::from(cose_key); assert_eq!(created_cbor_value, cbor_value); } + } + #[test] + fn test_cose_key_unknown_algorithm() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, + 1 => CoseKey::EC2_KEY_TYPE, // unknown algorithm 3 => 0, - -1 => P_256_CURVE, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], -3 => [0u8; 32], }; @@ -1396,11 +1402,15 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) ); + } + + #[test] + fn test_cose_key_unknown_type() { let cbor_value = cbor_map! { // unknown type 1 => 0, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], -3 => [0u8; 32], }; @@ -1408,9 +1418,13 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) ); + } + + #[test] + fn test_cose_key_unknown_curve() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, // unknown curve -1 => 0, -2 => [0u8; 32], @@ -1420,10 +1434,14 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) ); + } + + #[test] + fn test_cose_key_wrong_length_x() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, // wrong length -2 => [0u8; 31], -3 => [0u8; 32], @@ -1432,10 +1450,14 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); + } + + #[test] + fn test_cose_key_wrong_length_y() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], // wrong length -3 => [0u8; 33], From 3e42531011d554b1b39d3dcb66fe8536e990c7b9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 14:26:59 +0100 Subject: [PATCH 21/86] full URL --- src/ctap/data_formats.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ffe2336..dfdf4ed 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -738,7 +738,8 @@ impl TryFrom for ecdh::PubKey { // Since algorithm can be used for different COSE key types, we check // whether the current type is correct for ECDH. For an OpenSSH bugfix, - // the algorithm ES256_ALGORITHM is allowed here too. See #90. + // the algorithm ES256_ALGORITHM is allowed here too. + // https://github.com/google/OpenSK/issues/90 if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } From c6726660ac09ea05766d09960f93761fe3651d6c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 11:22:41 +0100 Subject: [PATCH 22/86] adds the command logic for credential management --- src/ctap/command.rs | 91 ++- src/ctap/credential_management.rs | 912 ++++++++++++++++++++++++++++++ src/ctap/data_formats.rs | 148 ++++- src/ctap/mod.rs | 86 ++- src/ctap/pin_protocol_v1.rs | 20 + src/ctap/response.rs | 141 ++++- src/ctap/storage.rs | 10 +- 7 files changed, 1366 insertions(+), 42 deletions(-) create mode 100644 src/ctap/credential_management.rs diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 6f0aa72..57056a7 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -14,10 +14,10 @@ use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, - extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, - GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, CredentialManagementSubCommand, + CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -41,6 +41,7 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, + AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands @@ -110,6 +111,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } + Command::AUTHENTICATOR_CREDENTIAL_MANAGEMENT => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorCredentialManagement( + AuthenticatorCredentialManagementParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_SELECTION => { // Parameters are ignored. Ok(Command::AuthenticatorSelection) @@ -388,6 +395,43 @@ impl TryFrom for AuthenticatorAttestationMaterial { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorCredentialManagementParameters { + pub sub_command: CredentialManagementSubCommand, + pub sub_command_params: Option, + pub pin_protocol: Option, + pub pin_auth: Option>, +} + +impl TryFrom for AuthenticatorCredentialManagementParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_protocol, + 0x04 => pin_auth, + } = extract_map(cbor_value)?; + } + + let sub_command = CredentialManagementSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = sub_command_params + .map(CredentialManagementSubCommandParameters::try_from) + .transpose()?; + let pin_protocol = pin_protocol.map(extract_unsigned).transpose()?; + let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + + Ok(AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorVendorConfigureParameters { pub lockdown: bool, @@ -551,10 +595,10 @@ mod test { 9 => 0x03, 10 => "example.com", }; - let returned_pin_protocol_parameters = + let returned_client_pin_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); - let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { + let expected_client_pin_parameters = AuthenticatorClientPinParameters { pin_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), @@ -568,8 +612,8 @@ mod test { }; assert_eq!( - returned_pin_protocol_parameters, - expected_pin_protocol_parameters + returned_client_pin_parameters, + expected_client_pin_parameters ); } @@ -595,6 +639,37 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } + #[test] + fn test_from_cbor_cred_management_parameters() { + let cbor_value = cbor_map! { + 1 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 2 => cbor_map!{ + 0x01 => vec![0x1D; 32], + }, + 3 => 1, + 4 => vec! [0x9A; 16], + }; + let returned_cred_management_parameters = + AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap(); + + let params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: None, + user: None, + }; + let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(params), + pin_protocol: Some(1), + pin_auth: Some(vec![0x9A; 16]), + }; + + assert_eq!( + returned_cred_management_parameters, + expected_cred_management_parameters + ); + } + #[test] fn test_deserialize_selection() { let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs new file mode 100644 index 0000000..4a0d29c --- /dev/null +++ b/src/ctap/credential_management.rs @@ -0,0 +1,912 @@ +// Copyright 2020-2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::command::AuthenticatorCredentialManagementParameters; +use super::data_formats::{ + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, +}; +use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; +use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use super::timed_permission::TimedPermission; +use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION}; +use alloc::collections::BTreeSet; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::iter::FromIterator; +use crypto::sha256::Sha256; +use crypto::Hash256; +use libtock_drivers::timer::ClockValue; + +/// Generates the response for subcommands enumerating RPs. +fn enumerate_rps_response( + rp_id: Option, + total_rps: Option, +) -> Result { + let rp = rp_id.clone().map(|rp_id| PublicKeyCredentialRpEntity { + rp_id, + rp_name: None, + rp_icon: None, + }); + let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); + + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp, + rp_id_hash, + total_rps, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }) +} + +/// Generates the response for subcommands enumerating credentials. +fn enumerate_credentials_response( + credential: PublicKeyCredentialSource, + total_credentials: Option, +) -> Result { + let PublicKeyCredentialSource { + key_type, + credential_id, + private_key, + rp_id: _, + user_handle, + user_display_name, + cred_protect_policy, + creation_order: _, + user_name, + user_icon, + } = credential; + let user = PublicKeyCredentialUserEntity { + user_id: user_handle, + user_name, + user_display_name, + user_icon, + }; + let credential_id = PublicKeyCredentialDescriptor { + key_type, + key_id: credential_id, + transports: None, // You can set USB as a hint here. + }; + let public_key = CoseKey::from(private_key.genpk()); + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: Some(user), + credential_id: Some(credential_id), + public_key: Some(public_key), + total_credentials, + cred_protect: cred_protect_policy, + // TODO(kaczmarczyck) add when largeBlobKey is implemented + large_blob_key: None, + }) +} + +/// Processes the subcommand getCredsMetadata for CredentialManagement. +fn process_get_creds_metadata( + persistent_store: &PersistentStore, +) -> Result { + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: Some(persistent_store.count_credentials()? as u64), + max_possible_remaining_resident_credentials_count: Some( + persistent_store.remaining_credentials()? as u64, + ), + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }) +} + +/// Processes the subcommand enumerateRPsBegin for CredentialManagement. +fn process_enumerate_rps_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + let mut rp_set = BTreeSet::new(); + let mut iter_result = Ok(()); + for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { + rp_set.insert(credential.rp_id); + } + iter_result?; + let mut rp_ids = Vec::from_iter(rp_set); + let total_rps = rp_ids.len(); + + // TODO(kaczmarczyck) behaviour with empty list? + let rp_id = rp_ids.pop(); + if total_rps > 1 { + *stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + *stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids)); + } + enumerate_rps_response(rp_id, Some(total_rps as u64)) +} + +/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. +fn process_enumerate_rps_get_next_rp( + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + check_command_permission(stateful_command_permission, now)?; + if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type { + let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + enumerate_rps_response(Some(rp_id), None) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } +} + +/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. +fn process_enumerate_credentials_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + sub_command_params: CredentialManagementSubCommandParameters, + now: ClockValue, +) -> Result { + let rp_id_hash = sub_command_params + .rp_id_hash + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + let mut iter_result = Ok(()); + let iter = persistent_store.iter_credentials(&mut iter_result)?; + let mut rp_credentials: Vec = iter + .filter_map(|(key, credential)| { + let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes()); + if cred_rp_id_hash == rp_id_hash.as_slice() { + Some(key) + } else { + None + } + }) + .collect(); + iter_result?; + let total_credentials = rp_credentials.len(); + let current_key = rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + let credential = persistent_store.get_credential(current_key)?; + if total_credentials > 1 { + *stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + *stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials)); + } + enumerate_credentials_response(credential, Some(total_credentials as u64)) +} + +/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement. +fn process_enumerate_credentials_get_next_credential( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + mut stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + check_command_permission(stateful_command_permission, now)?; + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type + { + let current_key = rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = persistent_store.get_credential(current_key)?; + enumerate_credentials_response(credential, None) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } +} + +/// Processes the subcommand deleteCredential for CredentialManagement. +fn process_delete_credential( + persistent_store: &mut PersistentStore, + sub_command_params: CredentialManagementSubCommandParameters, +) -> Result<(), Ctap2StatusCode> { + let credential_id = sub_command_params + .credential_id + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? + .key_id; + persistent_store.delete_credential(&credential_id) +} + +/// Processes the subcommand updateUserInformation for CredentialManagement. +fn process_update_user_information( + persistent_store: &mut PersistentStore, + sub_command_params: CredentialManagementSubCommandParameters, +) -> Result<(), Ctap2StatusCode> { + let credential_id = sub_command_params + .credential_id + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? + .key_id; + let user = sub_command_params + .user + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + persistent_store.update_credential(&credential_id, user) +} + +/// Checks the PIN protocol. +/// +/// TODO(#246) refactor after #246 is merged +fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + Some(1) => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } +} + +/// Processes the CredentialManagement command and all its subcommands. +pub fn process_credential_management( + persistent_store: &mut PersistentStore, + stateful_command_permission: &mut TimedPermission, + mut stateful_command_type: &mut Option, + pin_protocol_v1: &mut PinProtocolV1, + cred_management_params: AuthenticatorCredentialManagementParameters, + now: ClockValue, +) -> Result { + let AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + } = cred_management_params; + + match (sub_command, &mut stateful_command_type) { + ( + CredentialManagementSubCommand::EnumerateRpsGetNextRp, + Some(StatefulCommand::EnumerateRps(_)), + ) => (), + ( + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + Some(StatefulCommand::EnumerateCredentials(_)), + ) => (), + (_, _) => { + *stateful_command_type = None; + } + } + + match sub_command { + CredentialManagementSubCommand::GetCredsMetadata + | CredentialManagementSubCommand::EnumerateRpsBegin + | CredentialManagementSubCommand::DeleteCredential + | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::UpdateUserInformation => { + pin_uv_auth_protocol_check(pin_protocol)?; + persistent_store + .pin_hash()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let mut management_data = vec![sub_command as u8]; + if let Some(sub_command_params) = sub_command_params.clone() { + if !cbor::write(sub_command_params.into(), &mut management_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; + pin_protocol_v1.has_no_permission_rp_id()?; + // TODO(kaczmarczyck) sometimes allow a RP ID + } + CredentialManagementSubCommand::EnumerateRpsGetNextRp + | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} + } + + let response = match sub_command { + CredentialManagementSubCommand::GetCredsMetadata => { + Some(process_get_creds_metadata(persistent_store)?) + } + CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( + persistent_store, + stateful_command_permission, + stateful_command_type, + now, + )?), + CredentialManagementSubCommand::EnumerateRpsGetNextRp => { + Some(process_enumerate_rps_get_next_rp( + stateful_command_permission, + stateful_command_type, + now, + )?) + } + CredentialManagementSubCommand::EnumerateCredentialsBegin => { + Some(process_enumerate_credentials_begin( + persistent_store, + stateful_command_permission, + stateful_command_type, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + now, + )?) + } + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => { + Some(process_enumerate_credentials_get_next_credential( + persistent_store, + stateful_command_permission, + stateful_command_type, + now, + )?) + } + CredentialManagementSubCommand::DeleteCredential => { + process_delete_credential( + persistent_store, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + CredentialManagementSubCommand::UpdateUserInformation => { + process_update_user_information( + persistent_store, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + }; + Ok(ResponseData::AuthenticatorCredentialManagement(response)) +} + +#[cfg(test)] +mod test { + use super::super::data_formats::PublicKeyCredentialType; + use super::super::CtapState; + use super::*; + use crypto::rng256::{Rng256, ThreadRng256}; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + + fn create_credential_source(rng: &mut impl Rng256) -> PublicKeyCredentialSource { + let private_key = crypto::ecdsa::SecKey::gensk(rng); + PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: rng.gen_uniform_u8x32().to_vec(), + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x01], + user_display_name: Some("display_name".to_string()), + cred_protect_policy: None, + creation_order: 0, + user_name: Some("name".to_string()), + user_icon: Some("icon".to_string()), + } + } + + #[test] + fn test_process_get_creds_metadata() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source = create_credential_source(&mut rng); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, + 0xFB, 0x00, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth: pin_auth.clone(), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let initial_capacity = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.existing_resident_credentials_count, Some(0)); + response + .max_possible_remaining_resident_credentials_count + .unwrap() + } + _ => panic!("Invalid response type"), + }; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.existing_resident_credentials_count, Some(1)); + assert_eq!( + response.max_possible_remaining_resident_credentials_count, + Some(initial_capacity - 1) + ); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_enumerate_rps_with_uv() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source1 = create_credential_source(&mut rng); + let mut credential_source2 = create_credential_source(&mut rng); + credential_source2.rp_id = "another.example.com".to_string(); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, + 0xD0, 0xD1, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let first_rp_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.total_rps, Some(2)); + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + rp_id + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let second_rp_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.total_rps, None); + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + rp_id + } + _ => panic!("Invalid response type"), + }; + + assert!(first_rp_id != second_rp_id); + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_enumerate_credentials_with_uv() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source1 = create_credential_source(&mut rng); + let mut credential_source2 = create_credential_source(&mut rng); + credential_source2.user_handle = vec![0x02]; + credential_source2.user_name = Some("user2".to_string()); + credential_source2.user_display_name = Some("User Two".to_string()); + credential_source2.user_icon = Some("icon2".to_string()); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, + 0x2B, 0x68, + ]); + + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()), + credential_id: None, + user: None, + }; + // RP ID hash: + // A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947 + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let first_credential_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, Some(2)); + response.credential_id.unwrap().key_id + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let second_credential_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, None); + response.credential_id.unwrap().key_id + } + _ => panic!("Invalid response type"), + }; + + assert!(first_credential_id != second_credential_id); + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_delete_credential() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut credential_source = create_credential_source(&mut rng); + credential_source.credential_id = vec![0x1D; 32]; + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89, + 0x9C, 0x64, + ]); + + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, // You can set USB as a hint here. + }; + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: None, + credential_id: Some(credential_id), + user: None, + }; + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::DeleteCredential, + sub_command_params: Some(sub_command_params.clone()), + pin_protocol: Some(1), + pin_auth: pin_auth.clone(), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Ok(ResponseData::AuthenticatorCredentialManagement(None)) + ); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::DeleteCredential, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + ); + } + + #[test] + fn test_process_update_user_information() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut credential_source = create_credential_source(&mut rng); + credential_source.credential_id = vec![0x1D; 32]; + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD, + 0x9D, 0xB7, + ]); + + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, // You can set USB as a hint here. + }; + let new_user = PublicKeyCredentialUserEntity { + user_id: vec![0xFF], + user_name: Some("new_name".to_string()), + user_display_name: Some("new_display_name".to_string()), + user_icon: Some("new_icon".to_string()), + }; + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: None, + credential_id: Some(credential_id), + user: Some(new_user), + }; + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::UpdateUserInformation, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Ok(ResponseData::AuthenticatorCredentialManagement(None)) + ); + + let updated_credential = ctap_state + .persistent_store + .find_credential("example.com", &[0x1D; 32], false) + .unwrap() + .unwrap(); + assert_eq!(updated_credential.user_handle, vec![0x01]); + assert_eq!(&updated_credential.user_name.unwrap(), "new_name"); + assert_eq!( + &updated_credential.user_display_name.unwrap(), + "new_display_name" + ); + assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon"); + } + + #[test] + fn test_process_credential_management_invalid_pin_protocol() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, + 0xFB, 0x00, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(123456), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_process_credential_management_invalid_pin_auth() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth: Some(vec![0u8; 16]), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dfdf4ed..bb330ef 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,6 +27,7 @@ use enum_iterator::IntoEnumIterator; const ES256_ALGORITHM: i64 = -7; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity +#[derive(Clone)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialRpEntity { pub rp_id: String, @@ -58,8 +59,19 @@ impl TryFrom for PublicKeyCredentialRpEntity { } } +impl From for cbor::Value { + fn from(entity: PublicKeyCredentialRpEntity) -> Self { + cbor_map_options! { + "id" => entity.rp_id, + "name" => entity.rp_name, + "icon" => entity.rp_icon, + } + } +} + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -173,7 +185,8 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -210,7 +223,8 @@ impl TryFrom for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -788,6 +802,88 @@ impl TryFrom for ClientPinSubCommand { } } +#[derive(Clone, Copy)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum CredentialManagementSubCommand { + GetCredsMetadata = 0x01, + EnumerateRpsBegin = 0x02, + EnumerateRpsGetNextRp = 0x03, + EnumerateCredentialsBegin = 0x04, + EnumerateCredentialsGetNextCredential = 0x05, + DeleteCredential = 0x06, + UpdateUserInformation = 0x07, +} + +impl From for cbor::Value { + fn from(subcommand: CredentialManagementSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for CredentialManagementSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(CredentialManagementSubCommand::GetCredsMetadata), + 0x02 => Ok(CredentialManagementSubCommand::EnumerateRpsBegin), + 0x03 => Ok(CredentialManagementSubCommand::EnumerateRpsGetNextRp), + 0x04 => Ok(CredentialManagementSubCommand::EnumerateCredentialsBegin), + 0x05 => Ok(CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential), + 0x06 => Ok(CredentialManagementSubCommand::DeleteCredential), + 0x07 => Ok(CredentialManagementSubCommand::UpdateUserInformation), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct CredentialManagementSubCommandParameters { + pub rp_id_hash: Option>, + pub credential_id: Option, + pub user: Option, +} + +impl TryFrom for CredentialManagementSubCommandParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => rp_id_hash, + 0x02 => credential_id, + 0x03 => user, + } = extract_map(cbor_value)?; + } + + let rp_id_hash = rp_id_hash.map(extract_byte_string).transpose()?; + let credential_id = credential_id + .map(PublicKeyCredentialDescriptor::try_from) + .transpose()?; + let user = user + .map(PublicKeyCredentialUserEntity::try_from) + .transpose()?; + Ok(Self { + rp_id_hash, + credential_id, + user, + }) + } +} + +impl From for cbor::Value { + fn from(sub_command_params: CredentialManagementSubCommandParameters) -> Self { + cbor_map_options! { + 0x01 => sub_command_params.rp_id_hash, + 0x02 => sub_command_params.credential_id, + 0x03 => sub_command_params.user, + } + } +} + pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), @@ -1504,6 +1600,52 @@ mod test { } } + #[test] + fn test_from_into_cred_management_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = CredentialManagementSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = CredentialManagementSubCommand::GetCredsMetadata; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in CredentialManagementSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = CredentialManagementSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_into_cred_management_sub_command_params() { + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], + transports: Some(vec![AuthenticatorTransport::Usb]), + }; + let user_entity = PublicKeyCredentialUserEntity { + user_id: vec![0x1D, 0x1D, 0x1D, 0x1D], + user_name: Some("foo".to_string()), + user_display_name: Some("bar".to_string()), + user_icon: Some("example.com/foo/icon.png".to_string()), + }; + let cbor_sub_command_params = cbor_map! { + 0x01 => vec![0x1D; 32], + 0x02 => credential_id.clone(), + 0x03 => user_entity.clone(), + }; + let sub_command_params = + CredentialManagementSubCommandParameters::try_from(cbor_sub_command_params.clone()); + let expected_sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: Some(credential_id), + user: Some(user_entity), + }; + assert_eq!(sub_command_params, Ok(expected_sub_command_params)); + let created_cbor: cbor::Value = sub_command_params.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command_params); + } + #[test] fn test_credential_source_cbor_round_trip() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8895605..d4b73f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -14,6 +14,7 @@ pub mod apdu; pub mod command; +mod credential_management; #[cfg(feature = "with_ctap1")] mod ctap1; pub mod data_formats; @@ -30,6 +31,7 @@ use self::command::{ AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, MAX_CREDENTIAL_COUNT_IN_LIST, }; +use self::credential_management::process_credential_management; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, @@ -98,7 +100,7 @@ pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); -const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); +pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -139,15 +141,29 @@ struct AssertionInput { has_uv: bool, } -struct AssertionState { +pub struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. next_credential_keys: Vec, } -enum StatefulCommand { +pub enum StatefulCommand { Reset, GetAssertion(AssertionState), + EnumerateRps(Vec), + EnumerateCredentials(Vec), +} + +pub fn check_command_permission( + stateful_command_permission: &mut TimedPermission, + now: ClockValue, +) -> Result<(), Ctap2StatusCode> { + *stateful_command_permission = stateful_command_permission.check_expiration(now); + if stateful_command_permission.is_granted(now) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } } // This struct currently holds all state, not only the persistent memory. The persistent members are @@ -200,15 +216,6 @@ where self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); } - fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { - self.update_command_permission(now); - if self.stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } - } - pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { if USE_SIGNATURE_COUNTER { let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1; @@ -330,6 +337,14 @@ where Command::AuthenticatorGetNextAssertion, Some(StatefulCommand::GetAssertion(_)), ) => (), + ( + Command::AuthenticatorCredentialManagement(_), + Some(StatefulCommand::EnumerateRps(_)), + ) => (), + ( + Command::AuthenticatorCredentialManagement(_), + Some(StatefulCommand::EnumerateCredentials(_)), + ) => (), (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), // GetInfo does not reset stateful commands. (Command::AuthenticatorGetInfo, _) => (), @@ -350,6 +365,16 @@ where Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), + Command::AuthenticatorCredentialManagement(params) => { + process_credential_management( + &mut self.persistent_store, + &mut self.stateful_command_permission, + &mut self.stateful_command_type, + &mut self.pin_protocol_v1, + params, + now, + ) + } Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands @@ -818,7 +843,7 @@ where &mut self, now: ClockValue, ) -> Result { - self.check_command_permission(now)?; + check_command_permission(&mut self.stateful_command_permission, now)?; let (assertion_input, credential) = if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.stateful_command_type @@ -844,6 +869,7 @@ where String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("credMgmt"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -895,7 +921,7 @@ where ) -> Result { // Resets are only possible in the first 10 seconds after booting. // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? - self.check_command_permission(now)?; + check_command_permission(&mut self.stateful_command_permission, now)?; match &self.stateful_command_type { Some(StatefulCommand::Reset) => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), @@ -1053,11 +1079,12 @@ mod test { 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, - 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, - 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, - 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, + 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, + 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, + 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, + 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, + 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, + 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, ] .iter(), ); @@ -2034,6 +2061,22 @@ mod test { assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); } + #[test] + fn test_process_credential_management_unknown_subcommand() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // The subcommand 0xEE does not exist. + let reponse = ctap_state.process_command( + &[0x0A, 0xA1, 0x01, 0x18, 0xEE], + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let expected_response = vec![Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND as u8]; + assert_eq!(reponse, expected_response); + } + #[test] fn test_process_unknown_command() { let mut rng = ThreadRng256 {}; @@ -2041,10 +2084,9 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // This command does not exist. - let reset_reponse = - ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + let reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; - assert_eq!(reset_reponse, expected_response); + assert_eq!(reponse, expected_response); } #[test] diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index b8aeb21..14c3d56 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -563,6 +563,13 @@ impl PinProtocolV1 { } } + pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> { + if self.permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + Ok(()) + } + pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { if let Some(permissions_rp_id) = &self.permissions_rp_id { if rp_id != permissions_rp_id { @@ -1187,6 +1194,19 @@ mod test { } } + #[test] + fn test_has_no_permission_rp_id() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(())); + assert_eq!(pin_protocol_v1.permissions_rp_id, None,); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_permission_rp_id(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_has_permission_for_rp_id() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index f40dc21..8d5386b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -14,7 +14,8 @@ use super::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; use alloc::collections::BTreeMap; use alloc::string::String; @@ -30,6 +31,7 @@ pub enum ResponseData { AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorClientPin(Option), AuthenticatorReset, + AuthenticatorCredentialManagement(Option), AuthenticatorSelection, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -41,9 +43,9 @@ impl From for Option { ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetInfo(data) => Some(data.into()), - ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), - ResponseData::AuthenticatorClientPin(None) => None, + ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()), ResponseData::AuthenticatorReset => None, + ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()), ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -199,6 +201,54 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorCredentialManagementResponse { + pub existing_resident_credentials_count: Option, + pub max_possible_remaining_resident_credentials_count: Option, + pub rp: Option, + pub rp_id_hash: Option>, + pub total_rps: Option, + pub user: Option, + pub credential_id: Option, + pub public_key: Option, + pub total_credentials: Option, + pub cred_protect: Option, + pub large_blob_key: Option>, +} + +impl From for cbor::Value { + fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self { + let AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count, + max_possible_remaining_resident_credentials_count, + rp, + rp_id_hash, + total_rps, + user, + credential_id, + public_key, + total_credentials, + cred_protect, + large_blob_key, + } = cred_management_response; + + cbor_map_options! { + 0x01 => existing_resident_credentials_count, + 0x02 => max_possible_remaining_resident_credentials_count, + 0x03 => rp, + 0x04 => rp_id_hash, + 0x05 => total_rps, + 0x06 => user, + 0x07 => credential_id, + 0x08 => public_key.map(cbor::Value::from), + 0x09 => total_credentials, + 0x0A => cred_protect, + 0x0B => large_blob_key, + } + } +} + #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorVendorResponse { @@ -222,10 +272,11 @@ impl From for cbor::Value { #[cfg(test)] mod test { - use super::super::data_formats::PackedAttestationStatement; + use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType}; use super::super::ES256_CRED_PARAM; use super::*; use cbor::{cbor_bytes, cbor_map}; + use crypto::rng256::ThreadRng256; #[test] fn test_make_credential_into_cbor() { @@ -379,6 +430,88 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_used_credential_management_into_cbor() { + let cred_management_response = AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }; + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); + let expected_cbor = cbor_map_options! {}; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_used_credential_management_optionals_into_cbor() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let rp = PublicKeyCredentialRpEntity { + rp_id: String::from("example.com"), + rp_name: None, + rp_icon: None, + }; + let user = PublicKeyCredentialUserEntity { + user_id: vec![0xFA, 0xB1, 0xA2], + user_name: None, + user_display_name: None, + user_icon: None, + }; + let cred_descriptor = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, + }; + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + + let cred_management_response = AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: Some(100), + max_possible_remaining_resident_credentials_count: Some(96), + rp: Some(rp.clone()), + rp_id_hash: Some(vec![0x1D; 32]), + total_rps: Some(3), + user: Some(user.clone()), + credential_id: Some(cred_descriptor.clone()), + public_key: Some(cose_key.clone()), + total_credentials: Some(2), + cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional), + large_blob_key: Some(vec![0xBB; 64]), + }; + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); + let expected_cbor = cbor_map_options! { + 0x01 => 100, + 0x02 => 96, + 0x03 => rp, + 0x04 => vec![0x1D; 32], + 0x05 => 3, + 0x06 => user, + 0x07 => cred_descriptor, + 0x08 => cbor::Value::from(cose_key), + 0x09 => 2, + 0x0A => 0x01, + 0x0B => vec![0xBB; 64], + }; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_empty_credential_management_into_cbor() { + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(None).into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_selection_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 8df987d..7902f34 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -238,7 +238,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { + pub fn delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { let (key, _) = self.find_credential_item(credential_id)?; Ok(self.store.remove(key)?) } @@ -248,7 +248,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - pub fn _update_credential( + pub fn update_credential( &mut self, credential_id: &[u8], user: PublicKeyCredentialUserEntity, @@ -692,7 +692,7 @@ mod test { } let mut count = persistent_store.count_credentials().unwrap(); for credential_id in credential_ids { - assert!(persistent_store._delete_credential(&credential_id).is_ok()); + assert!(persistent_store.delete_credential(&credential_id).is_ok()); count -= 1; assert_eq!(persistent_store.count_credentials().unwrap(), count); } @@ -710,7 +710,7 @@ mod test { user_icon: Some("icon".to_string()), }; assert_eq!( - persistent_store._update_credential(&[0x1D], user.clone()), + persistent_store.update_credential(&[0x1D], user.clone()), Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) ); @@ -725,7 +725,7 @@ mod test { assert_eq!(stored_credential.user_display_name, None); assert_eq!(stored_credential.user_icon, None); assert!(persistent_store - ._update_credential(&credential_id, user.clone()) + .update_credential(&credential_id, user.clone()) .is_ok()); let stored_credential = persistent_store .find_credential("example.com", &credential_id, false) From 182afc7c3f76b2a185f5536ce9d2c1d131305261 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 14 Jan 2021 12:33:03 +0100 Subject: [PATCH 23/86] Add Feitian OpenSK USB Dongle (#257) (#259) Co-authored-by: superskybird Co-authored-by: Geoffrey Co-authored-by: superskybird --- docs/install.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/install.md b/docs/install.md index d00b991..166d5b5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -17,6 +17,7 @@ You will need one the following supported boards: * [Nordic nRF52840 Dongle](https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF52840-Dongle) to have a more practical form factor. * [Makerdiary nRF52840-MDK USB dongle](https://wiki.makerdiary.com/nrf52840-mdk/). +* [Feitian OpenSK dongle](https://feitiantech.github.io/OpenSK_USB/). In the case of the Nordic USB dongle, you may also need the following extra hardware: From 0bb6ee32fc96d32faa45a64127c7ae806a159019 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 14 Jan 2021 16:45:38 +0100 Subject: [PATCH 24/86] removes unused duplicate PIN protocol check helper --- src/ctap/credential_management.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index dfc004e..c83c955 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -23,7 +23,10 @@ use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use super::timed_permission::TimedPermission; -use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION}; +use super::{ + check_command_permission, check_pin_uv_auth_protocol, StatefulCommand, + STATEFUL_COMMAND_TIMEOUT_DURATION, +}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -251,16 +254,6 @@ fn process_update_user_information( persistent_store.update_credential(&credential_id, user) } -/// Checks the PIN protocol. -/// -/// TODO(#246) refactor after #246 is merged -fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option) -> Result<(), Ctap2StatusCode> { - match pin_uv_auth_protocol { - Some(1) => Ok(()), - _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - } -} - /// Processes the CredentialManagement command and all its subcommands. pub fn process_credential_management( persistent_store: &mut PersistentStore, @@ -297,7 +290,7 @@ pub fn process_credential_management( | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::UpdateUserInformation => { - pin_uv_auth_protocol_check(pin_protocol)?; + check_pin_uv_auth_protocol(pin_protocol)?; persistent_store .pin_hash()? .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; From 7268a9474b09fb2aa1c710e1b2769bcd9ab8044f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 18 Dec 2020 12:04:05 +0100 Subject: [PATCH 25/86] renames residential to resident --- src/ctap/mod.rs | 10 +++++----- src/ctap/storage.rs | 38 +++++++++++++++++--------------------- src/ctap/storage/key.rs | 8 ++++---- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d98f09d..fc8324c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1195,7 +1195,7 @@ mod test { } #[test] - fn test_residential_process_make_credential() { + fn test_resident_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1214,7 +1214,7 @@ mod test { } #[test] - fn test_non_residential_process_make_credential() { + fn test_non_resident_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1526,7 +1526,7 @@ mod test { } #[test] - fn test_residential_process_get_assertion() { + fn test_resident_process_get_assertion() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1629,7 +1629,7 @@ mod test { } #[test] - fn test_residential_process_get_assertion_hmac_secret() { + fn test_resident_process_get_assertion_hmac_secret() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); @@ -1681,7 +1681,7 @@ mod test { } #[test] - fn test_residential_process_get_assertion_with_cred_protect() { + fn test_resident_process_get_assertion_with_cred_protect() { let mut rng = ThreadRng256 {}; let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let credential_id = rng.gen_uniform_u8x32().to_vec(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index cf9afbc..022a3c5 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -38,11 +38,11 @@ use crypto::rng256::Rng256; // number of pages. This may improve in the future. Currently, using 20 pages gives between 20ms and // 240ms per operation. The rule of thumb is between 1ms and 12ms per additional page. // -// Limiting the number of residential keys permits to ensure a minimum number of counter increments. +// Limiting the number of resident keys permits to ensure a minimum number of counter increments. // Let: // - P the number of pages (NUM_PAGES) -// - K the maximum number of residential keys (MAX_SUPPORTED_RESIDENTIAL_KEYS) -// - S the maximum size of a residential key (about 500) +// - K the maximum number of resident keys (MAX_SUPPORTED_RESIDENT_KEYS) +// - S the maximum size of a resident key (about 500) // - C the number of erase cycles (10000) // - I the minimum number of counter increments // @@ -50,7 +50,7 @@ use crypto::rng256::Rng256; // // With P=20 and K=150, we have I=2M which is enough for 500 increments per day for 10 years. const NUM_PAGES: usize = 20; -const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; +const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; @@ -132,7 +132,7 @@ impl PersistentStore { /// Returns `CTAP2_ERR_VENDOR_INTERNAL_ERROR` if the key does not hold a valid credential. pub fn get_credential(&self, key: usize) -> Result { let min_key = key::CREDENTIALS.start; - if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENTIAL_KEYS { + if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENT_KEYS { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } let credential_entry = self @@ -200,13 +200,11 @@ impl PersistentStore { let mut old_key = None; let min_key = key::CREDENTIALS.start; // Holds whether a key is used (indices are shifted by min_key). - let mut keys = vec![false; MAX_SUPPORTED_RESIDENTIAL_KEYS]; + let mut keys = vec![false; MAX_SUPPORTED_RESIDENT_KEYS]; let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; for (key, credential) in iter { - if key < min_key - || key - min_key >= MAX_SUPPORTED_RESIDENTIAL_KEYS - || keys[key - min_key] + if key < min_key || key - min_key >= MAX_SUPPORTED_RESIDENT_KEYS || keys[key - min_key] { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } @@ -221,16 +219,14 @@ impl PersistentStore { } } iter_result?; - if old_key.is_none() - && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENTIAL_KEYS - { + if old_key.is_none() && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENT_KEYS { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } let key = match old_key { // This is a new credential being added, we need to allocate a free key. We choose the // first available key. None => key::CREDENTIALS - .take(MAX_SUPPORTED_RESIDENTIAL_KEYS) + .take(MAX_SUPPORTED_RESIDENT_KEYS) .find(|key| !keys[key - min_key]) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, // This is an existing credential being updated, we reuse its key. @@ -280,7 +276,7 @@ impl PersistentStore { /// Returns the estimated number of credentials that can still be stored. pub fn remaining_credentials(&self) -> Result { - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS .checked_sub(self.count_credentials()?) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } @@ -714,7 +710,7 @@ mod test { assert_eq!(persistent_store.count_credentials().unwrap(), 0); let mut credential_ids = vec![]; - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { let user_handle = i.to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); credential_ids.push(credential_source.credential_id.clone()); @@ -788,7 +784,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { let user_handle = i.to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); @@ -797,7 +793,7 @@ mod test { let credential_source = create_credential_source( &mut rng, "example.com", - vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], + vec![MAX_SUPPORTED_RESIDENT_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), @@ -805,7 +801,7 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } @@ -837,7 +833,7 @@ mod test { .is_some()); let mut persistent_store = PersistentStore::new(&mut rng); - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { let user_handle = i.to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); @@ -846,7 +842,7 @@ mod test { let credential_source = create_credential_source( &mut rng, "example.com", - vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], + vec![MAX_SUPPORTED_RESIDENT_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), @@ -854,7 +850,7 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index dfe44fc..c6e46e2 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -84,8 +84,8 @@ make_partition! { /// The credentials. /// - /// Depending on `MAX_SUPPORTED_RESIDENTIAL_KEYS`, only a prefix of those keys is used. Each - /// board may configure `MAX_SUPPORTED_RESIDENTIAL_KEYS` depending on the storage size. + /// Depending on `MAX_SUPPORTED_RESIDENT_KEYS`, only a prefix of those keys is used. Each + /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; /// The secret of the CredRandom feature. @@ -127,8 +127,8 @@ mod test { #[test] fn enough_credentials() { - use super::super::MAX_SUPPORTED_RESIDENTIAL_KEYS; - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS <= CREDENTIALS.end - CREDENTIALS.start); + use super::super::MAX_SUPPORTED_RESIDENT_KEYS; + assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start); } #[test] From 69bdd8c615934be5b4526950cb4d02099058597d Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 14 Jan 2021 18:05:38 +0100 Subject: [PATCH 26/86] renames to resident in README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 48f6c6e..0691fb8 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,7 @@ a few things you can personalize: check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for documentation. 1. Depending on your available flash storage, choose an appropriate maximum - number of supported residential keys and number of pages in - `ctap/storage.rs`. + number of supported resident keys and number of pages in `ctap/storage.rs`. 1. Change the default level for the credProtect extension in `ctap/mod.rs`. When changing the default, resident credentials become undiscoverable without user verification. This helps privacy, but can make usage less comfortable From 3702b61ce7c19dde35a5d9b74e2f9a61b3b0c972 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 15 Jan 2021 17:41:16 +0100 Subject: [PATCH 27/86] implements Default for Response type --- src/ctap/credential_management.rs | 25 +++---------------------- src/ctap/response.rs | 15 ++------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index c83c955..71c1e39 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -49,17 +49,10 @@ fn enumerate_rps_response( let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); Ok(AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, rp, rp_id_hash, total_rps, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, + ..Default::default() }) } @@ -93,11 +86,6 @@ fn enumerate_credentials_response( }; let public_key = CoseKey::from(private_key.genpk()); Ok(AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, - rp: None, - rp_id_hash: None, - total_rps: None, user: Some(user), credential_id: Some(credential_id), public_key: Some(public_key), @@ -105,6 +93,7 @@ fn enumerate_credentials_response( cred_protect: cred_protect_policy, // TODO(kaczmarczyck) add when largeBlobKey is implemented large_blob_key: None, + ..Default::default() }) } @@ -117,15 +106,7 @@ fn process_get_creds_metadata( max_possible_remaining_resident_credentials_count: Some( persistent_store.remaining_credentials()? as u64, ), - rp: None, - rp_id_hash: None, - total_rps: None, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, + ..Default::default() }) } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 03bc70d..e4cda5e 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -204,6 +204,7 @@ impl From for cbor::Value { } } +#[derive(Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorCredentialManagementResponse { @@ -435,19 +436,7 @@ mod test { #[test] fn test_used_credential_management_into_cbor() { - let cred_management_response = AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, - rp: None, - rp_id_hash: None, - total_rps: None, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, - }; + let cred_management_response = AuthenticatorCredentialManagementResponse::default(); let response_cbor: Option = ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); let expected_cbor = cbor_map_options! {}; From 55038cc084bedb493cd7d3a901a0c7b7ff22199f Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 18 Jan 2021 16:13:01 +0100 Subject: [PATCH 28/86] Add bound-test in addition to equality-test --- libraries/persistent_store/src/format.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 8de88e4..9b5631b 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -1080,9 +1080,12 @@ mod tests { #[test] fn position_offsets_fit_in_a_halfword() { - // The store stores the entry positions as their offset from the head. Those offsets are - // represented as u16. The bound below is a large over-approximation of the maximal offset - // but it already fits. - assert_eq!((MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE, 0xff80); + // The store stores in RAM the entry positions as their offset from the head. Those offsets + // are represented as u16. The bound below is a large over-approximation of the maximal + // offset. We first make sure it fits in a u16. + const MAX_POS: Nat = (MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE; + assert!(MAX_POS <= u16::MAX as Nat); + // We also check the actual value for up-to-date documentation, since it's a constant. + assert_eq!(MAX_POS, 0xff80); } } From a712d1476b373ed6295115d464f8bc357aba68f5 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 16 Dec 2020 19:40:55 +0100 Subject: [PATCH 29/86] Return error instead of debug assert With dirty storage we hit the assert. Returning an error permits to continue to catch if the invariant is broken for normal operation while being able to continue fuzzing with dirty storage. --- libraries/persistent_store/src/format.rs | 56 +++++++++---------- .../persistent_store/src/format/bitfield.rs | 29 ++++++---- libraries/persistent_store/src/store.rs | 36 +++++++----- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 9b5631b..f575750 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -335,12 +335,12 @@ impl Format { } /// Builds the storage representation of an init info. - pub fn build_init(&self, init: InitInfo) -> WordSlice { + pub fn build_init(&self, init: InitInfo) -> StoreResult { let mut word = ERASED_WORD; - INIT_CYCLE.set(&mut word, init.cycle); - INIT_PREFIX.set(&mut word, init.prefix); - WORD_CHECKSUM.set(&mut word, 0); - word.as_slice() + INIT_CYCLE.set(&mut word, init.cycle)?; + INIT_PREFIX.set(&mut word, init.prefix)?; + WORD_CHECKSUM.set(&mut word, 0)?; + Ok(word.as_slice()) } /// Returns the storage index of the compact info of a page. @@ -368,36 +368,36 @@ impl Format { } /// Builds the storage representation of a compact info. - pub fn build_compact(&self, compact: CompactInfo) -> WordSlice { + pub fn build_compact(&self, compact: CompactInfo) -> StoreResult { let mut word = ERASED_WORD; - COMPACT_TAIL.set(&mut word, compact.tail); - WORD_CHECKSUM.set(&mut word, 0); - word.as_slice() + COMPACT_TAIL.set(&mut word, compact.tail)?; + WORD_CHECKSUM.set(&mut word, 0)?; + Ok(word.as_slice()) } /// Builds the storage representation of an internal entry. - pub fn build_internal(&self, internal: InternalEntry) -> WordSlice { + pub fn build_internal(&self, internal: InternalEntry) -> StoreResult { let mut word = ERASED_WORD; match internal { InternalEntry::Erase { page } => { - ID_ERASE.set(&mut word); - ERASE_PAGE.set(&mut word, page); + ID_ERASE.set(&mut word)?; + ERASE_PAGE.set(&mut word, page)?; } InternalEntry::Clear { min_key } => { - ID_CLEAR.set(&mut word); - CLEAR_MIN_KEY.set(&mut word, min_key); + ID_CLEAR.set(&mut word)?; + CLEAR_MIN_KEY.set(&mut word, min_key)?; } InternalEntry::Marker { count } => { - ID_MARKER.set(&mut word); - MARKER_COUNT.set(&mut word, count); + ID_MARKER.set(&mut word)?; + MARKER_COUNT.set(&mut word, count)?; } InternalEntry::Remove { key } => { - ID_REMOVE.set(&mut word); - REMOVE_KEY.set(&mut word, key); + ID_REMOVE.set(&mut word)?; + REMOVE_KEY.set(&mut word, key)?; } } - WORD_CHECKSUM.set(&mut word, 0); - word.as_slice() + WORD_CHECKSUM.set(&mut word, 0)?; + Ok(word.as_slice()) } /// Parses the first word of an entry from its storage representation. @@ -459,31 +459,31 @@ impl Format { } /// Builds the storage representation of a user entry. - pub fn build_user(&self, key: Nat, value: &[u8]) -> Vec { + pub fn build_user(&self, key: Nat, value: &[u8]) -> StoreResult> { let length = usize_to_nat(value.len()); let word_size = self.word_size(); let footer = self.bytes_to_words(length); let mut result = vec![0xff; ((1 + footer) * word_size) as usize]; result[word_size as usize..][..length as usize].copy_from_slice(value); let mut word = ERASED_WORD; - ID_HEADER.set(&mut word); + ID_HEADER.set(&mut word)?; if footer > 0 && is_erased(&result[(footer * word_size) as usize..]) { HEADER_FLIPPED.set(&mut word); *result.last_mut().unwrap() = 0x7f; } - HEADER_LENGTH.set(&mut word, length); - HEADER_KEY.set(&mut word, key); + HEADER_LENGTH.set(&mut word, length)?; + HEADER_KEY.set(&mut word, key)?; HEADER_CHECKSUM.set( &mut word, count_zeros(&result[(footer * word_size) as usize..]), - ); + )?; result[..word_size as usize].copy_from_slice(&word.as_slice()); - result + Ok(result) } /// Sets the padding bit in the first word of a user entry. - pub fn set_padding(&self, word: &mut Word) { - ID_PADDING.set(word); + pub fn set_padding(&self, word: &mut Word) -> StoreResult<()> { + ID_PADDING.set(word) } /// Sets the deleted bit in the first word of a user entry. diff --git a/libraries/persistent_store/src/format/bitfield.rs b/libraries/persistent_store/src/format/bitfield.rs index 2cffc4b..32c0ae5 100644 --- a/libraries/persistent_store/src/format/bitfield.rs +++ b/libraries/persistent_store/src/format/bitfield.rs @@ -42,15 +42,20 @@ impl Field { /// Sets the value of a bit field. /// - /// # Preconditions + /// # Errors /// /// - The value must fit in the bit field: `num_bits(value) < self.len`. /// - The value must only change bits from 1 to 0: `self.get(*word) & value == value`. - pub fn set(&self, word: &mut Word, value: Nat) { - debug_assert_eq!(value & self.mask(), value); + pub fn set(&self, word: &mut Word, value: Nat) -> StoreResult<()> { + if value & self.mask() != value { + return Err(StoreError::InvalidStorage); + } let mask = !(self.mask() << self.pos); word.0 &= mask | (value << self.pos); - debug_assert_eq!(self.get(*word), value); + if self.get(*word) != value { + return Err(StoreError::InvalidStorage); + } + Ok(()) } /// Returns a bit mask the length of the bit field. @@ -82,8 +87,8 @@ impl ConstField { } /// Sets the bit field to its value. - pub fn set(&self, word: &mut Word) { - self.field.set(word, self.value); + pub fn set(&self, word: &mut Word) -> StoreResult<()> { + self.field.set(word, self.value) } } @@ -135,15 +140,15 @@ impl Checksum { /// Sets the checksum to the external increment value. /// - /// # Preconditions + /// # Errors /// /// - The bits of the checksum bit field should be set to one: `self.field.get(*word) == /// self.field.mask()`. /// - The checksum value should fit in the checksum bit field: `num_bits(word.count_zeros() + /// value) < self.field.len`. - pub fn set(&self, word: &mut Word, value: Nat) { + pub fn set(&self, word: &mut Word, value: Nat) -> StoreResult<()> { debug_assert_eq!(self.field.get(*word), self.field.mask()); - self.field.set(word, word.0.count_zeros() + value); + self.field.set(word, word.0.count_zeros() + value) } } @@ -290,7 +295,7 @@ mod tests { assert_eq!(field.get(Word(0x000000f8)), 0x1f); assert_eq!(field.get(Word(0x0000ff37)), 6); let mut word = Word(0xffffffff); - field.set(&mut word, 3); + field.set(&mut word, 3).unwrap(); assert_eq!(word, Word(0xffffff1f)); } @@ -305,7 +310,7 @@ mod tests { assert!(field.check(Word(0x00000048))); assert!(field.check(Word(0x0000ff4f))); let mut word = Word(0xffffffff); - field.set(&mut word); + field.set(&mut word).unwrap(); assert_eq!(word, Word(0xffffff4f)); } @@ -333,7 +338,7 @@ mod tests { assert_eq!(field.get(Word(0x00ffff67)), Ok(4)); assert_eq!(field.get(Word(0x7fffff07)), Err(StoreError::InvalidStorage)); let mut word = Word(0x0fffffff); - field.set(&mut word, 4); + field.set(&mut word, 4).unwrap(); assert_eq!(word, Word(0x0fffff47)); } diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index bc4258a..f707a89 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -300,7 +300,9 @@ impl Store { self.reserve(self.format.transaction_capacity(updates))?; // Write the marker entry. let marker = self.tail()?; - let entry = self.format.build_internal(InternalEntry::Marker { count }); + let entry = self + .format + .build_internal(InternalEntry::Marker { count })?; self.write_slice(marker, &entry)?; self.init_page(marker, marker)?; // Write the updates. @@ -308,7 +310,7 @@ impl Store { for update in updates { let length = match *update { StoreUpdate::Insert { key, ref value } => { - let entry = self.format.build_user(usize_to_nat(key), value); + let entry = self.format.build_user(usize_to_nat(key), value)?; let word_size = self.format.word_size(); let footer = usize_to_nat(entry.len()) / word_size - 1; self.write_slice(tail, &entry[..(footer * word_size) as usize])?; @@ -317,7 +319,7 @@ impl Store { } StoreUpdate::Remove { key } => { let key = usize_to_nat(key); - let remove = self.format.build_internal(InternalEntry::Remove { key }); + let remove = self.format.build_internal(InternalEntry::Remove { key })?; self.write_slice(tail, &remove)?; 0 } @@ -337,7 +339,9 @@ impl Store { if min_key > self.format.max_key() { return Err(StoreError::InvalidArgument); } - let clear = self.format.build_internal(InternalEntry::Clear { min_key }); + let clear = self + .format + .build_internal(InternalEntry::Clear { min_key })?; // We always have one word available. We can't use `reserve` because this is internal // capacity, not user capacity. while self.immediate_capacity()? < 1 { @@ -403,7 +407,7 @@ impl Store { if key > self.format.max_key() || value_len > self.format.max_value_len() { return Err(StoreError::InvalidArgument); } - let entry = self.format.build_user(key, value); + let entry = self.format.build_user(key, value)?; let entry_len = usize_to_nat(entry.len()); self.reserve(entry_len / self.format.word_size())?; let tail = self.tail()?; @@ -469,7 +473,7 @@ impl Store { let init_info = self.format.build_init(InitInfo { cycle: 0, prefix: 0, - }); + })?; self.storage_write_slice(index, &init_info) } @@ -681,7 +685,9 @@ impl Store { } let tail = max(self.tail()?, head.next_page(&self.format)); let index = self.format.index_compact(head.page(&self.format)); - let compact_info = self.format.build_compact(CompactInfo { tail: tail - head }); + let compact_info = self + .format + .build_compact(CompactInfo { tail: tail - head })?; self.storage_write_slice(index, &compact_info)?; self.compact_copy() } @@ -721,7 +727,7 @@ impl Store { self.init_page(tail, tail + (length - 1))?; tail += length; } - let erase = self.format.build_internal(InternalEntry::Erase { page }); + let erase = self.format.build_internal(InternalEntry::Erase { page })?; self.write_slice(tail, &erase)?; self.init_page(tail, tail)?; self.compact_erase(tail) @@ -851,7 +857,7 @@ impl Store { let init_info = self.format.build_init(InitInfo { cycle: new_first.cycle(&self.format), prefix: new_first.word(&self.format), - }); + })?; self.storage_write_slice(index, &init_info)?; Ok(()) } @@ -859,7 +865,7 @@ impl Store { /// Sets the padding bit of a user header. fn set_padding(&mut self, pos: Position) -> StoreResult<()> { let mut word = Word::from_slice(self.read_word(pos)); - self.format.set_padding(&mut word); + self.format.set_padding(&mut word)?; self.write_slice(pos, &word.as_slice())?; Ok(()) } @@ -1195,10 +1201,12 @@ impl Store { let format = Format::new(storage).unwrap(); // Write the init info of the first page. let mut index = format.index_init(0); - let init_info = format.build_init(InitInfo { - cycle: usize_to_nat(cycle), - prefix: 0, - }); + let init_info = format + .build_init(InitInfo { + cycle: usize_to_nat(cycle), + prefix: 0, + }) + .unwrap(); storage.write_slice(index, &init_info).unwrap(); // Pad the first word of the page. This makes the store looks used, otherwise we may confuse // it with a partially initialized store. From e3353cb232e2892de0df43e6a04a8d4a2d376b6c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 19 Jan 2021 12:42:41 +0100 Subject: [PATCH 30/86] only stores the RP ID index as state --- src/ctap/credential_management.rs | 128 ++++++++++++++++++++++++++---- src/ctap/mod.rs | 2 +- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 71c1e39..dba7d36 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -31,11 +31,23 @@ use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use core::iter::FromIterator; use crypto::sha256::Sha256; use crypto::Hash256; use libtock_drivers::timer::ClockValue; +/// Generates a set with all existing RP IDs. +fn get_stored_rp_ids( + persistent_store: &PersistentStore, +) -> Result, Ctap2StatusCode> { + let mut rp_set = BTreeSet::new(); + let mut iter_result = Ok(()); + for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { + rp_set.insert(credential.rp_id); + } + iter_result?; + Ok(rp_set) +} + /// Generates the response for subcommands enumerating RPs. fn enumerate_rps_response( rp_id: Option, @@ -117,34 +129,35 @@ fn process_enumerate_rps_begin( stateful_command_type: &mut Option, now: ClockValue, ) -> Result { - let mut rp_set = BTreeSet::new(); - let mut iter_result = Ok(()); - for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { - rp_set.insert(credential.rp_id); - } - iter_result?; - let mut rp_ids = Vec::from_iter(rp_set); - let total_rps = rp_ids.len(); + let rp_set = get_stored_rp_ids(persistent_store)?; + let total_rps = rp_set.len(); - // TODO(kaczmarczyck) behaviour with empty list? - let rp_id = rp_ids.pop(); + // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { *stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids)); + *stateful_command_type = Some(StatefulCommand::EnumerateRps(1)); } - enumerate_rps_response(rp_id, Some(total_rps as u64)) + // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() + enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) } /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. fn process_enumerate_rps_get_next_rp( + persistent_store: &PersistentStore, stateful_command_permission: &mut TimedPermission, stateful_command_type: &mut Option, now: ClockValue, ) -> Result { check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type { - let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = stateful_command_type { + let rp_set = get_stored_rp_ids(persistent_store)?; + // A BTreeSet is already sorted. + let rp_id = rp_set + .into_iter() + .nth(*rp_id_index) + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + *stateful_command_type = Some(StatefulCommand::EnumerateRps(*rp_id_index + 1)); enumerate_rps_response(Some(rp_id), None) } else { Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -305,6 +318,7 @@ pub fn process_credential_management( )?), CredentialManagementSubCommand::EnumerateRpsGetNextRp => { Some(process_enumerate_rps_get_next_rp( + persistent_store, stateful_command_permission, stateful_command_type, now, @@ -544,6 +558,90 @@ mod test { ); } + #[test] + fn test_process_enumerate_rps_completeness() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source = create_credential_source(&mut rng); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + const NUM_CREDENTIALS: usize = 20; + for i in 0..NUM_CREDENTIALS { + let mut credential = credential_source.clone(); + credential.rp_id = i.to_string(); + ctap_state + .persistent_store + .store_credential(credential) + .unwrap(); + } + + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + let pin_auth = Some(vec![ + 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, + 0xD0, 0xD1, + ]); + + let mut rp_set = BTreeSet::new(); + // This mut is just to make the test code shorter. + // The command is different on the first loop iteration. + let mut cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + + for _ in 0..NUM_CREDENTIALS { + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + if rp_set.is_empty() { + assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64)); + } else { + assert_eq!(response.total_rps, None); + } + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + assert!(!rp_set.contains(&rp_id)); + rp_set.insert(rp_id); + } + _ => panic!("Invalid response type"), + }; + cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + } + + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_enumerate_credentials_with_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 96dbd41..49d14f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -165,7 +165,7 @@ pub struct AssertionState { pub enum StatefulCommand { Reset, GetAssertion(AssertionState), - EnumerateRps(Vec), + EnumerateRps(usize), EnumerateCredentials(Vec), } From 134c880212cb91f400052c33c28be38eac4e3ab8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 19 Jan 2021 15:07:15 +0100 Subject: [PATCH 31/86] reworks command state to its own struct --- src/ctap/credential_management.rs | 85 +++++------------- src/ctap/mod.rs | 145 ++++++++++++++++++------------ 2 files changed, 111 insertions(+), 119 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index dba7d36..2dbe961 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -22,11 +22,7 @@ use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::timed_permission::TimedPermission; -use super::{ - check_command_permission, check_pin_uv_auth_protocol, StatefulCommand, - STATEFUL_COMMAND_TIMEOUT_DURATION, -}; +use super::{check_pin_uv_auth_protocol, StatefulCommand, StatefulPermission}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -125,8 +121,7 @@ fn process_get_creds_metadata( /// Processes the subcommand enumerateRPsBegin for CredentialManagement. fn process_enumerate_rps_begin( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, now: ClockValue, ) -> Result { let rp_set = get_stored_rp_ids(persistent_store)?; @@ -134,9 +129,7 @@ fn process_enumerate_rps_begin( // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { - *stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateRps(1)); + stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1)); } // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) @@ -145,19 +138,16 @@ fn process_enumerate_rps_begin( /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. fn process_enumerate_rps_get_next_rp( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, - now: ClockValue, + stateful_command_permission: &mut StatefulPermission, ) -> Result { - check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = stateful_command_type { + if let StatefulCommand::EnumerateRps(rp_id_index) = stateful_command_permission.get_command()? { let rp_set = get_stored_rp_ids(persistent_store)?; // A BTreeSet is already sorted. let rp_id = rp_set .into_iter() .nth(*rp_id_index) .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - *stateful_command_type = Some(StatefulCommand::EnumerateRps(*rp_id_index + 1)); + *rp_id_index += 1; enumerate_rps_response(Some(rp_id), None) } else { Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -167,8 +157,7 @@ fn process_enumerate_rps_get_next_rp( /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { @@ -194,9 +183,8 @@ fn process_enumerate_credentials_begin( .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; let credential = persistent_store.get_credential(current_key)?; if total_credentials > 1 { - *stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials)); + stateful_command_permission + .set_command(now, StatefulCommand::EnumerateCredentials(rp_credentials)); } enumerate_credentials_response(credential, Some(total_credentials as u64)) } @@ -204,12 +192,10 @@ fn process_enumerate_credentials_begin( /// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement. fn process_enumerate_credentials_get_next_credential( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - mut stateful_command_type: &mut Option, - now: ClockValue, + stateful_command_permission: &mut StatefulPermission, ) -> Result { - check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type + if let StatefulCommand::EnumerateCredentials(rp_credentials) = + stateful_command_permission.get_command()? { let current_key = rp_credentials .pop() @@ -251,8 +237,7 @@ fn process_update_user_information( /// Processes the CredentialManagement command and all its subcommands. pub fn process_credential_management( persistent_store: &mut PersistentStore, - stateful_command_permission: &mut TimedPermission, - mut stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, pin_protocol_v1: &mut PinProtocolV1, cred_management_params: AuthenticatorCredentialManagementParameters, now: ClockValue, @@ -264,17 +249,17 @@ pub fn process_credential_management( pin_auth, } = cred_management_params; - match (sub_command, &mut stateful_command_type) { + match (sub_command, stateful_command_permission.get_command()) { ( CredentialManagementSubCommand::EnumerateRpsGetNextRp, - Some(StatefulCommand::EnumerateRps(_)), - ) => (), - ( + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, - Some(StatefulCommand::EnumerateCredentials(_)), - ) => (), + Ok(StatefulCommand::EnumerateCredentials(_)), + ) => stateful_command_permission.check_command_permission(now)?, (_, _) => { - *stateful_command_type = None; + stateful_command_permission.clear(); } } @@ -313,22 +298,15 @@ pub fn process_credential_management( CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( persistent_store, stateful_command_permission, - stateful_command_type, now, )?), - CredentialManagementSubCommand::EnumerateRpsGetNextRp => { - Some(process_enumerate_rps_get_next_rp( - persistent_store, - stateful_command_permission, - stateful_command_type, - now, - )?) - } + CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some( + process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?, + ), CredentialManagementSubCommand::EnumerateCredentialsBegin => { Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, - stateful_command_type, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -337,8 +315,6 @@ pub fn process_credential_management( Some(process_enumerate_credentials_get_next_credential( persistent_store, stateful_command_permission, - stateful_command_type, - now, )?) } CredentialManagementSubCommand::DeleteCredential => { @@ -412,7 +388,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -441,7 +416,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -496,7 +470,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -521,7 +494,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -547,7 +519,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -600,7 +571,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -631,7 +601,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -690,7 +659,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -714,7 +682,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -739,7 +706,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -793,7 +759,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -812,7 +777,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -872,7 +836,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -922,7 +885,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -950,7 +912,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 49d14f4..46281fa 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -101,8 +101,9 @@ const ED_FLAG: u8 = 0x80; pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); +// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); -pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -169,15 +170,50 @@ pub enum StatefulCommand { EnumerateCredentials(Vec), } -pub fn check_command_permission( - stateful_command_permission: &mut TimedPermission, - now: ClockValue, -) -> Result<(), Ctap2StatusCode> { - *stateful_command_permission = stateful_command_permission.check_expiration(now); - if stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) +pub struct StatefulPermission { + permission: TimedPermission, + command_type: Option, +} + +impl StatefulPermission { + // Resets are only possible in the first 10 seconds after booting. + // Therefore, initialization includes allowing Reset. + pub fn new_reset(now: ClockValue) -> StatefulPermission { + StatefulPermission { + permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + command_type: Some(StatefulCommand::Reset), + } + } + + pub fn clear(&mut self) { + self.permission = TimedPermission::waiting(); + self.command_type = None; + } + + pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + if self.permission.is_granted(now) { + Ok(()) + } else { + self.clear(); + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + pub fn get_command(&mut self) -> Result<&mut StatefulCommand, Ctap2StatusCode> { + self.command_type + .as_mut() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + + pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { + match &new_command_type { + // Reset is only allowed after a power cycle. + StatefulCommand::Reset => unreachable!(), + _ => { + self.permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.command_type = Some(new_command_type); + } + } } } @@ -194,8 +230,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. - stateful_command_permission: TimedPermission, - stateful_command_type: Option, + stateful_command_permission: StatefulPermission, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -220,13 +255,15 @@ where U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), - stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), - stateful_command_type: Some(StatefulCommand::Reset), + stateful_command_permission: StatefulPermission::new_reset(now), } } pub fn update_command_permission(&mut self, now: ClockValue) { - self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + // Ignore the result, just update. + let _ = self + .stateful_command_permission + .check_command_permission(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { @@ -345,27 +382,23 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } - match (&command, &self.stateful_command_type) { - ( - Command::AuthenticatorGetNextAssertion, - Some(StatefulCommand::GetAssertion(_)), - ) => (), - ( + match (&command, self.stateful_command_permission.get_command()) { + (Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_))) + | (Command::AuthenticatorReset, Ok(StatefulCommand::Reset)) + // AuthenticatorGetInfo still allows Reset. + | (Command::AuthenticatorGetInfo, Ok(StatefulCommand::Reset)) + // AuthenticatorSelection still allows Reset. + | (Command::AuthenticatorSelection, Ok(StatefulCommand::Reset)) + // AuthenticatorCredentialManagement handles its subcommands later. + | ( Command::AuthenticatorCredentialManagement(_), - Some(StatefulCommand::EnumerateRps(_)), - ) => (), - ( + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( Command::AuthenticatorCredentialManagement(_), - Some(StatefulCommand::EnumerateCredentials(_)), + Ok(StatefulCommand::EnumerateCredentials(_)), ) => (), - (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), - // GetInfo does not reset stateful commands. - (Command::AuthenticatorGetInfo, _) => (), - // AuthenticatorSelection does not reset stateful commands. - (Command::AuthenticatorSelection, _) => (), - (_, _) => { - self.stateful_command_type = None; - } + (_, _) => self.stateful_command_permission.clear(), } let response = match command { Command::AuthenticatorMakeCredential(params) => { @@ -382,7 +415,6 @@ where process_credential_management( &mut self.persistent_store, &mut self.stateful_command_permission, - &mut self.stateful_command_type, &mut self.pin_protocol_v1, params, now, @@ -855,12 +887,12 @@ where None } else { let number_of_credentials = Some(next_credential_keys.len() + 1); - self.stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + let assertion_state = StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), next_credential_keys, - })); + }); + self.stateful_command_permission + .set_command(now, assertion_state); number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) @@ -870,20 +902,20 @@ where &mut self, now: ClockValue, ) -> Result { - check_command_permission(&mut self.stateful_command_permission, now)?; - let (assertion_input, credential) = - if let Some(StatefulCommand::GetAssertion(assertion_state)) = - &mut self.stateful_command_type - { - let credential_key = assertion_state - .next_credential_keys - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self.persistent_store.get_credential(credential_key)?; - (assertion_state.assertion_input.clone(), credential) - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); - }; + self.stateful_command_permission + .check_command_permission(now)?; + let (assertion_input, credential) = if let StatefulCommand::GetAssertion(assertion_state) = + self.stateful_command_permission.get_command()? + { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = self.persistent_store.get_credential(credential_key)?; + (assertion_state.assertion_input.clone(), credential) + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + }; self.assertion_response(credential, assertion_input, None) } @@ -949,11 +981,10 @@ where cid: ChannelID, now: ClockValue, ) -> Result { - // Resets are only possible in the first 10 seconds after booting. - // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? - check_command_permission(&mut self.stateful_command_permission, now)?; - match &self.stateful_command_type { - Some(StatefulCommand::Reset) => (), + self.stateful_command_permission + .check_command_permission(now)?; + match self.stateful_command_permission.get_command()? { + StatefulCommand::Reset => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; From 9296f51e19daa839fe7bf3167ea0b2b6b7aafcd1 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 12:08:07 +0100 Subject: [PATCH 32/86] stricter API for StatefulCommandPermission --- src/ctap/credential_management.rs | 34 ++++--------- src/ctap/mod.rs | 85 ++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 2dbe961..25fe3b9 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -140,18 +140,14 @@ fn process_enumerate_rps_get_next_rp( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, ) -> Result { - if let StatefulCommand::EnumerateRps(rp_id_index) = stateful_command_permission.get_command()? { - let rp_set = get_stored_rp_ids(persistent_store)?; - // A BTreeSet is already sorted. - let rp_id = rp_set - .into_iter() - .nth(*rp_id_index) - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - *rp_id_index += 1; - enumerate_rps_response(Some(rp_id), None) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + let rp_id_index = stateful_command_permission.next_enumerate_rp()?; + let rp_set = get_stored_rp_ids(persistent_store)?; + // A BTreeSet is already sorted. + let rp_id = rp_set + .into_iter() + .nth(rp_id_index) + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + enumerate_rps_response(Some(rp_id), None) } /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. @@ -194,17 +190,9 @@ fn process_enumerate_credentials_get_next_credential( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, ) -> Result { - if let StatefulCommand::EnumerateCredentials(rp_credentials) = - stateful_command_permission.get_command()? - { - let current_key = rp_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = persistent_store.get_credential(current_key)?; - enumerate_credentials_response(credential, None) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + let credential_key = stateful_command_permission.next_enumerate_credential()?; + let credential = persistent_store.get_credential(credential_key)?; + enumerate_credentials_response(credential, None) } /// Processes the subcommand deleteCredential for CredentialManagement. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 46281fa..96d7b25 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -149,20 +149,23 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +/// Holds data necessary to sign an assertion for a credential. #[derive(Clone)] -struct AssertionInput { +pub struct AssertionInput { client_data_hash: Vec, auth_data: Vec, hmac_secret_input: Option, has_uv: bool, } +/// Contains the state we need to store for GetNextAssertion. pub struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. next_credential_keys: Vec, } +/// Stores which command currently holds state for subsequent calls. pub enum StatefulCommand { Reset, GetAssertion(AssertionState), @@ -170,14 +173,25 @@ pub enum StatefulCommand { EnumerateCredentials(Vec), } +/// Stores the current CTAP command state and when it times out. +/// +/// Some commands are executed in a series of calls to the authenticator. +/// Interleaving calls to other commands interrupt the current command and +/// remove all state and permissions. Power cycling allows the Reset command, +/// and to prevent misuse or accidents, we disallow Reset after receiving +/// different commands. Therefore, Reset behaves just like all other stateful +/// commands and is included here. Please not that the allowed time for Reset +/// differs from all other stateful commands. pub struct StatefulPermission { permission: TimedPermission, command_type: Option, } impl StatefulPermission { - // Resets are only possible in the first 10 seconds after booting. - // Therefore, initialization includes allowing Reset. + /// Creates the command state at device startup. + /// + /// Resets are only possible after a power cycle. Therefore, initialization + /// means allowing Reset, and Reset cannot be granted later. pub fn new_reset(now: ClockValue) -> StatefulPermission { StatefulPermission { permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), @@ -185,11 +199,13 @@ impl StatefulPermission { } } + /// Clears all permissions and state. pub fn clear(&mut self) { self.permission = TimedPermission::waiting(); self.command_type = None; } + /// Checks the permission timeout. pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { if self.permission.is_granted(now) { Ok(()) @@ -199,12 +215,14 @@ impl StatefulPermission { } } - pub fn get_command(&mut self) -> Result<&mut StatefulCommand, Ctap2StatusCode> { + /// Gets a reference to the current command state, if any exists. + pub fn get_command(&self) -> Result<&StatefulCommand, Ctap2StatusCode> { self.command_type - .as_mut() + .as_ref() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } + /// Sets a new command state, and starts a new clock for timeouts. pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { match &new_command_type { // Reset is only allowed after a power cycle. @@ -215,6 +233,47 @@ impl StatefulPermission { } } } + + /// Returns the state for the next assertion and advances it. + /// + /// The state includes all information from GetAssertion and the storage key + /// to the next credential that needs to be processed. + pub fn next_assertion_credential( + &mut self, + ) -> Result<(AssertionInput, usize), Ctap2StatusCode> { + if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.command_type { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + Ok((assertion_state.assertion_input.clone(), credential_key)) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the index to the next RP ID for enumeration and advances it. + pub fn next_enumerate_rp(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = &mut self.command_type { + let current_index = *rp_id_index; + *rp_id_index += 1; + Ok(current_index) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the next storage credential key for enumeration and advances it. + pub fn next_enumerate_credential(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut self.command_type + { + rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } } // This struct currently holds all state, not only the persistent memory. The persistent members are @@ -904,18 +963,10 @@ where ) -> Result { self.stateful_command_permission .check_command_permission(now)?; - let (assertion_input, credential) = if let StatefulCommand::GetAssertion(assertion_state) = - self.stateful_command_permission.get_command()? - { - let credential_key = assertion_state - .next_credential_keys - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self.persistent_store.get_credential(credential_key)?; - (assertion_state.assertion_input.clone(), credential) - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); - }; + let (assertion_input, credential_key) = self + .stateful_command_permission + .next_assertion_credential()?; + let credential = self.persistent_store.get_credential(credential_key)?; self.assertion_response(credential, assertion_input, None) } From 6bf4a7edec3370f1bb3b452888f04f5f8c6caec1 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 13:22:24 +0100 Subject: [PATCH 33/86] fix typo --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 96d7b25..8ac324c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -180,7 +180,7 @@ pub enum StatefulCommand { /// remove all state and permissions. Power cycling allows the Reset command, /// and to prevent misuse or accidents, we disallow Reset after receiving /// different commands. Therefore, Reset behaves just like all other stateful -/// commands and is included here. Please not that the allowed time for Reset +/// commands and is included here. Please note that the allowed time for Reset /// differs from all other stateful commands. pub struct StatefulPermission { permission: TimedPermission, From 8634e2ec2437eed88229f0a28a00c78016d6e3dc Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 20 Jan 2021 15:56:06 +0100 Subject: [PATCH 34/86] Make StoreUpdate generic over the byte slice ownership This permits to call it without having to create a Vec when possible. --- libraries/persistent_store/fuzz/src/store.rs | 2 +- libraries/persistent_store/src/format.rs | 17 ++++++++++++----- libraries/persistent_store/src/model.rs | 4 ++-- libraries/persistent_store/src/store.rs | 18 +++++++++++------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index 3113f77..006532b 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -303,7 +303,7 @@ impl<'a> Fuzzer<'a> { } /// Generates a possibly invalid update. - fn update(&mut self) -> StoreUpdate { + fn update(&mut self) -> StoreUpdate> { match self.entropy.read_range(0, 1) { 0 => { let key = self.key(); diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index f575750..a70dcc4 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -20,6 +20,7 @@ use self::bitfield::Length; use self::bitfield::{count_zeros, num_bits, Bit, Checksum, ConstField, Field}; use crate::{usize_to_nat, Nat, Storage, StorageIndex, StoreError, StoreResult, StoreUpdate}; use alloc::vec::Vec; +use core::borrow::Borrow; use core::cmp::min; use core::convert::TryFrom; @@ -492,13 +493,16 @@ impl Format { } /// Returns the capacity required by a transaction. - pub fn transaction_capacity(&self, updates: &[StoreUpdate]) -> Nat { + pub fn transaction_capacity>( + &self, + updates: &[StoreUpdate], + ) -> Nat { match updates.len() { // An empty transaction doesn't consume anything. 0 => 0, // Transactions with a single update are optimized by avoiding a marker entry. 1 => match &updates[0] { - StoreUpdate::Insert { value, .. } => self.entry_size(value), + StoreUpdate::Insert { value, .. } => self.entry_size(value.borrow()), // Transactions with a single update which is a removal don't consume anything. StoreUpdate::Remove { .. } => 0, }, @@ -508,9 +512,9 @@ impl Format { } /// Returns the capacity of an update. - fn update_capacity(&self, update: &StoreUpdate) -> Nat { + fn update_capacity>(&self, update: &StoreUpdate) -> Nat { match update { - StoreUpdate::Insert { value, .. } => self.entry_size(value), + StoreUpdate::Insert { value, .. } => self.entry_size(value.borrow()), StoreUpdate::Remove { .. } => 1, } } @@ -523,7 +527,10 @@ impl Format { /// Checks if a transaction is valid and returns its sorted keys. /// /// Returns `None` if the transaction is invalid. - pub fn transaction_valid(&self, updates: &[StoreUpdate]) -> Option> { + pub fn transaction_valid>( + &self, + updates: &[StoreUpdate], + ) -> Option> { if usize_to_nat(updates.len()) > self.max_updates() { return None; } diff --git a/libraries/persistent_store/src/model.rs b/libraries/persistent_store/src/model.rs index c509b03..eebc329 100644 --- a/libraries/persistent_store/src/model.rs +++ b/libraries/persistent_store/src/model.rs @@ -34,7 +34,7 @@ pub enum StoreOperation { /// Applies a transaction. Transaction { /// The list of updates to be applied. - updates: Vec, + updates: Vec>>, }, /// Deletes all keys above a threshold. @@ -89,7 +89,7 @@ impl StoreModel { } /// Applies a transaction. - fn transaction(&mut self, updates: Vec) -> StoreResult<()> { + fn transaction(&mut self, updates: Vec>>) -> StoreResult<()> { // Fail if the transaction is invalid. if self.format.transaction_valid(&updates).is_none() { return Err(StoreError::InvalidArgument); diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index f707a89..224eeb9 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -25,6 +25,7 @@ pub use crate::{ }; use alloc::boxed::Box; use alloc::vec::Vec; +use core::borrow::Borrow; use core::cmp::{max, min, Ordering}; use core::convert::TryFrom; use core::option::NoneError; @@ -159,15 +160,15 @@ impl StoreHandle { /// Represents an update to the store as part of a transaction. #[derive(Clone, Debug)] -pub enum StoreUpdate { +pub enum StoreUpdate> { /// Inserts or replaces an entry in the store. - Insert { key: usize, value: Vec }, + Insert { key: usize, value: ByteSlice }, /// Removes an entry from the store. Remove { key: usize }, } -impl StoreUpdate { +impl> StoreUpdate { /// Returns the key affected by the update. pub fn key(&self) -> usize { match *self { @@ -179,7 +180,7 @@ impl StoreUpdate { /// Returns the value written by the update. pub fn value(&self) -> Option<&[u8]> { match self { - StoreUpdate::Insert { value, .. } => Some(value), + StoreUpdate::Insert { value, .. } => Some(value.borrow()), StoreUpdate::Remove { .. } => None, } } @@ -280,14 +281,17 @@ impl Store { /// - There are too many updates. /// - The updates overlap, i.e. their keys are not disjoint. /// - The updates are invalid, e.g. key out of bound or value too long. - pub fn transaction(&mut self, updates: &[StoreUpdate]) -> StoreResult<()> { + pub fn transaction>( + &mut self, + updates: &[StoreUpdate], + ) -> StoreResult<()> { let count = usize_to_nat(updates.len()); if count == 0 { return Ok(()); } if count == 1 { match updates[0] { - StoreUpdate::Insert { key, ref value } => return self.insert(key, value), + StoreUpdate::Insert { key, ref value } => return self.insert(key, value.borrow()), StoreUpdate::Remove { key } => return self.remove(key), } } @@ -310,7 +314,7 @@ impl Store { for update in updates { let length = match *update { StoreUpdate::Insert { key, ref value } => { - let entry = self.format.build_user(usize_to_nat(key), value)?; + let entry = self.format.build_user(usize_to_nat(key), value.borrow())?; let word_size = self.format.word_size(); let footer = usize_to_nat(entry.len()) / word_size - 1; self.write_slice(tail, &entry[..(footer * word_size) as usize])?; From 14189a398addfc273e179b84d43ecce4f3666258 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 18:46:38 +0100 Subject: [PATCH 35/86] implements the credBlob extensions --- README.md | 2 + src/ctap/command.rs | 42 ++-- src/ctap/credential_management.rs | 2 + src/ctap/data_formats.rs | 47 +++- src/ctap/mod.rs | 359 +++++++++++++++++++++++------- src/ctap/storage.rs | 6 +- 6 files changed, 356 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 0691fb8..da46d7f 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ a few things you can personalize: allows some relying parties to read the minimum PIN length by default. The latter allows storing more relying parties that may check the minimum PIN length. +1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs + bigger than the default value. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 620a06f..128c4b4 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -147,8 +147,9 @@ pub struct AuthenticatorMakeCredentialParameters { pub user: PublicKeyCredentialUserEntity, pub pub_key_cred_params: Vec, pub exclude_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: MakeCredentialExtensions, + // Same for options, use defaults when not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, @@ -198,15 +199,13 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let extensions = extensions .map(MakeCredentialExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => MakeCredentialOptions::try_from(entry)?, - None => MakeCredentialOptions { - rk: false, - uv: false, - }, - }; + let options = options + .map(MakeCredentialOptions::try_from) + .transpose()? + .unwrap_or_default(); 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()?; @@ -230,8 +229,9 @@ pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, pub allow_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: GetAssertionExtensions, + // Same for options, use defaults when not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, @@ -272,15 +272,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { let extensions = extensions .map(GetAssertionExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => GetAssertionOptions::try_from(entry)?, - None => GetAssertionOptions { - up: true, - uv: false, - }, - }; + let options = options + .map(GetAssertionOptions::try_from) + .transpose()? + .unwrap_or_default(); 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()?; @@ -545,7 +543,7 @@ mod test { user, pub_key_cred_params: vec![ES256_CRED_PARAM], exclude_list: Some(vec![]), - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), @@ -591,7 +589,7 @@ mod test { rp_id, client_data_hash, allow_list: Some(vec![pub_key_cred_descriptor]), - extensions: None, + extensions: GetAssertionExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 25fe3b9..7681fa1 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -80,6 +80,7 @@ fn enumerate_credentials_response( creation_order: _, user_name, user_icon, + cred_blob: _, } = credential; let user = PublicKeyCredentialUserEntity { user_id: user_handle, @@ -346,6 +347,7 @@ mod test { creation_order: 0, user_name: Some("name".to_string()), user_icon: Some("icon".to_string()), + cred_blob: None, } } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9d4d10f..da992f8 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -275,11 +275,13 @@ impl From for cbor::Value { } } +#[derive(Default)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, pub min_pin_length: bool, + pub cred_blob: Option>, } impl TryFrom for MakeCredentialExtensions { @@ -288,6 +290,7 @@ impl TryFrom for MakeCredentialExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "credProtect" => cred_protect, "hmac-secret" => hmac_secret, "minPinLength" => min_pin_length, @@ -299,17 +302,21 @@ impl TryFrom for MakeCredentialExtensions { .map(CredentialProtectionPolicy::try_from) .transpose()?; let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; Ok(Self { hmac_secret, cred_protect, min_pin_length, + cred_blob, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Default)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct GetAssertionExtensions { pub hmac_secret: Option, + pub cred_blob: bool, } impl TryFrom for GetAssertionExtensions { @@ -318,6 +325,7 @@ impl TryFrom for GetAssertionExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "hmac-secret" => hmac_secret, } = extract_map(cbor_value)?; } @@ -325,7 +333,11 @@ impl TryFrom for GetAssertionExtensions { let hmac_secret = hmac_secret .map(GetAssertionHmacSecretInput::try_from) .transpose()?; - Ok(Self { hmac_secret }) + let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?; + Ok(Self { + hmac_secret, + cred_blob, + }) } } @@ -361,6 +373,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. +#[derive(Default)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct MakeCredentialOptions { pub rk: bool, @@ -400,6 +413,15 @@ pub struct GetAssertionOptions { pub uv: bool, } +impl Default for GetAssertionOptions { + fn default() -> Self { + GetAssertionOptions { + up: true, + uv: false, + } + } +} + impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; @@ -523,6 +545,7 @@ pub struct PublicKeyCredentialSource { pub creation_order: u64, pub user_name: Option, pub user_icon: Option, + pub cred_blob: Option>, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -537,6 +560,7 @@ enum PublicKeyCredentialSourceField { CreationOrder = 7, UserName = 8, UserIcon = 9, + CredBlob = 10, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: @@ -563,6 +587,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, PublicKeyCredentialSourceField::UserName => credential.user_name, PublicKeyCredentialSourceField::UserIcon => credential.user_icon, + PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, } } } @@ -582,6 +607,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserIcon => user_icon, + PublicKeyCredentialSourceField::CredBlob => cred_blob, } = extract_map(cbor_value)?; } @@ -601,6 +627,7 @@ impl TryFrom for PublicKeyCredentialSource { let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; let user_name = user_name.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -622,6 +649,7 @@ impl TryFrom for PublicKeyCredentialSource { creation_order, user_name, user_icon, + cred_blob, }) } } @@ -1493,12 +1521,14 @@ mod test { "hmac-secret" => true, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "minPinLength" => true, + "credBlob" => vec![0xCB], }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: true, + cred_blob: Some(vec![0xCB]), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1515,6 +1545,7 @@ mod test { 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, + "credBlob" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { @@ -1524,6 +1555,7 @@ mod test { }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), + cred_blob: true, }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1816,6 +1848,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert_eq!( @@ -1858,6 +1891,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + cred_blob: Some(vec![0xCB]), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8ac324c..6ffd108 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -35,7 +35,7 @@ use self::command::{ use self::config_command::process_config; use self::credential_management::process_credential_management; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, @@ -47,17 +47,18 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::PersistentStore; +use self::storage::{PersistentStore, MAX_RP_IDS_LENGTH}; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; +use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; -use cbor::{cbor_map, cbor_map_options}; +use cbor::cbor_map_options; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; @@ -124,6 +125,8 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa // - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) // - Some(CredentialProtectionPolicy::UserVerificationRequired) const DEFAULT_CRED_PROTECT: Option = None; +// Maximum size stored with the credBlob extension. Must be at least 32. +const MAX_CRED_BLOB_LENGTH: usize = 32; // Checks the PIN protocol parameter against all supported versions. pub fn check_pin_uv_auth_protocol( @@ -154,7 +157,7 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { pub struct AssertionInput { client_data_hash: Vec, auth_data: Vec, - hmac_secret_input: Option, + extensions: GetAssertionExtensions, has_uv: bool, } @@ -168,7 +171,7 @@ pub struct AssertionState { /// Stores which command currently holds state for subsequent calls. pub enum StatefulCommand { Reset, - GetAssertion(AssertionState), + GetAssertion(Box), EnumerateRps(usize), EnumerateCredentials(Vec), } @@ -419,6 +422,7 @@ where creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, })) } @@ -558,27 +562,31 @@ where } let rp_id = rp.rp_id; - let (use_hmac_extension, cred_protect_policy, min_pin_length) = - if let Some(extensions) = extensions { - let mut cred_protect = extensions.cred_protect; - if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - < DEFAULT_CRED_PROTECT - .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - { - cred_protect = DEFAULT_CRED_PROTECT; - } - let min_pin_length = extensions.min_pin_length - && self - .persistent_store - .min_pin_length_rp_ids()? - .contains(&rp_id); - (extensions.hmac_secret, cred_protect, min_pin_length) - } else { - (false, DEFAULT_CRED_PROTECT, false) - }; - - let has_extension_output = - use_hmac_extension || cred_protect_policy.is_some() || min_pin_length; + let mut cred_protect_policy = extensions.cred_protect; + if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect_policy = DEFAULT_CRED_PROTECT; + } + let min_pin_length = extensions.min_pin_length + && self + .persistent_store + .min_pin_length_rp_ids()? + .contains(&rp_id); + // None for no input, false for invalid input, true for valid input. + let has_cred_blob_output = extensions.cred_blob.is_some(); + let cred_blob = extensions + .cred_blob + .filter(|c| options.rk && c.len() <= MAX_CRED_BLOB_LENGTH); + let cred_blob_output = if has_cred_blob_output { + Some(cred_blob.is_some()) + } else { + None + }; + let has_extension_output = extensions.hmac_secret + || cred_protect_policy.is_some() + || min_pin_length + || has_cred_blob_output; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { @@ -656,6 +664,7 @@ where user_icon: user .user_icon .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + cred_blob, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -675,7 +684,11 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if has_extension_output { - let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let hmac_secret_output = if extensions.hmac_secret { + Some(true) + } else { + None + }; let min_pin_length_output = if min_pin_length { Some(self.persistent_store.min_pin_length()? as u64) } else { @@ -685,6 +698,7 @@ where "hmac-secret" => hmac_secret_output, "credProtect" => cred_protect_policy, "minPinLength" => min_pin_length_output, + "credBlob" => cred_blob_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -754,18 +768,30 @@ where let AssertionInput { client_data_hash, mut auth_data, - hmac_secret_input, + extensions, has_uv, } = assertion_input; // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; - let encrypted_output = self - .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &cred_random)?; - let extensions_output = cbor_map! { + if extensions.hmac_secret.is_some() || extensions.cred_blob { + let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret { + let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; + Some( + self.pin_protocol_v1 + .process_hmac_secret(hmac_secret_input, &cred_random)?, + ) + } else { + None + }; + // This could be written more nicely with `then_some` when stable. + let cred_blob = if extensions.cred_blob { + Some(credential.cred_blob.unwrap_or_default()) + } else { + None + }; + let extensions_output = cbor_map_options! { "hmac-secret" => encrypted_output, + "credBlob" => cred_blob, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -854,8 +880,7 @@ where self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; - let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); - if hmac_secret_input.is_some() && !options.up { + if extensions.hmac_secret.is_some() && !options.up { // The extension is actually supported, but we need user presence. return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } @@ -891,7 +916,7 @@ where if options.up { flags |= UP_FLAG; } - if hmac_secret_input.is_some() { + if extensions.hmac_secret.is_some() || extensions.cred_blob { flags |= ED_FLAG; } @@ -939,17 +964,17 @@ where let assertion_input = AssertionInput { client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, - hmac_secret_input, + extensions, has_uv, }; let number_of_credentials = if next_credential_keys.is_empty() { None } else { let number_of_credentials = Some(next_credential_keys.len() + 1); - let assertion_state = StatefulCommand::GetAssertion(AssertionState { + let assertion_state = StatefulCommand::GetAssertion(Box::new(AssertionState { assertion_input: assertion_input.clone(), next_credential_keys, - }); + })); self.stateful_command_permission .set_command(now, assertion_state); number_of_credentials @@ -993,22 +1018,21 @@ where String::from("hmac-secret"), String::from("credProtect"), String::from("minPinLength"), + String::from("credBlob"), ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // TODO(#106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, - max_cred_blob_length: None, - // TODO(kaczmarczyck) update when extension is implemented - max_rp_ids_for_set_min_pin_length: None, + max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), + max_rp_ids_for_set_min_pin_length: Some(MAX_RP_IDS_LENGTH as u64), remaining_discoverable_credentials: Some( self.persistent_store.remaining_credentials()? as u64, ), @@ -1149,11 +1173,11 @@ where mod test { use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; - use cbor::cbor_array; + use cbor::{cbor_array, cbor_map}; use crypto::rng256::ThreadRng256; const CLOCK_FREQUENCY_HZ: usize = 32768; @@ -1209,7 +1233,7 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAB, 0x01]; + let mut expected_response = vec![0x00, 0xAD, 0x01]; // The version array differs with CTAP1, always including 2.0 and 2.1. #[cfg(not(feature = "with_ctap1"))] let version_count = 2; @@ -1221,10 +1245,11 @@ mod test { expected_response.extend( [ 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x84, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50, + 0x65, 0x6E, 0x67, 0x74, 0x68, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, + 0x03, 0x50, ] .iter(), ); @@ -1237,7 +1262,7 @@ mod test { 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, - 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, + 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96, ] .iter(), ); @@ -1269,7 +1294,7 @@ mod test { user, pub_key_cred_params, exclude_list: None, - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: None, pin_uv_auth_protocol: None, @@ -1293,11 +1318,12 @@ mod test { fn create_make_credential_parameters_with_cred_protect_policy( policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: Some(policy), min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; make_credential_params @@ -1380,6 +1406,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1458,11 +1485,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = extensions; @@ -1487,11 +1515,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1516,11 +1545,12 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // First part: The extension is ignored, since the RP ID is not on the list. - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: None, min_pin_length: true, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1541,11 +1571,12 @@ mod test { Ok(()) ); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: None, min_pin_length: true, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1563,6 +1594,82 @@ mod test { ); } + #[test] + fn test_process_make_credential_cred_blob_ok() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: false, + cred_blob: Some(vec![0xCB]), + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, Some(vec![0xCB])); + } + + #[test] + fn test_process_make_credential_cred_blob_too_big() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: false, + cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]), + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF4, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, None); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; @@ -1586,6 +1693,7 @@ mod test { flags: u8, signature_counter: u32, expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], ) { match response.unwrap() { ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { @@ -1605,6 +1713,7 @@ mod test { &mut expected_auth_data[signature_counter_position..], signature_counter, ); + expected_auth_data.extend(expected_extension_cbor); assert_eq!(auth_data, expected_auth_data); assert_eq!(user, Some(expected_user)); assert_eq!(number_of_credentials, expected_number_of_credentials); @@ -1613,6 +1722,29 @@ mod test { } } + fn check_assertion_response_with_extension( + response: Result, + expected_user_id: Vec, + signature_counter: u32, + expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x80, + signature_counter, + expected_number_of_credentials, + expected_extension_cbor, + ); + } + fn check_assertion_response( response: Result, expected_user_id: Vec, @@ -1631,6 +1763,7 @@ mod test { 0x00, signature_counter, expected_number_of_credentials, + &[], ); } @@ -1649,7 +1782,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1676,11 +1809,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = make_extensions; @@ -1704,9 +1838,10 @@ mod test { salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], }; - let get_extensions = Some(GetAssertionExtensions { + let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - }); + cred_blob: false, + }; let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, @@ -1744,11 +1879,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; assert!(ctap_state @@ -1761,9 +1897,10 @@ mod test { salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], }; - let get_extensions = Some(GetAssertionExtensions { + let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - }); + cred_blob: false, + }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), @@ -1800,7 +1937,7 @@ mod test { let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, key_id: credential_id.clone(), - transports: None, // You can set USB as a hint here. + transports: None, }; let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, @@ -1815,6 +1952,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1825,7 +1963,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1847,7 +1985,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc.clone()]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1877,6 +2015,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1887,7 +2026,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1906,6 +2045,69 @@ mod test { ); } + #[test] + fn test_process_get_assertion_with_cred_blob() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: Some(vec![0xCB]), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + hmac_secret: None, + cred_blob: true, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB, + ]; + check_assertion_response_with_extension( + get_assertion_response, + vec![0x1D], + signature_counter, + None, + &expected_extension_cbor, + ); + } + #[test] fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; @@ -1951,7 +2153,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: true, @@ -1974,6 +2176,7 @@ mod test { 0x04, signature_counter, Some(2), + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -1983,6 +2186,7 @@ mod test { 0x04, signature_counter, None, + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -2027,7 +2231,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -2091,7 +2295,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -2147,6 +2351,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 18dd199..ffc5dd6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -57,7 +57,7 @@ const DEFAULT_MIN_PIN_LENGTH: u8 = 4; const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. -const MAX_RP_IDS_LENGTH: usize = 8; +pub const MAX_RP_IDS_LENGTH: usize = 8; /// Wrapper for master keys. pub struct MasterKeys { @@ -690,6 +690,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, } } @@ -906,6 +907,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -927,6 +929,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1160,6 +1163,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From de3addba74530ca34f8eb5d26e12c8a0c7c2184a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 21 Jan 2021 17:38:22 +0100 Subject: [PATCH 36/86] force PIN changes --- src/ctap/config_command.rs | 56 +++++++++++++++++++++++++++++++++++++ src/ctap/mod.rs | 18 ++++++++---- src/ctap/pin_protocol_v1.rs | 49 ++++++++++++++++++++++++++++++++ src/ctap/storage.rs | 29 +++++++++++++++++-- src/ctap/storage/key.rs | 3 ++ 5 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 726634c..e96ce7a 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -248,6 +248,62 @@ mod test { ); } + #[test] + fn test_process_set_min_pin_length_force_pin_change_implicit() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + // Increase min PIN, force PIN change. + let min_pin_length = 6; + let mut config_params = create_min_pin_config_params(min_pin_length, None); + let pin_uv_auth_param = Some(vec![ + 0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E, + 0xA8, 0xC8, + ]); + config_params.pin_uv_auth_param = pin_uv_auth_param; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); + } + + #[test] + fn test_process_set_min_pin_length_force_pin_change_explicit() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + let pin_uv_auth_param = Some(vec![ + 0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1, + 0x53, 0xD7, + ]); + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(persistent_store.min_pin_length().unwrap()), + min_pin_length_rp_ids: None, + force_change_pin: Some(true), + }; + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::SetMinPinLength, + sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength( + set_min_pin_length_params, + )), + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); + } + #[test] fn test_process_config_vendor_prototype() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6ffd108..8a4479a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1006,6 +1006,10 @@ where ); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); + options_map.insert( + String::from("forcePINChange"), + self.persistent_store.has_force_pin_change()?, + ); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -1256,13 +1260,15 @@ mod test { expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend( [ - 0x04, 0xA5, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, + 0x04, 0xA6, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, - 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, - 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, - 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, - 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96, + 0x69, 0x6E, 0xF4, 0x6E, 0x66, 0x6F, 0x72, 0x63, 0x65, 0x50, 0x49, 0x4E, 0x43, 0x68, + 0x61, 0x6E, 0x67, 0x65, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, + 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, + 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, + 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, + 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, + 0x18, 0x96, ] .iter(), ); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 0e46573..ae1b1df 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -328,6 +328,9 @@ impl PinProtocolV1 { let token_encryption_key = crypto::aes256::EncryptionKey::new(&shared_secret); let pin_decryption_key = crypto::aes256::DecryptionKey::new(&token_encryption_key); self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; + if persistent_store.has_force_pin_change()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. let iv = [0u8; 16]; @@ -821,6 +824,28 @@ mod test { ); } + #[test] + fn test_process_get_pin_token_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert_eq!( + pin_protocol_v1.process_get_pin_token( + &mut rng, + &mut persistent_store, + key_agreement, + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + ); + } + #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { let mut rng = ThreadRng256 {}; @@ -885,6 +910,30 @@ mod test { ); } + #[test] + fn test_process_get_pin_token_force_pin_change_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert_eq!( + pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut rng, + &mut persistent_store, + key_agreement, + pin_hash_enc, + 0x03, + Some(String::from("example.com")), + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + ); + } + #[test] fn test_process() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index ffc5dd6..74c11a6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -384,7 +384,9 @@ impl PersistentStore { let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; pin_properties[0] = pin_code_point_length; pin_properties[1..].clone_from_slice(pin_hash); - Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?) + self.store.insert(key::PIN_PROPERTIES, &pin_properties)?; + // If this second transaction fails, you are forced to retry. + Ok(self.store.remove(key::FORCE_PIN_CHANGE)?) } /// Returns the number of remaining PIN retries. @@ -541,9 +543,18 @@ impl PersistentStore { Ok(()) } + /// Returns whether the PIN needs to be changed before its next usage. + pub fn has_force_pin_change(&self) -> Result { + match self.store.find(key::FORCE_PIN_CHANGE)? { + None => Ok(false), + Some(value) if value.len() == 1 && value[0] == 1 => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks the PIN as outdated with respect to the new PIN policy. pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { - // TODO(kaczmarczyck) implement storage logic - Ok(()) + Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[1])?) } } @@ -1148,6 +1159,18 @@ mod test { } } + #[test] + fn test_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + assert!(persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.set_pin(&[0x88; 16], 8), Ok(())); + assert!(!persistent_store.has_force_pin_change().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index c6e46e2..1c0e21e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -88,6 +88,9 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; + /// If this entry exists and equals 1, the PIN needs to be changed. + FORCE_PIN_CHANGE = 2040; + /// The secret of the CredRandom feature. CRED_RANDOM_SECRET = 2041; From 3408c0a2edf7f6f880803c9d844e779c954a5322 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 21 Jan 2021 18:24:25 +0100 Subject: [PATCH 37/86] makes test_get_info more readable --- src/ctap/mod.rs | 75 +++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8a4479a..6f10589 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1181,7 +1181,7 @@ mod test { MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; - use cbor::{cbor_array, cbor_map}; + use cbor::{cbor_array, cbor_array_vec, cbor_map}; use crypto::rng256::ThreadRng256; const CLOCK_FREQUENCY_HZ: usize = 32768; @@ -1237,43 +1237,44 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAD, 0x01]; - // The version array differs with CTAP1, always including 2.0 and 2.1. - #[cfg(not(feature = "with_ctap1"))] - let version_count = 2; - #[cfg(feature = "with_ctap1")] - let version_count = 3; - expected_response.push(0x80 + version_count); - #[cfg(feature = "with_ctap1")] - expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); - expected_response.extend( - [ - 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x84, 0x6B, 0x68, 0x6D, 0x61, - 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, - 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, - 0x03, 0x50, - ] - .iter(), - ); - expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_response.extend( - [ - 0x04, 0xA6, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, - 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, - 0x69, 0x6E, 0xF4, 0x6E, 0x66, 0x6F, 0x72, 0x63, 0x65, 0x50, 0x49, 0x4E, 0x43, 0x68, - 0x61, 0x6E, 0x67, 0x65, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, - 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, - 0x01, 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, - 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, - 0x69, 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, - 0x18, 0x96, - ] - .iter(), - ); + let expected_cbor = cbor_map_options! { + 0x01 => cbor_array_vec![vec![ + #[cfg(feature = "with_ctap1")] + String::from(U2F_VERSION_STRING), + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]], + 0x02 => cbor_array_vec![vec![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + String::from("credBlob"), + ]], + 0x03 => ctap_state.persistent_store.aaguid().unwrap(), + 0x04 => cbor_map! { + "rk" => true, + "up" => true, + "clientPin" => false, + "credMgmt" => true, + "setMinPINLength" => true, + "forcePINChange" => false, + }, + 0x05 => 1024, + 0x06 => cbor_array_vec![vec![1]], + 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), + 0x08 => CREDENTIAL_ID_SIZE as u64, + 0x09 => cbor_array_vec![vec!["usb"]], + 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], + 0x0C => DEFAULT_CRED_PROTECT.map(|c| c as u64), + 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, + 0x0F => MAX_CRED_BLOB_LENGTH as u64, + 0x10 => MAX_RP_IDS_LENGTH as u64, + 0x14 => ctap_state.persistent_store.remaining_credentials().unwrap() as u64, + }; - assert_eq!(info_reponse, expected_response); + let mut response_cbor = vec![0x00]; + assert!(cbor::write(expected_cbor, &mut response_cbor)); + assert_eq!(info_reponse, response_cbor); } fn create_minimal_make_credential_parameters() -> AuthenticatorMakeCredentialParameters { From 5fe111698bb6993cd72255075bb6d407c45a837d Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 21 Jan 2021 18:47:00 +0100 Subject: [PATCH 38/86] remove resolved TODO --- src/ctap/config_command.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index e96ce7a..5e4daf3 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -44,7 +44,6 @@ fn process_set_min_pin_length( force_change_pin |= new_min_pin_length > old_length; } if force_change_pin { - // TODO(kaczmarczyck) actually force a PIN change in PinProtocolV1 persistent_store.force_pin_change()?; } persistent_store.set_min_pin_length(new_min_pin_length)?; From c38f00624a1a315d5533952c4be45faf8f1943c5 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 10:55:11 +0100 Subject: [PATCH 39/86] use transactions, and how to store a bool --- src/ctap/pin_protocol_v1.rs | 1 + src/ctap/storage.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index ae1b1df..eb537f0 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -328,6 +328,7 @@ impl PinProtocolV1 { let token_encryption_key = crypto::aes256::EncryptionKey::new(&shared_secret); let pin_decryption_key = crypto::aes256::DecryptionKey::new(&token_encryption_key); self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; + // TODO(kaczmarczyck) can this be moved up in the specification? if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 74c11a6..b85f918 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -30,6 +30,7 @@ use arrayref::array_ref; use cbor::cbor_array_vec; use core::convert::TryInto; use crypto::rng256::Rng256; +use persistent_store::StoreUpdate; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -384,9 +385,15 @@ impl PersistentStore { let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; pin_properties[0] = pin_code_point_length; pin_properties[1..].clone_from_slice(pin_hash); - self.store.insert(key::PIN_PROPERTIES, &pin_properties)?; - // If this second transaction fails, you are forced to retry. - Ok(self.store.remove(key::FORCE_PIN_CHANGE)?) + Ok(self.store.transaction(&[ + StoreUpdate::Insert { + key: key::PIN_PROPERTIES, + value: &pin_properties[..], + }, + StoreUpdate::Remove { + key: key::FORCE_PIN_CHANGE, + }, + ])?) } /// Returns the number of remaining PIN retries. @@ -547,14 +554,14 @@ impl PersistentStore { pub fn has_force_pin_change(&self) -> Result { match self.store.find(key::FORCE_PIN_CHANGE)? { None => Ok(false), - Some(value) if value.len() == 1 && value[0] == 1 => Ok(true), + Some(value) if value.is_empty() => Ok(true), _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } } /// Marks the PIN as outdated with respect to the new PIN policy. pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { - Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[1])?) + Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } } From b2c8c5a12879cf8421e2ab5c18a074bf26746ec9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 6 Jan 2021 13:39:59 +0100 Subject: [PATCH 40/86] adds the new command AuthenticatorLargeBlobs --- src/ctap/command.rs | 236 ++++++++++++++++- src/ctap/credential_management.rs | 2 +- src/ctap/large_blobs.rs | 422 ++++++++++++++++++++++++++++++ src/ctap/mod.rs | 12 +- src/ctap/pin_protocol_v1.rs | 2 +- src/ctap/response.rs | 35 +++ src/ctap/storage.rs | 209 +++++++++++++++ src/ctap/storage/key.rs | 5 + 8 files changed, 914 insertions(+), 9 deletions(-) create mode 100644 src/ctap/large_blobs.rs diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 128c4b4..2e1fe3b 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -22,6 +22,7 @@ use super::data_formats::{ }; use super::key_material; use super::status_code::Ctap2StatusCode; +use super::storage::MAX_LARGE_BLOB_ARRAY_SIZE; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; @@ -33,6 +34,9 @@ use core::convert::TryFrom; // You might also want to set the max credential size in process_get_info then. pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; +// This constant is a consequence of the structure of messages. +const MIN_LARGE_BLOB_LEN: usize = 17; + // CTAP specification (version 20190130) section 6.1 #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub enum Command { @@ -44,8 +48,8 @@ pub enum Command { AuthenticatorGetNextAssertion, AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, + AuthenticatorLargeBlobs(AuthenticatorLargeBlobsParameters), AuthenticatorConfig(AuthenticatorConfigParameters), - // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } @@ -56,8 +60,6 @@ impl From for Ctap2StatusCode { } } -// TODO: Remove this `allow(dead_code)` once the constants are used. -#[allow(dead_code)] impl Command { const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01; const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02; @@ -65,8 +67,8 @@ impl Command { const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_RESET: u8 = 0x07; const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; - // TODO(kaczmarczyck) use or remove those constants - const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; + // Implement Bio Enrollment when your hardware supports biometrics. + const _AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A; const AUTHENTICATOR_SELECTION: u8 = 0x0B; const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C; @@ -123,6 +125,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_LARGE_BLOBS => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorLargeBlobs( + AuthenticatorLargeBlobsParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_CONFIG => { let decoded_cbor = cbor::read(&bytes[1..])?; Ok(Command::AuthenticatorConfig( @@ -351,6 +359,81 @@ impl TryFrom for AuthenticatorClientPinParameters { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorLargeBlobsParameters { + pub get: Option, + pub set: Option>, + pub offset: usize, + pub length: Option, + pub pin_uv_auth_param: Option>, + pub pin_uv_auth_protocol: Option, +} + +impl TryFrom for AuthenticatorLargeBlobsParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => get, + 2 => set, + 3 => offset, + 4 => length, + 5 => pin_uv_auth_param, + 6 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } + + // careful: some missing parameters here are CTAP1_ERR_INVALID_PARAMETER + let get = get.map(extract_unsigned).transpose()?.map(|u| u as usize); + let set = set.map(extract_byte_string).transpose()?; + let offset = + extract_unsigned(offset.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?)? as usize; + let length = length + .map(extract_unsigned) + .transpose()? + .map(|u| u as usize); + 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()?; + + if get.is_none() && set.is_none() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if get.is_some() && set.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if get.is_some() + && (length.is_some() || pin_uv_auth_param.is_some() || pin_uv_auth_protocol.is_some()) + { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if set.is_some() && offset == 0 { + match length { + None => return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + Some(len) if len > MAX_LARGE_BLOB_ARRAY_SIZE => { + return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL) + } + Some(len) if len < MIN_LARGE_BLOB_LEN => { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } + Some(_) => (), + } + } + if set.is_some() && offset != 0 && length.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + Ok(AuthenticatorLargeBlobsParameters { + get, + set, + offset, + length, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorConfigParameters { pub sub_command: ConfigSubCommand, @@ -698,6 +781,149 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorSelection)); } + #[test] + fn test_from_cbor_large_blobs_parameters() { + // successful get + let cbor_value = cbor_map! { + 1 => 2, + 3 => 4, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: Some(2), + set: None, + offset: 4, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful first set + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 0, + 4 => MIN_LARGE_BLOB_LEN as u64, + 5 => vec! [0xA9], + 6 => 1, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(vec![0x5E]), + offset: 0, + length: Some(MIN_LARGE_BLOB_LEN), + pin_uv_auth_param: Some(vec![0xA9]), + pin_uv_auth_protocol: Some(1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful next set + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 1, + 5 => vec! [0xA9], + 6 => 1, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(vec![0x5E]), + offset: 1, + length: None, + pin_uv_auth_param: Some(vec![0xA9]), + pin_uv_auth_protocol: Some(1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // failing with neither get nor set + let cbor_value = cbor_map! { + 3 => 4, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and set + let cbor_value = cbor_map! { + 1 => 2, + 2 => vec! [0x5E], + 3 => 4, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and length + let cbor_value = cbor_map! { + 1 => 2, + 3 => 4, + 4 => MIN_LARGE_BLOB_LEN as u64, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with zero offset and no length present + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 0, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with length smaller than minimum + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 0, + 4 => MIN_LARGE_BLOB_LEN as u64 - 1, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with non-zero offset and length present + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 4, + 4 => MIN_LARGE_BLOB_LEN as u64, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_vendor_configure() { // Incomplete command diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 7681fa1..7665ea7 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -100,7 +100,7 @@ fn enumerate_credentials_response( public_key: Some(public_key), total_credentials, cred_protect: cred_protect_policy, - // TODO(kaczmarczyck) add when largeBlobKey is implemented + // TODO(kaczmarczyck) add when largeBlobKey extension is implemented large_blob_key: None, ..Default::default() }) diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs new file mode 100644 index 0000000..32934e9 --- /dev/null +++ b/src/ctap/large_blobs.rs @@ -0,0 +1,422 @@ +// Copyright 2020-2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::check_pin_uv_auth_protocol; +use super::command::AuthenticatorLargeBlobsParameters; +use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; +use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use alloc::vec; +use alloc::vec::Vec; +use byteorder::{ByteOrder, LittleEndian}; +use crypto::sha256::Sha256; +use crypto::Hash256; + +/// This is maximum message size supported by the authenticator. 1024 is the default. +/// Increasing this values can speed up commands with longer responses, but lead to +/// packets dropping or unexpected failures. +pub const MAX_MSG_SIZE: usize = 1024; +/// The length of the truncated hash that as appended to the large blob data. +const TRUNCATED_HASH_LEN: usize = 16; + +pub struct LargeBlobs { + buffer: Vec, + expected_length: usize, + expected_next_offset: usize, +} + +/// Implements the logic for the AuthenticatorLargeBlobs command and keeps its state. +impl LargeBlobs { + pub fn new() -> LargeBlobs { + LargeBlobs { + buffer: Vec::new(), + expected_length: 0, + expected_next_offset: 0, + } + } + + /// Process the large blob command. + pub fn process_command( + &mut self, + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + large_blobs_params: AuthenticatorLargeBlobsParameters, + ) -> Result { + let AuthenticatorLargeBlobsParameters { + get, + set, + offset, + length, + pin_uv_auth_param, + pin_uv_auth_protocol, + } = large_blobs_params; + + const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64; + + if let Some(get) = get { + if get > MAX_FRAGMENT_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH); + } + let config = persistent_store.get_large_blob_array(get, offset)?; + return Ok(ResponseData::AuthenticatorLargeBlobs(Some( + AuthenticatorLargeBlobsResponse { config }, + ))); + } + + if let Some(mut set) = set { + if set.len() > MAX_FRAGMENT_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH); + } + if offset == 0 { + // Checks for offset and length are already done in command. + self.expected_length = + length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + self.expected_next_offset = 0; + } + if offset != self.expected_next_offset { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ); + } + if persistent_store.pin_hash()?.is_some() { + let pin_uv_auth_param = + pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + // TODO(kaczmarczyck) Error codes for PIN protocol differ across commands. + // Change to Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED for None? + check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; + pin_protocol_v1.has_permission(PinPermission::LargeBlobWrite)?; + let mut message = vec![0xFF; 32]; + message.extend(&[0x0C, 0x00]); + let mut offset_bytes = [0u8; 4]; + LittleEndian::write_u32(&mut offset_bytes, offset as u32); + message.extend(&offset_bytes); + message.extend(&Sha256::hash(set.as_slice())); + if !pin_protocol_v1.verify_pin_auth_token(&message, &pin_uv_auth_param) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + } + if offset + set.len() > self.expected_length { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if offset == 0 { + self.buffer = Vec::with_capacity(self.expected_length); + } + self.buffer.append(&mut set); + self.expected_next_offset = self.buffer.len(); + if self.expected_next_offset == self.expected_length { + self.expected_length = 0; + self.expected_next_offset = 0; + // Must be a positive number. + let buffer_hash_index = self.buffer.len() - TRUNCATED_HASH_LEN; + if Sha256::hash(&self.buffer[..buffer_hash_index])[..TRUNCATED_HASH_LEN] + != self.buffer[buffer_hash_index..] + { + self.buffer = Vec::new(); + return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE); + } + persistent_store.commit_large_blob_array(&self.buffer)?; + self.buffer = Vec::new(); + } + return Ok(ResponseData::AuthenticatorLargeBlobs(None)); + } + + // This should be unreachable, since the command has either get or set. + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crypto::rng256::ThreadRng256; + + #[test] + fn test_process_command_get_empty() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + let large_blob = vec![ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, + 0x7a, 0x6d, 0x3c, + ]; + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: Some(large_blob.len()), + set: None, + offset: 0, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + match large_blobs_response.unwrap() { + ResponseData::AuthenticatorLargeBlobs(Some(response)) => { + assert_eq!(response.config, large_blob); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_command_commit_and_get() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 200; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[..BLOB_LEN / 2].to_vec()), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[BLOB_LEN / 2..].to_vec()), + offset: BLOB_LEN / 2, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: Some(BLOB_LEN), + set: None, + offset: 0, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + match large_blobs_response.unwrap() { + ResponseData::AuthenticatorLargeBlobs(Some(response)) => { + assert_eq!(response.config, large_blob); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_command_commit_unexpected_offset() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 200; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[..BLOB_LEN / 2].to_vec()), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[BLOB_LEN / 2..].to_vec()), + // The offset is 1 too big. + offset: BLOB_LEN / 2 + 1, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ), + ); + } + + #[test] + fn test_process_command_commit_unexpected_length() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 200; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[..BLOB_LEN / 2].to_vec()), + offset: 0, + // The length is 1 too small. + length: Some(BLOB_LEN - 1), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[BLOB_LEN / 2..].to_vec()), + offset: BLOB_LEN / 2, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + ); + } + + #[test] + fn test_process_command_commit_unexpected_hash() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 20; + // This blob does not have an appropriate hash. + let large_blob = vec![0x1B; BLOB_LEN]; + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob.to_vec()), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE), + ); + } + + #[test] + fn test_process_command_commit_with_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 20; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + let pin_uv_auth_param = Some(vec![ + 0x68, 0x0C, 0x3F, 0x6A, 0x62, 0x47, 0xE6, 0x7C, 0x23, 0x1F, 0x79, 0xE3, 0xDC, 0x6D, + 0xC3, 0xDE, + ]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + } +} diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6f10589..95b5b0a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -21,6 +21,7 @@ mod ctap1; pub mod data_formats; pub mod hid; mod key_material; +mod large_blobs; mod pin_protocol_v1; pub mod response; pub mod status_code; @@ -41,6 +42,7 @@ use self::data_formats::{ SignatureAlgorithm, }; use self::hid::ChannelID; +use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; use self::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, @@ -293,6 +295,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. stateful_command_permission: StatefulPermission, + large_blobs: LargeBlobs, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -318,6 +321,7 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ), stateful_command_permission: StatefulPermission::new_reset(now), + large_blobs: LargeBlobs::new(), } } @@ -484,12 +488,16 @@ where ) } Command::AuthenticatorSelection => self.process_selection(cid), + Command::AuthenticatorLargeBlobs(params) => self.large_blobs.process_command( + &mut self.persistent_store, + &mut self.pin_protocol_v1, + params, + ), Command::AuthenticatorConfig(params) => process_config( &mut self.persistent_store, &mut self.pin_protocol_v1, params, ), - // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands Command::AuthenticatorVendorConfigure(params) => { self.process_vendor_configure(params, cid) @@ -1026,7 +1034,7 @@ where ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), - max_msg_size: Some(1024), + max_msg_size: Some(MAX_MSG_SIZE as u64), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index eb537f0..6ec5644 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -162,7 +162,7 @@ pub enum PinPermission { GetAssertion = 0x02, CredentialManagement = 0x04, BioEnrollment = 0x08, - PlatformConfiguration = 0x10, + LargeBlobWrite = 0x10, AuthenticatorConfiguration = 0x20, } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index e4cda5e..245218f 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -33,6 +33,7 @@ pub enum ResponseData { AuthenticatorReset, AuthenticatorCredentialManagement(Option), AuthenticatorSelection, + AuthenticatorLargeBlobs(Option), // TODO(kaczmarczyck) dummy, extend AuthenticatorConfig, AuthenticatorVendor(AuthenticatorVendorResponse), @@ -49,6 +50,7 @@ impl From for Option { ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()), ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()), ResponseData::AuthenticatorConfig => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -204,6 +206,22 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorLargeBlobsResponse { + pub config: Vec, +} + +impl From for cbor::Value { + fn from(platform_large_blobs_response: AuthenticatorLargeBlobsResponse) -> Self { + let AuthenticatorLargeBlobsResponse { config } = platform_large_blobs_response; + + cbor_map_options! { + 0x01 => config, + } + } +} + #[derive(Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] @@ -510,6 +528,23 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_large_blobs_into_cbor() { + let large_blobs_response = AuthenticatorLargeBlobsResponse { config: vec![0xC0] }; + let response_cbor: Option = + ResponseData::AuthenticatorLargeBlobs(Some(large_blobs_response)).into(); + let expected_cbor = cbor_map_options! { + 0x01 => vec![0xC0], + }; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_empty_large_blobs_into_cbor() { + let response_cbor: Option = ResponseData::AuthenticatorLargeBlobs(None).into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_config_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorConfig.into(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b85f918..a89c02d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -28,6 +28,7 @@ use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; use cbor::cbor_array_vec; +use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; use persistent_store::StoreUpdate; @@ -59,6 +60,9 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; +const SHARD_SIZE: usize = 128; +pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = + SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start); /// Wrapper for master keys. pub struct MasterKeys { @@ -467,6 +471,70 @@ impl PersistentStore { )?) } + /// Reads the byte vector stored as the serialized large blobs array. + /// + /// If more data is requested than stored, return as many bytes as possible. + pub fn get_large_blob_array( + &self, + mut byte_count: usize, + mut offset: usize, + ) -> Result, Ctap2StatusCode> { + if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() { + return Ok(vec![ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, + 0x7a, 0x6d, 0x3c, + ]); + } + let mut output = Vec::with_capacity(byte_count); + while byte_count > 0 { + let shard = offset / SHARD_SIZE; + let shard_offset = offset % SHARD_SIZE; + let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count); + + let shard_key = key::LARGE_BLOB_SHARDS.start + shard; + if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { + // This request should have been caught at application level. + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let shard_entry = self.store.find(shard_key)?.unwrap_or_default(); + if shard_entry.len() < shard_offset + shard_length { + output.extend(&shard_entry[..]); + return Ok(output); + } + output.extend(&shard_entry[shard_offset..shard_offset + shard_length]); + offset += shard_length; + byte_count -= shard_length; + } + Ok(output) + } + + /// Sets a byte vector as the serialized large blobs array. + pub fn commit_large_blob_array( + &mut self, + large_blob_array: &[u8], + ) -> Result<(), Ctap2StatusCode> { + let mut large_blob_index = 0; + let mut shard_key = key::LARGE_BLOB_SHARDS.start; + while large_blob_index < large_blob_array.len() { + if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); + self.store.insert( + shard_key, + &large_blob_array[large_blob_index..large_blob_index + shard_length], + )?; + large_blob_index += shard_length; + shard_key += 1; + } + // The length is not stored, so overwrite old entries explicitly. + for key in shard_key..key::LARGE_BLOB_SHARDS.end { + // Assuming the store optimizes out unnecessary writes. + self.store.remove(key)?; + } + Ok(()) + } + /// Returns the attestation private key if defined. pub fn attestation_private_key( &self, @@ -1144,6 +1212,147 @@ mod test { assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids); } + #[test] + #[allow(clippy::assertions_on_constants)] + fn test_max_large_blob_array_size() { + assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + } + + #[test] + fn test_commit_get_large_blob_array_1_shard() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0xC0; 1]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + + let large_blob_array = vec![0xC0; SHARD_SIZE]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + } + + #[test] + fn test_commit_get_large_blob_array_2_shards() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0xC0; SHARD_SIZE + 1]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE, 0) + .unwrap(); + assert_eq!( + large_blob_array[..SHARD_SIZE], + restored_large_blob_array[..] + ); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + + let large_blob_array = vec![0xC0; 2 * SHARD_SIZE]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(2 * SHARD_SIZE, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + } + + #[test] + fn test_commit_get_large_blob_array_3_shards() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let mut large_blob_array = vec![0x11; SHARD_SIZE]; + large_blob_array.extend([0x22; SHARD_SIZE].iter()); + large_blob_array.extend([0x33; 1].iter()); + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(3 * SHARD_SIZE, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let shard1 = persistent_store + .get_large_blob_array(SHARD_SIZE, 0) + .unwrap(); + let shard2 = persistent_store + .get_large_blob_array(SHARD_SIZE, SHARD_SIZE) + .unwrap(); + let shard3 = persistent_store + .get_large_blob_array(1, 2 * SHARD_SIZE) + .unwrap(); + assert_eq!(large_blob_array[..SHARD_SIZE], shard1[..]); + assert_eq!(large_blob_array[SHARD_SIZE..2 * SHARD_SIZE], shard2[..]); + assert_eq!(large_blob_array[2 * SHARD_SIZE..], shard3[..]); + let shard12 = persistent_store + .get_large_blob_array(2, SHARD_SIZE - 1) + .unwrap(); + let shard23 = persistent_store + .get_large_blob_array(2, 2 * SHARD_SIZE - 1) + .unwrap(); + assert_eq!(vec![0x11, 0x22], shard12); + assert_eq!(vec![0x22, 0x33], shard23); + } + + #[test] + fn test_commit_get_large_blob_array_overwrite() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0x11; SHARD_SIZE + 1]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let large_blob_array = vec![0x22; SHARD_SIZE]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(1, SHARD_SIZE) + .unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + + assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + let empty_blob_array = vec![ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, + 0x7a, 0x6d, 0x3c, + ]; + assert_eq!(empty_blob_array, restored_large_blob_array); + } + #[test] fn test_global_signature_counter() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 1c0e21e..4f6ba51 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -88,6 +88,11 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; + /// Storage for the serialized large blob array. + /// + /// The stored large blob can be too big for one key, so it has to be sharded. + LARGE_BLOB_SHARDS = 2000..2016; + /// If this entry exists and equals 1, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; From 3517b1163d9e76243923153d743f33c8977cc15f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 13:48:27 +0100 Subject: [PATCH 41/86] bigger shards, fixed get_large_blob --- src/ctap/large_blobs.rs | 4 ++-- src/ctap/storage.rs | 27 +++++++++++++++++---------- src/ctap/storage/key.rs | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index 32934e9..6dc80ce 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -150,8 +150,8 @@ mod test { let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, - 0x7a, 0x6d, 0x3c, + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, ]; let large_blobs_params = AuthenticatorLargeBlobsParameters { get: Some(large_blob.len()), diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a89c02d..9b6ce31 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -60,7 +60,7 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; -const SHARD_SIZE: usize = 128; +const SHARD_SIZE: usize = 1023; pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start); @@ -473,7 +473,8 @@ impl PersistentStore { /// Reads the byte vector stored as the serialized large blobs array. /// - /// If more data is requested than stored, return as many bytes as possible. + /// If too few bytes exist at that offset, return the maximum number + /// available. This includes cases of offset being beyond the stored array. pub fn get_large_blob_array( &self, mut byte_count: usize, @@ -481,24 +482,24 @@ impl PersistentStore { ) -> Result, Ctap2StatusCode> { if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() { return Ok(vec![ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, - 0x7a, 0x6d, 0x3c, + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, ]); } let mut output = Vec::with_capacity(byte_count); while byte_count > 0 { - let shard = offset / SHARD_SIZE; let shard_offset = offset % SHARD_SIZE; let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count); - let shard_key = key::LARGE_BLOB_SHARDS.start + shard; + let shard_key = key::LARGE_BLOB_SHARDS.start + offset / SHARD_SIZE; if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { // This request should have been caught at application level. return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } let shard_entry = self.store.find(shard_key)?.unwrap_or_default(); if shard_entry.len() < shard_offset + shard_length { - output.extend(&shard_entry[..]); + // If fewer bytes exist than requested, return them all. + output.extend(&shard_entry[shard_offset..]); return Ok(output); } output.extend(&shard_entry[shard_offset..shard_offset + shard_length]); @@ -529,7 +530,7 @@ impl PersistentStore { } // The length is not stored, so overwrite old entries explicitly. for key in shard_key..key::LARGE_BLOB_SHARDS.end { - // Assuming the store optimizes out unnecessary writes. + // Assuming the store optimizes out unnecessary removes. self.store.remove(key)?; } Ok(()) @@ -1223,12 +1224,18 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0xC0; 1]; + let large_blob_array = vec![0x01, 0x02, 0x03]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); + assert_eq!(vec![0x01], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 1).unwrap(); + assert_eq!(vec![0x02], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 2).unwrap(); + assert_eq!(vec![0x03], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(2, 2).unwrap(); + assert_eq!(vec![0x03], restored_large_blob_array); let large_blob_array = vec![0xC0; SHARD_SIZE]; assert!(persistent_store diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 4f6ba51..2093685 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -91,7 +91,7 @@ make_partition! { /// Storage for the serialized large blob array. /// /// The stored large blob can be too big for one key, so it has to be sharded. - LARGE_BLOB_SHARDS = 2000..2016; + LARGE_BLOB_SHARDS = 2000..2004; /// If this entry exists and equals 1, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; From cf8b54b39c1f0ab7b5232ba4af8572628b46fc4b Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 14:16:34 +0100 Subject: [PATCH 42/86] large blob commit is one transaction --- src/ctap/storage.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 9b6ce31..0b66f4f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -514,26 +514,25 @@ impl PersistentStore { &mut self, large_blob_array: &[u8], ) -> Result<(), Ctap2StatusCode> { - let mut large_blob_index = 0; - let mut shard_key = key::LARGE_BLOB_SHARDS.start; - while large_blob_index < large_blob_array.len() { - if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + const MIN_SHARD_KEY: usize = key::LARGE_BLOB_SHARDS.start; + const SHARD_COUNT: usize = key::LARGE_BLOB_SHARDS.end - MIN_SHARD_KEY; + let mut transactions = Vec::with_capacity(SHARD_COUNT); + for shard_key in MIN_SHARD_KEY..key::LARGE_BLOB_SHARDS.end { + let large_blob_index = (shard_key - MIN_SHARD_KEY) * SHARD_SIZE; + if large_blob_array.len() > large_blob_index { + let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); + transactions.push(StoreUpdate::Insert { + key: shard_key, + value: &large_blob_array[large_blob_index..large_blob_index + shard_length], + }); + } else { + transactions.push(StoreUpdate::Remove { key: shard_key }); } - let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); - self.store.insert( - shard_key, - &large_blob_array[large_blob_index..large_blob_index + shard_length], - )?; - large_blob_index += shard_length; - shard_key += 1; } - // The length is not stored, so overwrite old entries explicitly. - for key in shard_key..key::LARGE_BLOB_SHARDS.end { - // Assuming the store optimizes out unnecessary removes. - self.store.remove(key)?; - } - Ok(()) + Ok(self.store.transaction(&transactions)?) } /// Returns the attestation private key if defined. From 7d04c5c6d0140115ea5914469f8b09365385ece6 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 14:23:32 +0100 Subject: [PATCH 43/86] fixes const usage in test_get_info --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 95b5b0a..8fa622c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1267,7 +1267,7 @@ mod test { "setMinPINLength" => true, "forcePINChange" => false, }, - 0x05 => 1024, + 0x05 => MAX_MSG_SIZE as u64, 0x06 => cbor_array_vec![vec![1]], 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), 0x08 => CREDENTIAL_ID_SIZE as u64, From 19c089e955547d34ce5857c1661f5f62252e07e7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 18:54:45 +0100 Subject: [PATCH 44/86] improvements to large blob storage --- src/ctap/large_blobs.rs | 2 +- src/ctap/storage.rs | 188 +++++++++++++++++++++++++--------------- 2 files changed, 120 insertions(+), 70 deletions(-) diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index 6dc80ce..ab38df0 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -69,7 +69,7 @@ impl LargeBlobs { if get > MAX_FRAGMENT_LENGTH { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH); } - let config = persistent_store.get_large_blob_array(get, offset)?; + let config = persistent_store.get_large_blob_array(offset, get)?; return Ok(ResponseData::AuthenticatorLargeBlobs(Some( AuthenticatorLargeBlobsResponse { config }, ))); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0b66f4f..934d533 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -60,9 +60,7 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; -const SHARD_SIZE: usize = 1023; -pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = - SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start); +pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048; /// Wrapper for master keys. pub struct MasterKeys { @@ -471,38 +469,55 @@ impl PersistentStore { )?) } + /// The size used for shards of large blobs. + /// + /// This value is constant during the lifetime of the device. + fn shard_size(&self) -> usize { + self.store.max_value_length() + } + /// Reads the byte vector stored as the serialized large blobs array. /// /// If too few bytes exist at that offset, return the maximum number /// available. This includes cases of offset being beyond the stored array. + /// + /// If no large blob is committed to the store, get responds as if an empty + /// CBOR array (0x80) was written, together with the 16 byte prefix of its + /// SHA256, to a total length of 17 byte (which is the shortest legitemate + /// large blob entry possible). pub fn get_large_blob_array( &self, - mut byte_count: usize, mut offset: usize, + mut byte_count: usize, ) -> Result, Ctap2StatusCode> { - if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() { - return Ok(vec![ - 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, - 0x7A, 0x6D, 0x3C, - ]); - } let mut output = Vec::with_capacity(byte_count); while byte_count > 0 { - let shard_offset = offset % SHARD_SIZE; - let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count); - - let shard_key = key::LARGE_BLOB_SHARDS.start + offset / SHARD_SIZE; + let shard_key = key::LARGE_BLOB_SHARDS.start + offset / self.shard_size(); if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { // This request should have been caught at application level. return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - let shard_entry = self.store.find(shard_key)?.unwrap_or_default(); - if shard_entry.len() < shard_offset + shard_length { - // If fewer bytes exist than requested, return them all. - output.extend(&shard_entry[shard_offset..]); - return Ok(output); + let shard_entry = self.store.find(shard_key)?; + let shard_entry = if shard_key == key::LARGE_BLOB_SHARDS.start { + shard_entry.unwrap_or_else(|| { + vec![ + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, + 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, + ] + }) + } else { + shard_entry.unwrap_or_default() + }; + + let shard_offset = offset % self.shard_size(); + if shard_entry.len() < shard_offset { + break; + } + let shard_length = cmp::min(shard_entry.len() - shard_offset, byte_count); + output.extend(&shard_entry[shard_offset..][..shard_length]); + if shard_entry.len() < self.shard_size() { + break; } - output.extend(&shard_entry[shard_offset..shard_offset + shard_length]); offset += shard_length; byte_count -= shard_length; } @@ -517,22 +532,18 @@ impl PersistentStore { if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - const MIN_SHARD_KEY: usize = key::LARGE_BLOB_SHARDS.start; - const SHARD_COUNT: usize = key::LARGE_BLOB_SHARDS.end - MIN_SHARD_KEY; - let mut transactions = Vec::with_capacity(SHARD_COUNT); - for shard_key in MIN_SHARD_KEY..key::LARGE_BLOB_SHARDS.end { - let large_blob_index = (shard_key - MIN_SHARD_KEY) * SHARD_SIZE; - if large_blob_array.len() > large_blob_index { - let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); - transactions.push(StoreUpdate::Insert { - key: shard_key, - value: &large_blob_array[large_blob_index..large_blob_index + shard_length], - }); - } else { - transactions.push(StoreUpdate::Remove { key: shard_key }); - } + + let mut shards = large_blob_array.chunks(self.shard_size()); + let mut updates = Vec::with_capacity(shards.len()); + for key in key::LARGE_BLOB_SHARDS { + let update = match shards.next() { + Some(value) => StoreUpdate::Insert { key, value }, + None if self.store.find(key)?.is_some() => StoreUpdate::Remove { key }, + _ => break, + }; + updates.push(update); } - Ok(self.store.transaction(&transactions)?) + Ok(self.store.transaction(&updates)?) } /// Returns the attestation private key if defined. @@ -1213,9 +1224,19 @@ mod test { } #[test] - #[allow(clippy::assertions_on_constants)] fn test_max_large_blob_array_size() { - assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + + #[allow(clippy::assertions_on_constants)] + { + assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + } + assert!( + MAX_LARGE_BLOB_ARRAY_SIZE + <= persistent_store.shard_size() + * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start) + ); } #[test] @@ -1227,25 +1248,29 @@ mod test { assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); assert_eq!(vec![0x01], restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(1, 1).unwrap(); assert_eq!(vec![0x02], restored_large_blob_array); - let restored_large_blob_array = persistent_store.get_large_blob_array(1, 2).unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(2, 1).unwrap(); assert_eq!(vec![0x03], restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(2, 2).unwrap(); assert_eq!(vec![0x03], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(3, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); - let large_blob_array = vec![0xC0; SHARD_SIZE]; + let large_blob_array = vec![0xC0; persistent_store.shard_size()]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE, 0) + .get_large_blob_array(0, persistent_store.shard_size()) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); } @@ -1255,32 +1280,32 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0xC0; SHARD_SIZE + 1]; + let large_blob_array = vec![0xC0; persistent_store.shard_size() + 1]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE, 0) + .get_large_blob_array(0, persistent_store.shard_size()) .unwrap(); assert_eq!( - large_blob_array[..SHARD_SIZE], + large_blob_array[..persistent_store.shard_size()], restored_large_blob_array[..] ); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); - let large_blob_array = vec![0xC0; 2 * SHARD_SIZE]; + let large_blob_array = vec![0xC0; 2 * persistent_store.shard_size()]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(2 * SHARD_SIZE, 0) + .get_large_blob_array(0, 2 * persistent_store.shard_size()) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); } @@ -1290,37 +1315,46 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut large_blob_array = vec![0x11; SHARD_SIZE]; - large_blob_array.extend([0x22; SHARD_SIZE].iter()); - large_blob_array.extend([0x33; 1].iter()); + let mut large_blob_array = vec![0x11; persistent_store.shard_size()]; + large_blob_array.extend(vec![0x22; persistent_store.shard_size()]); + large_blob_array.extend(&[0x33; 1]); assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(3 * SHARD_SIZE, 0) + .get_large_blob_array(0, 3 * persistent_store.shard_size()) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let shard1 = persistent_store - .get_large_blob_array(SHARD_SIZE, 0) + .get_large_blob_array(0, persistent_store.shard_size()) .unwrap(); let shard2 = persistent_store - .get_large_blob_array(SHARD_SIZE, SHARD_SIZE) + .get_large_blob_array(persistent_store.shard_size(), persistent_store.shard_size()) .unwrap(); let shard3 = persistent_store - .get_large_blob_array(1, 2 * SHARD_SIZE) + .get_large_blob_array(2 * persistent_store.shard_size(), 1) .unwrap(); - assert_eq!(large_blob_array[..SHARD_SIZE], shard1[..]); - assert_eq!(large_blob_array[SHARD_SIZE..2 * SHARD_SIZE], shard2[..]); - assert_eq!(large_blob_array[2 * SHARD_SIZE..], shard3[..]); + assert_eq!( + large_blob_array[..persistent_store.shard_size()], + shard1[..] + ); + assert_eq!( + large_blob_array[persistent_store.shard_size()..2 * persistent_store.shard_size()], + shard2[..] + ); + assert_eq!( + large_blob_array[2 * persistent_store.shard_size()..], + shard3[..] + ); let shard12 = persistent_store - .get_large_blob_array(2, SHARD_SIZE - 1) + .get_large_blob_array(persistent_store.shard_size() - 1, 2) .unwrap(); let shard23 = persistent_store - .get_large_blob_array(2, 2 * SHARD_SIZE - 1) + .get_large_blob_array(2 * persistent_store.shard_size() - 1, 2) .unwrap(); assert_eq!(vec![0x11, 0x22], shard12); assert_eq!(vec![0x22, 0x33], shard23); @@ -1331,32 +1365,48 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0x11; SHARD_SIZE + 1]; + let large_blob_array = vec![0x11; persistent_store.shard_size() + 1]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let large_blob_array = vec![0x22; SHARD_SIZE]; + let large_blob_array = vec![0x22; persistent_store.shard_size()]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(1, SHARD_SIZE) + .get_large_blob_array(persistent_store.shard_size(), 1) .unwrap(); assert_eq!(Vec::::new(), restored_large_blob_array); assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); + // Committing an empty array resets to the default blob of 17 byte. + assert_eq!(restored_large_blob_array.len(), 17); + } + + #[test] + fn test_commit_get_large_blob_array_no_commit() { + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + let empty_blob_array = vec![ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, - 0x7a, 0x6d, 0x3c, + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, ]; + let restored_large_blob_array = persistent_store + .get_large_blob_array(0, persistent_store.shard_size()) + .unwrap(); assert_eq!(empty_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); + assert_eq!(vec![0x80], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(16, 1).unwrap(); + assert_eq!(vec![0x3C], restored_large_blob_array); } #[test] From f0c51950cb92be099e76ad51b2c8758464399a2b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Fri, 22 Jan 2021 19:19:52 +0100 Subject: [PATCH 45/86] Add fragmentation support --- libraries/persistent_store/src/fragment.rs | 179 ++++++++++++++++++ libraries/persistent_store/src/lib.rs | 1 + libraries/persistent_store/src/store.rs | 22 ++- libraries/persistent_store/tests/config.rs | 49 +++++ libraries/persistent_store/tests/fragment.rs | 188 +++++++++++++++++++ 5 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 libraries/persistent_store/src/fragment.rs create mode 100644 libraries/persistent_store/tests/config.rs create mode 100644 libraries/persistent_store/tests/fragment.rs diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs new file mode 100644 index 0000000..5bab46f --- /dev/null +++ b/libraries/persistent_store/src/fragment.rs @@ -0,0 +1,179 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helper functions for fragmented entries. +//! +//! This module permits to handle entries larger than the [maximum value +//! length](Store::max_value_length) by storing ordered consecutive fragments in a sequence of keys. +//! The first keys hold fragments of maximal length, followed by a possibly partial fragment. The +//! remaining keys are not used. + +use crate::{Storage, Store, StoreError, StoreHandle, StoreResult, StoreUpdate}; +use alloc::vec::Vec; +use core::ops::Range; + +/// Represents a sequence of keys. +pub trait Keys { + /// Returns the number of keys. + fn len(&self) -> usize; + + /// Returns the position of a key in the sequence. + fn pos(&self, key: usize) -> Option; + + /// Returns the key of a position in the sequence. + /// + /// # Preconditions + /// + /// The position must be within the length: `pos < len()`. + fn key(&self, pos: usize) -> usize; +} + +impl Keys for Range { + fn len(&self) -> usize { + self.end - self.start + } + + fn pos(&self, key: usize) -> Option { + if self.start <= key && key < self.end { + Some(key - self.start) + } else { + None + } + } + + fn key(&self, pos: usize) -> usize { + debug_assert!(pos < Keys::len(self)); + self.start + pos + } +} + +/// Reads the concatenated value of a sequence of keys. +pub fn read(store: &Store, keys: &impl Keys) -> StoreResult>> { + let handles = get_handles(store, keys)?; + if handles.is_empty() { + return Ok(None); + } + let mut result = Vec::with_capacity(handles.len() * store.max_value_length()); + for handle in handles { + result.extend(handle.get_value(store)?); + } + Ok(Some(result)) +} + +/// Reads a range from the concatenated value of a sequence of keys. +/// +/// This is equivalent to calling [`read`] then taking the range except that: +/// - Only the needed chunks are read. +/// - The range is truncated to fit in the value. +pub fn read_range( + store: &Store, + keys: &impl Keys, + range: Range, +) -> StoreResult>> { + let range_len = match range.end.checked_sub(range.start) { + None => return Err(StoreError::InvalidArgument), + Some(x) => x, + }; + let handles = get_handles(store, keys)?; + if handles.is_empty() { + return Ok(None); + } + let mut result = Vec::with_capacity(range_len); + let mut offset = 0; + for handle in handles { + let start = range.start.saturating_sub(offset); + let length = handle.get_length(store)?; + let end = core::cmp::min(range.end.saturating_sub(offset), length); + offset += length; + if start < end && end <= length { + result.extend(&handle.get_value(store)?[start..end]); + } + } + Ok(Some(result)) +} + +/// Writes a value to a sequence of keys as chunks. +pub fn write(store: &mut Store, keys: &impl Keys, value: &[u8]) -> StoreResult<()> { + let handles = get_handles(store, keys)?; + let keys_len = keys.len(); + let mut updates = Vec::with_capacity(keys_len); + let mut chunks = value.chunks(store.max_value_length()); + for pos in 0..keys_len { + let key = keys.key(pos); + match (handles.get(pos), chunks.next()) { + // No existing handle and no new chunk: nothing to do. + (None, None) => (), + // Existing handle and no new chunk: remove old handle. + (Some(_), None) => updates.push(StoreUpdate::Remove { key }), + // Existing handle with same value as new chunk: nothing to do. + (Some(handle), Some(value)) if handle.get_value(store)? == value => (), + // New chunk: Write (or overwrite) the new value. + (_, Some(value)) => updates.push(StoreUpdate::Insert { key, value }), + } + } + if chunks.next().is_some() { + // The value is too long. + return Err(StoreError::InvalidArgument); + } + store.transaction(&updates) +} + +/// Deletes the value of a sequence of keys. +pub fn delete(store: &mut Store, keys: &impl Keys) -> StoreResult<()> { + let updates: Vec>> = get_handles(store, keys)? + .iter() + .map(|handle| StoreUpdate::Remove { + key: handle.get_key(), + }) + .collect(); + store.transaction(&updates) +} + +/// Returns the handles of a sequence of keys. +/// +/// The handles are truncated to the keys that are present. +fn get_handles(store: &Store, keys: &impl Keys) -> StoreResult> { + let keys_len = keys.len(); + let mut handles: Vec> = vec![None; keys_len as usize]; + for handle in store.iter()? { + let handle = handle?; + let pos = match keys.pos(handle.get_key()) { + Some(pos) => pos, + None => continue, + }; + if pos >= keys_len { + return Err(StoreError::InvalidArgument); + } + if let Some(old_handle) = &handles[pos] { + if old_handle.get_key() != handle.get_key() { + // The user provided a non-injective `pos` function. + return Err(StoreError::InvalidArgument); + } else { + return Err(StoreError::InvalidStorage); + } + } + handles[pos] = Some(handle); + } + let num_handles = handles.iter().filter(|x| x.is_some()).count(); + let mut result = Vec::with_capacity(num_handles); + for (i, handle) in handles.into_iter().enumerate() { + match (i < num_handles, handle) { + (true, Some(handle)) => result.push(handle), + (false, None) => (), + // We should have `num_handles` Somes followed by Nones. + _ => return Err(StoreError::InvalidStorage), + } + } + Ok(result) +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 06a3a68..41acbaf 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -354,6 +354,7 @@ mod buffer; #[cfg(feature = "std")] mod driver; mod format; +pub mod fragment; #[cfg(feature = "std")] mod model; mod storage; diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index 224eeb9..2630950 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -100,7 +100,7 @@ pub type StoreResult = Result; /// /// [capacity]: struct.Store.html#method.capacity /// [lifetime]: struct.Store.html#method.lifetime -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct StoreRatio { /// How much of the metric is used. pub(crate) used: Nat, @@ -148,6 +148,15 @@ impl StoreHandle { self.key as usize } + /// Returns the length of value of the entry. + /// + /// # Errors + /// + /// Returns `InvalidArgument` if the entry has been deleted or compacted. + pub fn get_length(&self, store: &Store) -> StoreResult { + store.get_length(self) + } + /// Returns the value of the entry. /// /// # Errors @@ -446,6 +455,17 @@ impl Store { self.format.max_value_len() as usize } + /// Returns the length of the value of an entry given its handle. + fn get_length(&self, handle: &StoreHandle) -> StoreResult { + self.check_handle(handle)?; + let mut pos = handle.pos; + match self.parse_entry(&mut pos)? { + ParsedEntry::User(header) => Ok(header.length as usize), + ParsedEntry::Padding => Err(StoreError::InvalidArgument), + _ => Err(StoreError::InvalidStorage), + } + } + /// Returns the value of an entry given its handle. fn get_value(&self, handle: &StoreHandle) -> StoreResult> { self.check_handle(handle)?; diff --git a/libraries/persistent_store/tests/config.rs b/libraries/persistent_store/tests/config.rs new file mode 100644 index 0000000..8812743 --- /dev/null +++ b/libraries/persistent_store/tests/config.rs @@ -0,0 +1,49 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use persistent_store::{BufferOptions, BufferStorage, Store, StoreDriverOff}; + +#[derive(Clone)] +pub struct Config { + word_size: usize, + page_size: usize, + num_pages: usize, + max_word_writes: usize, + max_page_erases: usize, +} + +impl Config { + pub fn new_driver(&self) -> StoreDriverOff { + let options = BufferOptions { + word_size: self.word_size, + page_size: self.page_size, + max_word_writes: self.max_word_writes, + max_page_erases: self.max_page_erases, + strict_mode: true, + }; + StoreDriverOff::new(options, self.num_pages) + } + + pub fn new_store(&self) -> Store { + self.new_driver().power_on().unwrap().extract_store() + } +} + +pub const MINIMAL: Config = Config { + word_size: 4, + page_size: 64, + num_pages: 5, + max_word_writes: 2, + max_page_erases: 9, +}; diff --git a/libraries/persistent_store/tests/fragment.rs b/libraries/persistent_store/tests/fragment.rs new file mode 100644 index 0000000..fd045bc --- /dev/null +++ b/libraries/persistent_store/tests/fragment.rs @@ -0,0 +1,188 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use persistent_store::fragment; + +mod config; + +#[test] +fn read_empty_entry() { + let store = config::MINIMAL.new_store(); + assert_eq!(fragment::read(&store, &(0..4)), Ok(None)); +} + +#[test] +fn read_single_chunk() { + let mut store = config::MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(store.insert(0, &value), Ok(())); + assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); +} + +#[test] +fn read_multiple_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); +} + +#[test] +fn read_range_first_chunk() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 0..10), + Ok(Some((0..10).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 10..20), + Ok(Some((10..20).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 40..52), + Ok(Some((40..52).collect())) + ); +} + +#[test] +fn read_range_second_chunk() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 52..53), + Ok(Some(vec![52])) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 53..54), + Ok(Some(vec![53])) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 59..60), + Ok(Some(vec![59])) + ); +} + +#[test] +fn read_range_both_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 40..60), + Ok(Some((40..60).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 0..60), + Ok(Some((0..60).collect())) + ); +} + +#[test] +fn read_range_outside() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 40..100), + Ok(Some((40..60).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 60..100), + Ok(Some(vec![])) + ); +} + +#[test] +fn write_single_chunk() { + let mut store = config::MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some(value))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn write_multiple_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((0..52).collect()))); + assert_eq!(store.find(1), Ok(Some((52..60).collect()))); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn overwrite_less_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + let value: Vec<_> = (42..69).collect(); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((42..69).collect()))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn overwrite_needed_chunks() { + let mut store = config::MINIMAL.new_store(); + let mut value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + // Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data. + let mut lifetime = 2 + 60 / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); + // Update the value. + value.extend(60..80); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + // Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data. + lifetime += 1 + (80 - 52) / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); +} + +#[test] +fn delete_empty() { + let mut store = config::MINIMAL.new_store(); + assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn delete_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} From 41a3f512c81b074b1e2ffb5bb6ceb1cfd12ac0b5 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 25 Jan 2021 11:31:42 +0100 Subject: [PATCH 46/86] Remove useless check --- libraries/persistent_store/src/fragment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index 5bab46f..73b6d29 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -96,7 +96,7 @@ pub fn read_range( let length = handle.get_length(store)?; let end = core::cmp::min(range.end.saturating_sub(offset), length); offset += length; - if start < end && end <= length { + if start < end { result.extend(&handle.get_value(store)?[start..end]); } } From 0e537733f12314b7edf3a4113bdf2bacd3ae4575 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 25 Jan 2021 17:04:01 +0100 Subject: [PATCH 47/86] Improve count_credentials by not deserializing them --- src/ctap/storage.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b85f918..c5182bc 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -268,11 +268,11 @@ impl PersistentStore { /// Returns the number of credentials. pub fn count_credentials(&self) -> Result { - let mut iter_result = Ok(()); - let iter = self.iter_credentials(&mut iter_result)?; - let result = iter.count(); - iter_result?; - Ok(result) + let mut count = 0; + for handle in self.store.iter()? { + count += key::CREDENTIALS.contains(&handle?.get_key()) as usize; + } + Ok(count) } /// Returns the estimated number of credentials that can still be stored. From ae0156d2876871fa13b3ee6b41717767c3960cc2 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 25 Jan 2021 17:30:50 +0100 Subject: [PATCH 48/86] Factor test tools between store and fragment Those need the driver to deal with the fact that the store is stateful. Those tests can't be moved to the test suite because they use private functions. --- libraries/persistent_store/src/fragment.rs | 165 +++++++++++++++ libraries/persistent_store/src/lib.rs | 4 +- libraries/persistent_store/src/store.rs | 70 +------ .../{tests/config.rs => src/test.rs} | 37 +++- libraries/persistent_store/tests/fragment.rs | 188 ------------------ 5 files changed, 211 insertions(+), 253 deletions(-) rename libraries/persistent_store/{tests/config.rs => src/test.rs} (60%) delete mode 100644 libraries/persistent_store/tests/fragment.rs diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index 73b6d29..9aec377 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -177,3 +177,168 @@ fn get_handles(store: &Store, keys: &impl Keys) -> StoreResult = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(read(&store, &(0..4)), Ok(Some(value))); + } + + #[test] + fn read_range_first_chunk() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + read_range(&store, &(0..4), 0..10), + Ok(Some((0..10).collect())) + ); + assert_eq!( + read_range(&store, &(0..4), 10..20), + Ok(Some((10..20).collect())) + ); + assert_eq!( + read_range(&store, &(0..4), 40..52), + Ok(Some((40..52).collect())) + ); + } + + #[test] + fn read_range_second_chunk() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(read_range(&store, &(0..4), 52..53), Ok(Some(vec![52]))); + assert_eq!(read_range(&store, &(0..4), 53..54), Ok(Some(vec![53]))); + assert_eq!(read_range(&store, &(0..4), 59..60), Ok(Some(vec![59]))); + } + + #[test] + fn read_range_both_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + read_range(&store, &(0..4), 40..60), + Ok(Some((40..60).collect())) + ); + assert_eq!( + read_range(&store, &(0..4), 0..60), + Ok(Some((0..60).collect())) + ); + } + + #[test] + fn read_range_outside() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + read_range(&store, &(0..4), 40..100), + Ok(Some((40..60).collect())) + ); + assert_eq!(read_range(&store, &(0..4), 60..100), Ok(Some(vec![]))); + } + + #[test] + fn write_single_chunk() { + let mut store = MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some(value))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn write_multiple_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((0..52).collect()))); + assert_eq!(store.find(1), Ok(Some((52..60).collect()))); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn overwrite_less_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + let value: Vec<_> = (42..69).collect(); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((42..69).collect()))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn overwrite_needed_chunks() { + let mut store = MINIMAL.new_store(); + let mut value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + // Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data. + let mut lifetime = 2 + 60 / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); + // Update the value. + value.extend(60..80); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + // Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data. + lifetime += 1 + (80 - 52) / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); + } + + #[test] + fn delete_empty() { + let mut store = MINIMAL.new_store(); + assert_eq!(delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn delete_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 41acbaf..a8adc2b 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -359,6 +359,8 @@ pub mod fragment; mod model; mod storage; mod store; +#[cfg(test)] +mod test; #[cfg(feature = "std")] pub use self::buffer::{BufferCorruptFunction, BufferOptions, BufferStorage}; diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index 2630950..f19f463 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -1301,71 +1301,15 @@ fn is_write_needed(source: &[u8], target: &[u8]) -> StoreResult { #[cfg(test)] mod tests { use super::*; - use crate::BufferOptions; - - #[derive(Clone)] - struct Config { - word_size: usize, - page_size: usize, - num_pages: usize, - max_word_writes: usize, - max_page_erases: usize, - } - - impl Config { - fn new_driver(&self) -> StoreDriverOff { - let options = BufferOptions { - word_size: self.word_size, - page_size: self.page_size, - max_word_writes: self.max_word_writes, - max_page_erases: self.max_page_erases, - strict_mode: true, - }; - StoreDriverOff::new(options, self.num_pages) - } - } - - const MINIMAL: Config = Config { - word_size: 4, - page_size: 64, - num_pages: 5, - max_word_writes: 2, - max_page_erases: 9, - }; - - const NORDIC: Config = Config { - word_size: 4, - page_size: 0x1000, - num_pages: 20, - max_word_writes: 2, - max_page_erases: 10000, - }; - - const TITAN: Config = Config { - word_size: 4, - page_size: 0x800, - num_pages: 10, - max_word_writes: 2, - max_page_erases: 10000, - }; + use crate::test::MINIMAL; #[test] - fn nordic_capacity() { - let driver = NORDIC.new_driver().power_on().unwrap(); - assert_eq!(driver.model().capacity().total, 19123); - } - - #[test] - fn titan_capacity() { - let driver = TITAN.new_driver().power_on().unwrap(); - assert_eq!(driver.model().capacity().total, 4315); - } - - #[test] - fn minimal_virt_page_size() { - // Make sure a virtual page has 14 words. We use this property in the other tests below to - // know whether entries are spanning, starting, and ending pages. - assert_eq!(MINIMAL.new_driver().model().format().virt_page_size(), 14); + fn is_write_needed_ok() { + assert_eq!(is_write_needed(&[], &[]), Ok(false)); + assert_eq!(is_write_needed(&[0], &[0]), Ok(false)); + assert_eq!(is_write_needed(&[0], &[1]), Err(StoreError::InvalidStorage)); + assert_eq!(is_write_needed(&[1], &[0]), Ok(true)); + assert_eq!(is_write_needed(&[1], &[1]), Ok(false)); } #[test] diff --git a/libraries/persistent_store/tests/config.rs b/libraries/persistent_store/src/test.rs similarity index 60% rename from libraries/persistent_store/tests/config.rs rename to libraries/persistent_store/src/test.rs index 8812743..2d20574 100644 --- a/libraries/persistent_store/tests/config.rs +++ b/libraries/persistent_store/src/test.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use persistent_store::{BufferOptions, BufferStorage, Store, StoreDriverOff}; +use crate::{BufferOptions, BufferStorage, Store, StoreDriverOff}; #[derive(Clone)] pub struct Config { @@ -47,3 +47,38 @@ pub const MINIMAL: Config = Config { max_word_writes: 2, max_page_erases: 9, }; + +const NORDIC: Config = Config { + word_size: 4, + page_size: 0x1000, + num_pages: 20, + max_word_writes: 2, + max_page_erases: 10000, +}; + +const TITAN: Config = Config { + word_size: 4, + page_size: 0x800, + num_pages: 10, + max_word_writes: 2, + max_page_erases: 10000, +}; + +#[test] +fn nordic_capacity() { + let driver = NORDIC.new_driver().power_on().unwrap(); + assert_eq!(driver.model().capacity().total, 19123); +} + +#[test] +fn titan_capacity() { + let driver = TITAN.new_driver().power_on().unwrap(); + assert_eq!(driver.model().capacity().total, 4315); +} + +#[test] +fn minimal_virt_page_size() { + // Make sure a virtual page has 14 words. We use this property in the other tests below to + // know whether entries are spanning, starting, and ending pages. + assert_eq!(MINIMAL.new_driver().model().format().virt_page_size(), 14); +} diff --git a/libraries/persistent_store/tests/fragment.rs b/libraries/persistent_store/tests/fragment.rs deleted file mode 100644 index fd045bc..0000000 --- a/libraries/persistent_store/tests/fragment.rs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use persistent_store::fragment; - -mod config; - -#[test] -fn read_empty_entry() { - let store = config::MINIMAL.new_store(); - assert_eq!(fragment::read(&store, &(0..4)), Ok(None)); -} - -#[test] -fn read_single_chunk() { - let mut store = config::MINIMAL.new_store(); - let value = b"hello".to_vec(); - assert_eq!(store.insert(0, &value), Ok(())); - assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); -} - -#[test] -fn read_multiple_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); -} - -#[test] -fn read_range_first_chunk() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 0..10), - Ok(Some((0..10).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 10..20), - Ok(Some((10..20).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 40..52), - Ok(Some((40..52).collect())) - ); -} - -#[test] -fn read_range_second_chunk() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 52..53), - Ok(Some(vec![52])) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 53..54), - Ok(Some(vec![53])) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 59..60), - Ok(Some(vec![59])) - ); -} - -#[test] -fn read_range_both_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 40..60), - Ok(Some((40..60).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 0..60), - Ok(Some((0..60).collect())) - ); -} - -#[test] -fn read_range_outside() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 40..100), - Ok(Some((40..60).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 60..100), - Ok(Some(vec![])) - ); -} - -#[test] -fn write_single_chunk() { - let mut store = config::MINIMAL.new_store(); - let value = b"hello".to_vec(); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - assert_eq!(store.find(0), Ok(Some(value))); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn write_multiple_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - assert_eq!(store.find(0), Ok(Some((0..52).collect()))); - assert_eq!(store.find(1), Ok(Some((52..60).collect()))); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn overwrite_less_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - let value: Vec<_> = (42..69).collect(); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - assert_eq!(store.find(0), Ok(Some((42..69).collect()))); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn overwrite_needed_chunks() { - let mut store = config::MINIMAL.new_store(); - let mut value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - // Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data. - let mut lifetime = 2 + 60 / 4; - assert_eq!(store.lifetime().unwrap().used(), lifetime); - // Update the value. - value.extend(60..80); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - // Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data. - lifetime += 1 + (80 - 52) / 4; - assert_eq!(store.lifetime().unwrap().used(), lifetime); -} - -#[test] -fn delete_empty() { - let mut store = config::MINIMAL.new_store(); - assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); - assert_eq!(store.find(0), Ok(None)); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn delete_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); - assert_eq!(store.find(0), Ok(None)); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} From 563f35184ac8e97849b677f4d4f026e849329aa3 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 17:50:01 +0100 Subject: [PATCH 49/86] use new store fragments --- src/ctap/storage.rs | 79 ++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 934d533..dab6620 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -32,6 +32,7 @@ use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; use persistent_store::StoreUpdate; +use persistent_store::fragment::{read_range, write}; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -469,13 +470,6 @@ impl PersistentStore { )?) } - /// The size used for shards of large blobs. - /// - /// This value is constant during the lifetime of the device. - fn shard_size(&self) -> usize { - self.store.max_value_length() - } - /// Reads the byte vector stored as the serialized large blobs array. /// /// If too few bytes exist at that offset, return the maximum number @@ -483,45 +477,23 @@ impl PersistentStore { /// /// If no large blob is committed to the store, get responds as if an empty /// CBOR array (0x80) was written, together with the 16 byte prefix of its - /// SHA256, to a total length of 17 byte (which is the shortest legitemate + /// SHA256, to a total length of 17 byte (which is the shortest legitimate /// large blob entry possible). pub fn get_large_blob_array( &self, - mut offset: usize, - mut byte_count: usize, + offset: usize, + byte_count: usize, ) -> Result, Ctap2StatusCode> { - let mut output = Vec::with_capacity(byte_count); - while byte_count > 0 { - let shard_key = key::LARGE_BLOB_SHARDS.start + offset / self.shard_size(); - if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { - // This request should have been caught at application level. - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - let shard_entry = self.store.find(shard_key)?; - let shard_entry = if shard_key == key::LARGE_BLOB_SHARDS.start { - shard_entry.unwrap_or_else(|| { - vec![ - 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, - 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, - ] - }) - } else { - shard_entry.unwrap_or_default() - }; - - let shard_offset = offset % self.shard_size(); - if shard_entry.len() < shard_offset { - break; - } - let shard_length = cmp::min(shard_entry.len() - shard_offset, byte_count); - output.extend(&shard_entry[shard_offset..][..shard_length]); - if shard_entry.len() < self.shard_size() { - break; - } - offset += shard_length; - byte_count -= shard_length; - } - Ok(output) + let byte_range = offset..offset + byte_count; + let output = read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; + Ok(output.unwrap_or_else(|| { + let empty_large_blob = vec![ + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, + ]; + let last_index = cmp::min(empty_large_blob.len(), offset + byte_count); + empty_large_blob.get(offset..last_index).unwrap_or_default().to_vec() + })) } /// Sets a byte vector as the serialized large blobs array. @@ -529,21 +501,11 @@ impl PersistentStore { &mut self, large_blob_array: &[u8], ) -> Result<(), Ctap2StatusCode> { + // This input should have been caught at caller level. if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - - let mut shards = large_blob_array.chunks(self.shard_size()); - let mut updates = Vec::with_capacity(shards.len()); - for key in key::LARGE_BLOB_SHARDS { - let update = match shards.next() { - Some(value) => StoreUpdate::Insert { key, value }, - None if self.store.find(key)?.is_some() => StoreUpdate::Remove { key }, - _ => break, - }; - updates.push(update); - } - Ok(self.store.transaction(&updates)?) + Ok(write(&mut self.store, &key::LARGE_BLOB_SHARDS, large_blob_array)?) } /// Returns the attestation private key if defined. @@ -642,6 +604,15 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + + /// The size used for shards of large blobs. + /// + /// This value is constant during the lifetime of the device. + #[cfg(test)] + fn shard_size(&self) -> usize { + self.store.max_value_length() + } + } impl From for Ctap2StatusCode { From 4f3c773b15ccc6f7f04bc871b0e760dbb016aab2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 18:08:48 +0100 Subject: [PATCH 50/86] formats code, clippy --- libraries/persistent_store/src/fragment.rs | 1 + src/ctap/storage.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index 73b6d29..851e3d2 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -24,6 +24,7 @@ use alloc::vec::Vec; use core::ops::Range; /// Represents a sequence of keys. +#[allow(clippy::len_without_is_empty)] pub trait Keys { /// Returns the number of keys. fn len(&self) -> usize; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index dab6620..0408041 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -31,8 +31,8 @@ use cbor::cbor_array_vec; use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; -use persistent_store::StoreUpdate; use persistent_store::fragment::{read_range, write}; +use persistent_store::StoreUpdate; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -492,7 +492,10 @@ impl PersistentStore { 0x7A, 0x6D, 0x3C, ]; let last_index = cmp::min(empty_large_blob.len(), offset + byte_count); - empty_large_blob.get(offset..last_index).unwrap_or_default().to_vec() + empty_large_blob + .get(offset..last_index) + .unwrap_or_default() + .to_vec() })) } @@ -505,7 +508,11 @@ impl PersistentStore { if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(write(&mut self.store, &key::LARGE_BLOB_SHARDS, large_blob_array)?) + Ok(write( + &mut self.store, + &key::LARGE_BLOB_SHARDS, + large_blob_array, + )?) } /// Returns the attestation private key if defined. @@ -612,7 +619,6 @@ impl PersistentStore { fn shard_size(&self) -> usize { self.store.max_value_length() } - } impl From for Ctap2StatusCode { From 2af85ad9d0eebe8ef11d837b935f9d7739e1f9ea Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 18:29:38 +0100 Subject: [PATCH 51/86] style fix --- src/ctap/storage.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 7fdf50c..6f75461 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -31,8 +31,7 @@ use cbor::cbor_array_vec; use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; -use persistent_store::fragment::{read_range, write}; -use persistent_store::StoreUpdate; +use persistent_store::{fragment, StoreUpdate}; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -485,14 +484,14 @@ impl PersistentStore { byte_count: usize, ) -> Result, Ctap2StatusCode> { let byte_range = offset..offset + byte_count; - let output = read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; + let output = fragment::read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; Ok(output.unwrap_or_else(|| { - let empty_large_blob = vec![ + const EMPTY_LARGE_BLOB: [u8; 17] = [ 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, ]; - let last_index = cmp::min(empty_large_blob.len(), offset + byte_count); - empty_large_blob + let last_index = cmp::min(EMPTY_LARGE_BLOB.len(), offset + byte_count); + EMPTY_LARGE_BLOB .get(offset..last_index) .unwrap_or_default() .to_vec() @@ -508,7 +507,7 @@ impl PersistentStore { if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(write( + Ok(fragment::write( &mut self.store, &key::LARGE_BLOB_SHARDS, large_blob_array, From 769a2ae1c543a731a3daae1e386de72d842ff019 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 18:43:51 +0100 Subject: [PATCH 52/86] reduce testing to not account for shard size --- src/ctap/storage.rs | 130 +++----------------------------------------- 1 file changed, 8 insertions(+), 122 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 6f75461..43a00c7 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,14 +610,6 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } - - /// The size used for shards of large blobs. - /// - /// This value is constant during the lifetime of the device. - #[cfg(test)] - fn shard_size(&self) -> usize { - self.store.max_value_length() - } } impl From for Ctap2StatusCode { @@ -1210,13 +1202,13 @@ mod test { } assert!( MAX_LARGE_BLOB_ARRAY_SIZE - <= persistent_store.shard_size() + <= persistent_store.store.max_value_length() * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start) ); } #[test] - fn test_commit_get_large_blob_array_1_shard() { + fn test_commit_get_large_blob_array() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -1236,104 +1228,6 @@ mod test { assert_eq!(Vec::::new(), restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); assert_eq!(Vec::::new(), restored_large_blob_array); - - let large_blob_array = vec![0xC0; persistent_store.shard_size()]; - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - } - - #[test] - fn test_commit_get_large_blob_array_2_shards() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - - let large_blob_array = vec![0xC0; persistent_store.shard_size() + 1]; - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); - assert_eq!( - large_blob_array[..persistent_store.shard_size()], - restored_large_blob_array[..] - ); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - - let large_blob_array = vec![0xC0; 2 * persistent_store.shard_size()]; - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 2 * persistent_store.shard_size()) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - } - - #[test] - fn test_commit_get_large_blob_array_3_shards() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - - let mut large_blob_array = vec![0x11; persistent_store.shard_size()]; - large_blob_array.extend(vec![0x22; persistent_store.shard_size()]); - large_blob_array.extend(&[0x33; 1]); - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 3 * persistent_store.shard_size()) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let shard1 = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); - let shard2 = persistent_store - .get_large_blob_array(persistent_store.shard_size(), persistent_store.shard_size()) - .unwrap(); - let shard3 = persistent_store - .get_large_blob_array(2 * persistent_store.shard_size(), 1) - .unwrap(); - assert_eq!( - large_blob_array[..persistent_store.shard_size()], - shard1[..] - ); - assert_eq!( - large_blob_array[persistent_store.shard_size()..2 * persistent_store.shard_size()], - shard2[..] - ); - assert_eq!( - large_blob_array[2 * persistent_store.shard_size()..], - shard3[..] - ); - let shard12 = persistent_store - .get_large_blob_array(persistent_store.shard_size() - 1, 2) - .unwrap(); - let shard23 = persistent_store - .get_large_blob_array(2 * persistent_store.shard_size() - 1, 2) - .unwrap(); - assert_eq!(vec![0x11, 0x22], shard12); - assert_eq!(vec![0x22, 0x33], shard23); } #[test] @@ -1341,27 +1235,21 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0x11; persistent_store.shard_size() + 1]; + let large_blob_array = vec![0x11; 5]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let large_blob_array = vec![0x22; persistent_store.shard_size()]; + let large_blob_array = vec![0x22; 4]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 5).unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(persistent_store.shard_size(), 1) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); assert_eq!(Vec::::new(), restored_large_blob_array); assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 20).unwrap(); // Committing an empty array resets to the default blob of 17 byte. assert_eq!(restored_large_blob_array.len(), 17); } @@ -1375,9 +1263,7 @@ mod test { 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, ]; - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 17).unwrap(); assert_eq!(empty_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); assert_eq!(vec![0x80], restored_large_blob_array); From 2dbe1c5f075a2f563f1cd880ad355c03e0a38eab Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 20:59:26 +0100 Subject: [PATCH 53/86] adds enterprise for make, byte keys --- src/ctap/command.rs | 226 ++++++++++++++++++++++--------------------- src/ctap/mod.rs | 2 + src/ctap/response.rs | 46 ++++----- 3 files changed, 141 insertions(+), 133 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 2e1fe3b..8a2cade 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -161,6 +161,7 @@ pub struct AuthenticatorMakeCredentialParameters { pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -169,15 +170,16 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { fn try_from(cbor_value: cbor::Value) -> Result { 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, + 0x01 => client_data_hash, + 0x02 => rp, + 0x03 => user, + 0x04 => cred_param_vec, + 0x05 => exclude_list, + 0x06 => extensions, + 0x07 => options, + 0x08 => pin_uv_auth_param, + 0x09 => pin_uv_auth_protocol, + 0x0A => enterprise_attestation, } = extract_map(cbor_value)?; } @@ -217,6 +219,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { 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()?; + let enterprise_attestation = enterprise_attestation.map(extract_bool).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -228,6 +231,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation, }) } } @@ -251,13 +255,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { fn try_from(cbor_value: cbor::Value) -> Result { 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, + 0x01 => rp_id, + 0x02 => client_data_hash, + 0x03 => allow_list, + 0x04 => extensions, + 0x05 => options, + 0x06 => pin_uv_auth_param, + 0x07 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } @@ -321,14 +325,14 @@ impl TryFrom for AuthenticatorClientPinParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => pin_protocol, - 2 => sub_command, - 3 => key_agreement, - 4 => pin_auth, - 5 => new_pin_enc, - 6 => pin_hash_enc, - 9 => permissions, - 10 => permissions_rp_id, + 0x01 => pin_protocol, + 0x02 => sub_command, + 0x03 => key_agreement, + 0x04 => pin_auth, + 0x05 => new_pin_enc, + 0x06 => pin_hash_enc, + 0x09 => permissions, + 0x0A => permissions_rp_id, } = extract_map(cbor_value)?; } @@ -375,12 +379,12 @@ impl TryFrom for AuthenticatorLargeBlobsParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => get, - 2 => set, - 3 => offset, - 4 => length, - 5 => pin_uv_auth_param, - 6 => pin_uv_auth_protocol, + 0x01 => get, + 0x02 => set, + 0x03 => offset, + 0x04 => length, + 0x05 => pin_uv_auth_param, + 0x06 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } @@ -486,8 +490,8 @@ impl TryFrom for AuthenticatorAttestationMaterial { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => certificate, - 2 => private_key, + 0x01 => certificate, + 0x02 => private_key, } = extract_map(cbor_value)?; } let certificate = extract_byte_string(ok_or_missing(certificate)?)?; @@ -552,8 +556,8 @@ impl TryFrom for AuthenticatorVendorConfigureParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => lockdown, - 2 => attestation_material, + 0x01 => lockdown, + 0x02 => attestation_material, } = extract_map(cbor_value)?; } let lockdown = lockdown.map_or(Ok(false), extract_bool)?; @@ -581,22 +585,23 @@ mod test { #[test] fn test_from_cbor_make_credential_parameters() { let cbor_value = cbor_map! { - 1 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], - 2 => cbor_map! { + 0x01 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], + 0x02 => cbor_map! { "id" => "example.com", "name" => "Example", "icon" => "example.com/icon.png", }, - 3 => cbor_map! { + 0x03 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], "name" => "foo", "displayName" => "bar", "icon" => "example.com/foo/icon.png", }, - 4 => cbor_array![ES256_CRED_PARAM], - 5 => cbor_array![], - 8 => vec![0x12, 0x34], - 9 => 1, + 0x04 => cbor_array![ES256_CRED_PARAM], + 0x05 => cbor_array![], + 0x08 => vec![0x12, 0x34], + 0x09 => 1, + 0x0A => true, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -630,6 +635,7 @@ mod test { options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), + enterprise_attestation: Some(true), }; assert_eq!( @@ -641,15 +647,15 @@ mod test { #[test] fn test_from_cbor_get_assertion_parameters() { let cbor_value = cbor_map! { - 1 => "example.com", - 2 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], - 3 => cbor_array![ cbor_map! { + 0x01 => "example.com", + 0x02 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], + 0x03 => cbor_array![ cbor_map! { "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], "transports" => cbor_array!["usb"], } ], - 6 => vec![0x12, 0x34], - 7 => 1, + 0x06 => vec![0x12, 0x34], + 0x07 => 1, }; let returned_get_assertion_parameters = AuthenticatorGetAssertionParameters::try_from(cbor_value).unwrap(); @@ -692,14 +698,14 @@ mod test { let cose_key = CoseKey::from(pk); let cbor_value = cbor_map! { - 1 => 1, - 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor::Value::from(cose_key.clone()), - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], - 9 => 0x03, - 10 => "example.com", + 0x01 => 1, + 0x02 => ClientPinSubCommand::GetPinRetries, + 0x03 => cbor::Value::from(cose_key.clone()), + 0x04 => vec! [0xBB], + 0x05 => vec! [0xCC], + 0x06 => vec! [0xDD], + 0x09 => 0x03, + 0x0A => "example.com", }; let returned_client_pin_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); @@ -746,12 +752,12 @@ mod test { #[test] fn test_from_cbor_cred_management_parameters() { let cbor_value = cbor_map! { - 1 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, - 2 => cbor_map!{ + 0x01 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 0x02 => cbor_map!{ 0x01 => vec![0x1D; 32], }, - 3 => 1, - 4 => vec! [0x9A; 16], + 0x03 => 1, + 0x04 => vec! [0x9A; 16], }; let returned_cred_management_parameters = AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap(); @@ -785,8 +791,8 @@ mod test { fn test_from_cbor_large_blobs_parameters() { // successful get let cbor_value = cbor_map! { - 1 => 2, - 3 => 4, + 0x01 => 2, + 0x03 => 4, }; let returned_large_blobs_parameters = AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); @@ -805,11 +811,11 @@ mod test { // successful first set let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 0, - 4 => MIN_LARGE_BLOB_LEN as u64, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, }; let returned_large_blobs_parameters = AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); @@ -828,10 +834,10 @@ mod test { // successful next set let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 1, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 1, + 0x05 => vec! [0xA9], + 0x06 => 1, }; let returned_large_blobs_parameters = AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); @@ -850,9 +856,9 @@ mod test { // failing with neither get nor set let cbor_value = cbor_map! { - 3 => 4, - 5 => vec! [0xA9], - 6 => 1, + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -861,11 +867,11 @@ mod test { // failing with get and set let cbor_value = cbor_map! { - 1 => 2, - 2 => vec! [0x5E], - 3 => 4, - 5 => vec! [0xA9], - 6 => 1, + 0x01 => 2, + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -874,11 +880,11 @@ mod test { // failing with get and length let cbor_value = cbor_map! { - 1 => 2, - 3 => 4, - 4 => MIN_LARGE_BLOB_LEN as u64, - 5 => vec! [0xA9], - 6 => 1, + 0x01 => 2, + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -887,10 +893,10 @@ mod test { // failing with zero offset and no length present let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 0, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -899,11 +905,11 @@ mod test { // failing with length smaller than minimum let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 0, - 4 => MIN_LARGE_BLOB_LEN as u64 - 1, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x04 => MIN_LARGE_BLOB_LEN as u64 - 1, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -912,11 +918,11 @@ mod test { // failing with non-zero offset and length present let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 4, - 4 => MIN_LARGE_BLOB_LEN as u64, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -948,10 +954,10 @@ mod test { // Attestation key is too short. let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 1 => dummy_cert, - 2 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] } }; assert_eq!( @@ -961,9 +967,9 @@ mod test { // Missing private key let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 1 => dummy_cert + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert } }; assert_eq!( @@ -973,9 +979,9 @@ mod test { // Missing certificate let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 2 => dummy_pkey + 0x01 => false, + 0x02 => cbor_map! { + 0x02 => dummy_pkey } }; assert_eq!( @@ -985,10 +991,10 @@ mod test { // Valid let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 1 => dummy_cert, - 2 => dummy_pkey + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey } }; assert_eq!( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8fa622c..7b426c1 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -561,6 +561,7 @@ where options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation: _, } = make_credential_params; self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; @@ -1313,6 +1314,7 @@ mod test { options, pin_uv_auth_param: None, pin_uv_auth_protocol: None, + enterprise_attestation: None, } } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 245218f..87d94cc 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -74,9 +74,9 @@ impl From for cbor::Value { } = make_credential_response; cbor_map_options! { - 1 => fmt, - 2 => auth_data, - 3 => att_stmt, + 0x01 => fmt, + 0x02 => auth_data, + 0x03 => att_stmt, } } } @@ -102,11 +102,11 @@ impl From for cbor::Value { } = get_assertion_response; cbor_map_options! { - 1 => credential, - 2 => auth_data, - 3 => signature, - 4 => user, - 5 => number_of_credentials, + 0x01 => credential, + 0x02 => auth_data, + 0x03 => signature, + 0x04 => user, + 0x05 => number_of_credentials, } } } @@ -199,9 +199,9 @@ impl From for cbor::Value { } = client_pin_response; cbor_map_options! { - 1 => key_agreement.map(cbor::Value::from), - 2 => pin_token, - 3 => retries, + 0x01 => key_agreement.map(cbor::Value::from), + 0x02 => pin_token, + 0x03 => retries, } } } @@ -286,8 +286,8 @@ impl From for cbor::Value { } = vendor_response; cbor_map_options! { - 1 => cert_programmed, - 2 => pkey_programmed, + 0x01 => cert_programmed, + 0x02 => pkey_programmed, } } } @@ -324,9 +324,9 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorMakeCredential(make_credential_response).into(); let expected_cbor = cbor_map_options! { - 1 => "packed", - 2 => vec![0xAD], - 3 => cbor_packed_attestation_statement, + 0x01 => "packed", + 0x02 => vec![0xAD], + 0x03 => cbor_packed_attestation_statement, }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -343,8 +343,8 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorGetAssertion(get_assertion_response).into(); let expected_cbor = cbor_map_options! { - 2 => vec![0xAD], - 3 => vec![0x51], + 0x02 => vec![0xAD], + 0x03 => vec![0x51], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -435,7 +435,7 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into(); let expected_cbor = cbor_map_options! { - 2 => vec![70], + 0x02 => vec![70], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -562,8 +562,8 @@ mod test { assert_eq!( response_cbor, Some(cbor_map_options! { - 1 => true, - 2 => false, + 0x01 => true, + 0x02 => false, }) ); let response_cbor: Option = @@ -575,8 +575,8 @@ mod test { assert_eq!( response_cbor, Some(cbor_map_options! { - 1 => false, - 2 => true, + 0x01 => false, + 0x02 => true, }) ); } From 5741595e575205211ccf4831acbf18c7e620afde Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 21:45:06 +0100 Subject: [PATCH 54/86] new extension entry for largeBlobKey --- src/ctap/credential_management.rs | 5 +- src/ctap/data_formats.rs | 39 +++++++ src/ctap/mod.rs | 179 +++++++++++++++++++++++------- src/ctap/response.rs | 59 ++++++++-- src/ctap/storage.rs | 4 + 5 files changed, 233 insertions(+), 53 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 7665ea7..acc9278 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -81,6 +81,7 @@ fn enumerate_credentials_response( user_name, user_icon, cred_blob: _, + large_blob_key, } = credential; let user = PublicKeyCredentialUserEntity { user_id: user_handle, @@ -100,8 +101,7 @@ fn enumerate_credentials_response( public_key: Some(public_key), total_credentials, cred_protect: cred_protect_policy, - // TODO(kaczmarczyck) add when largeBlobKey extension is implemented - large_blob_key: None, + large_blob_key, ..Default::default() }) } @@ -348,6 +348,7 @@ mod test { user_name: Some("name".to_string()), user_icon: Some("icon".to_string()), cred_blob: None, + large_blob_key: None, } } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index da992f8..ba9ca1a 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -282,6 +282,7 @@ pub struct MakeCredentialExtensions { pub cred_protect: Option, pub min_pin_length: bool, pub cred_blob: Option>, + pub large_blob_key: Option, } impl TryFrom for MakeCredentialExtensions { @@ -293,6 +294,7 @@ impl TryFrom for MakeCredentialExtensions { "credBlob" => cred_blob, "credProtect" => cred_protect, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, "minPinLength" => min_pin_length, } = extract_map(cbor_value)?; } @@ -303,11 +305,18 @@ impl TryFrom for MakeCredentialExtensions { .transpose()?; let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let large_blob_key = large_blob_key.map(extract_bool).transpose()?; + if let Some(large_blob_key) = large_blob_key { + if !large_blob_key { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + } Ok(Self { hmac_secret, cred_protect, min_pin_length, cred_blob, + large_blob_key, }) } } @@ -317,6 +326,7 @@ impl TryFrom for MakeCredentialExtensions { pub struct GetAssertionExtensions { pub hmac_secret: Option, pub cred_blob: bool, + pub large_blob_key: Option, } impl TryFrom for GetAssertionExtensions { @@ -327,6 +337,7 @@ impl TryFrom for GetAssertionExtensions { let { "credBlob" => cred_blob, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, } = extract_map(cbor_value)?; } @@ -334,9 +345,16 @@ impl TryFrom for GetAssertionExtensions { .map(GetAssertionHmacSecretInput::try_from) .transpose()?; let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?; + let large_blob_key = large_blob_key.map(extract_bool).transpose()?; + if let Some(large_blob_key) = large_blob_key { + if !large_blob_key { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + } Ok(Self { hmac_secret, cred_blob, + large_blob_key, }) } } @@ -546,6 +564,7 @@ pub struct PublicKeyCredentialSource { pub user_name: Option, pub user_icon: Option, pub cred_blob: Option>, + pub large_blob_key: Option>, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -561,6 +580,7 @@ enum PublicKeyCredentialSourceField { UserName = 8, UserIcon = 9, CredBlob = 10, + LargeBlobKey = 11, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: @@ -588,6 +608,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::UserName => credential.user_name, PublicKeyCredentialSourceField::UserIcon => credential.user_icon, PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => credential.large_blob_key, } } } @@ -608,6 +629,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserIcon => user_icon, PublicKeyCredentialSourceField::CredBlob => cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => large_blob_key, } = extract_map(cbor_value)?; } @@ -628,6 +650,7 @@ impl TryFrom for PublicKeyCredentialSource { let user_name = user_name.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?; let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let large_blob_key = large_blob_key.map(extract_byte_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -650,6 +673,7 @@ impl TryFrom for PublicKeyCredentialSource { user_name, user_icon, cred_blob, + large_blob_key, }) } } @@ -1522,6 +1546,7 @@ mod test { "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "minPinLength" => true, "credBlob" => vec![0xCB], + "largeBlobKey" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { @@ -1529,6 +1554,7 @@ mod test { cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: true, cred_blob: Some(vec![0xCB]), + large_blob_key: Some(true), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1546,6 +1572,7 @@ mod test { 3 => vec![0x03; 16], }, "credBlob" => true, + "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { @@ -1556,6 +1583,7 @@ mod test { let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), cred_blob: true, + large_blob_key: Some(true), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1849,6 +1877,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert_eq!( @@ -1901,6 +1930,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + large_blob_key: Some(vec![0x1B]), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 7b426c1..ab66177 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -49,7 +49,7 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::{PersistentStore, MAX_RP_IDS_LENGTH}; +use self::storage::{PersistentStore, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_RP_IDS_LENGTH}; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; @@ -427,6 +427,7 @@ where user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, })) } @@ -596,6 +597,10 @@ where || cred_protect_policy.is_some() || min_pin_length || has_cred_blob_output; + let large_blob_key = match (options.rk, extensions.large_blob_key) { + (true, Some(true)) => Some(self.rng.gen_uniform_u8x32().to_vec()), + _ => None, + }; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { @@ -674,6 +679,7 @@ where .user_icon .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_blob, + large_blob_key: large_blob_key.clone(), }; self.persistent_store.store_credential(credential_source)?; random_id @@ -749,6 +755,7 @@ where fmt: String::from("packed"), auth_data, att_stmt: attestation_statement, + large_blob_key, }, )) } @@ -806,6 +813,10 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } + let large_blob_key = match extensions.large_blob_key { + Some(true) => credential.large_blob_key, + _ => None, + }; let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); @@ -841,6 +852,7 @@ where signature: signature.to_asn1_der(), user, number_of_credentials: number_of_credentials.map(|n| n as u64), + large_blob_key, }, )) } @@ -1006,19 +1018,18 @@ where fn process_get_info(&self) -> Result { let mut options_map = BTreeMap::new(); - // TODO(kaczmarczyck) add authenticatorConfig and credProtect options options_map.insert(String::from("rk"), true); - options_map.insert(String::from("up"), true); options_map.insert( String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("up"), true); + options_map.insert(String::from("pinUvAuthToken"), true); + options_map.insert(String::from("largeBlobs"), true); + options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); - options_map.insert( - String::from("forcePINChange"), - self.persistent_store.has_force_pin_change()?, - ); + options_map.insert(String::from("makeCredUvNotRqd"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -1032,6 +1043,7 @@ where String::from("credProtect"), String::from("minPinLength"), String::from("credBlob"), + String::from("largeBlobKey"), ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), @@ -1041,7 +1053,8 @@ where max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), - default_cred_protect: DEFAULT_CRED_PROTECT, + max_serialized_large_blob_array: Some(MAX_LARGE_BLOB_ARRAY_SIZE as u64), + force_pin_change: Some(self.persistent_store.has_force_pin_change()?), min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), @@ -1214,6 +1227,7 @@ mod test { fmt, auth_data, att_stmt, + large_blob_key, } = make_credential_response; // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); @@ -1234,6 +1248,7 @@ mod test { expected_extension_cbor ); assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + assert_eq!(large_blob_key, None); } _ => panic!("Invalid response type"), } @@ -1258,15 +1273,19 @@ mod test { String::from("credProtect"), String::from("minPinLength"), String::from("credBlob"), + String::from("largeBlobKey"), ]], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), 0x04 => cbor_map! { "rk" => true, - "up" => true, "clientPin" => false, + "up" => true, + "pinUvAuthToken" => true, + "largeBlobs" => true, + "authnrCfg" => true, "credMgmt" => true, "setMinPINLength" => true, - "forcePINChange" => false, + "makeCredUvNotRqd" => true, }, 0x05 => MAX_MSG_SIZE as u64, 0x06 => cbor_array_vec![vec![1]], @@ -1274,7 +1293,8 @@ mod test { 0x08 => CREDENTIAL_ID_SIZE as u64, 0x09 => cbor_array_vec![vec!["usb"]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], - 0x0C => DEFAULT_CRED_PROTECT.map(|c| c as u64), + 0x0B => MAX_LARGE_BLOB_ARRAY_SIZE as u64, + 0x0C => false, 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, 0x0F => MAX_CRED_BLOB_LENGTH as u64, 0x10 => MAX_RP_IDS_LENGTH as u64, @@ -1336,10 +1356,8 @@ mod test { policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { let extensions = MakeCredentialExtensions { - hmac_secret: false, cred_protect: Some(policy), - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1424,6 +1442,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1504,9 +1523,7 @@ mod test { let extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1534,9 +1551,7 @@ mod test { let extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1563,10 +1578,8 @@ mod test { // First part: The extension is ignored, since the RP ID is not on the list. let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, min_pin_length: true, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1589,10 +1602,8 @@ mod test { ); let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, min_pin_length: true, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1618,10 +1629,8 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, - min_pin_length: false, cred_blob: Some(vec![0xCB]), + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1656,10 +1665,8 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, - min_pin_length: false, cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]), + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1687,6 +1694,39 @@ mod test { assert_eq!(stored_credential.cred_blob, None); } + #[test] + fn test_process_make_credential_large_blob_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + large_blob_key: Some(true), + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let large_blob_key = match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + make_credential_response.large_blob_key.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!(large_blob_key.len(), 32); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; @@ -1828,9 +1868,7 @@ mod test { let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1857,7 +1895,7 @@ mod test { }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - cred_blob: false, + ..Default::default() }; let cred_desc = PublicKeyCredentialDescriptor { @@ -1898,9 +1936,7 @@ mod test { let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; @@ -1916,7 +1952,7 @@ mod test { }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - cred_blob: false, + ..Default::default() }; let get_assertion_params = AuthenticatorGetAssertionParameters { @@ -1970,6 +2006,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2033,6 +2070,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2082,6 +2120,7 @@ mod test { user_name: None, user_icon: None, cred_blob: Some(vec![0xCB]), + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2089,8 +2128,8 @@ mod test { .is_ok()); let extensions = GetAssertionExtensions { - hmac_secret: None, cred_blob: true, + ..Default::default() }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), @@ -2125,6 +2164,63 @@ mod test { ); } + #[test] + fn test_process_get_assertion_with_large_blob_key() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: Some(vec![0x1C; 32]), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + large_blob_key: Some(true), + ..Default::default() + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let large_blob_key = match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + get_assertion_response.large_blob_key.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!(large_blob_key, vec![0x1C; 32]); + } + #[test] fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; @@ -2369,6 +2465,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 87d94cc..346a348 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -63,6 +63,7 @@ pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, pub att_stmt: PackedAttestationStatement, + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -71,12 +72,14 @@ impl From for cbor::Value { fmt, auth_data, att_stmt, + large_blob_key, } = make_credential_response; cbor_map_options! { 0x01 => fmt, 0x02 => auth_data, 0x03 => att_stmt, + 0x05 => large_blob_key, } } } @@ -89,6 +92,7 @@ pub struct AuthenticatorGetAssertionResponse { pub signature: Vec, pub user: Option, pub number_of_credentials: Option, + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -99,6 +103,7 @@ impl From for cbor::Value { signature, user, number_of_credentials, + large_blob_key, } = get_assertion_response; cbor_map_options! { @@ -107,6 +112,7 @@ impl From for cbor::Value { 0x03 => signature, 0x04 => user, 0x05 => number_of_credentials, + 0x07 => large_blob_key, } } } @@ -124,7 +130,8 @@ pub struct AuthenticatorGetInfoResponse { pub max_credential_id_length: Option, pub transports: Option>, pub algorithms: Option>, - pub default_cred_protect: Option, + pub max_serialized_large_blob_array: Option, + pub force_pin_change: Option, pub min_pin_length: u8, pub firmware_version: Option, pub max_cred_blob_length: Option, @@ -145,7 +152,8 @@ impl From for cbor::Value { max_credential_id_length, transports, algorithms, - default_cred_protect, + max_serialized_large_blob_array, + force_pin_change, min_pin_length, firmware_version, max_cred_blob_length, @@ -172,7 +180,8 @@ impl From for cbor::Value { 0x08 => max_credential_id_length, 0x09 => transports.map(|vec| cbor_array_vec!(vec)), 0x0A => algorithms.map(|vec| cbor_array_vec!(vec)), - 0x0C => default_cred_protect.map(|p| p as u64), + 0x0B => max_serialized_large_blob_array, + 0x0C => force_pin_change, 0x0D => min_pin_length as u64, 0x0E => firmware_version, 0x0F => max_cred_blob_length, @@ -297,7 +306,7 @@ mod test { use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType}; use super::super::ES256_CRED_PARAM; use super::*; - use cbor::{cbor_bytes, cbor_map}; + use cbor::{cbor_array, cbor_bytes, cbor_map}; use crypto::rng256::ThreadRng256; #[test] @@ -320,6 +329,7 @@ mod test { fmt: "packed".to_string(), auth_data: vec![0xAD], att_stmt, + large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = ResponseData::AuthenticatorMakeCredential(make_credential_response).into(); @@ -327,24 +337,50 @@ mod test { 0x01 => "packed", 0x02 => vec![0xAD], 0x03 => cbor_packed_attestation_statement, + 0x05 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); } #[test] fn test_get_assertion_into_cbor() { + let pub_key_cred_descriptor = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], + transports: Some(vec![AuthenticatorTransport::Usb]), + }; + let user = PublicKeyCredentialUserEntity { + user_id: vec![0x1D, 0x1D, 0x1D, 0x1D], + user_name: Some("foo".to_string()), + user_display_name: Some("bar".to_string()), + user_icon: Some("example.com/foo/icon.png".to_string()), + }; let get_assertion_response = AuthenticatorGetAssertionResponse { - credential: None, + credential: Some(pub_key_cred_descriptor), auth_data: vec![0xAD], signature: vec![0x51], - user: None, - number_of_credentials: None, + user: Some(user), + number_of_credentials: Some(2), + large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = ResponseData::AuthenticatorGetAssertion(get_assertion_response).into(); let expected_cbor = cbor_map_options! { + 0x01 => cbor_map! { + "type" => "public-key", + "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "transports" => cbor_array!["usb"], + }, 0x02 => vec![0xAD], 0x03 => vec![0x51], + 0x04 => cbor_map! { + "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "name" => "foo".to_string(), + "displayName" => "bar".to_string(), + "icon" => "example.com/foo/icon.png".to_string(), + }, + 0x05 => 2, + 0x07 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -363,7 +399,8 @@ mod test { max_credential_id_length: None, transports: None, algorithms: None, - default_cred_protect: None, + max_serialized_large_blob_array: None, + force_pin_change: None, min_pin_length: 4, firmware_version: None, max_cred_blob_length: None, @@ -395,7 +432,8 @@ mod test { max_credential_id_length: Some(256), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), - default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), + max_serialized_large_blob_array: Some(1024), + force_pin_change: Some(false), min_pin_length: 4, firmware_version: Some(0), max_cred_blob_length: Some(1024), @@ -415,7 +453,8 @@ mod test { 0x08 => 256, 0x09 => cbor_array_vec![vec!["usb"]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], - 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, + 0x0B => 1024, + 0x0C => false, 0x0D => 4, 0x0E => 0, 0x0F => 1024, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 43a00c7..b982922 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -756,6 +756,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, } } @@ -973,6 +974,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -995,6 +997,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1321,6 +1324,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From 371e8b6f35efc8b489a2101cb8f078bd717652f4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 05:46:03 +0100 Subject: [PATCH 55/86] remove conditional trait implementation --- libraries/crypto/src/ec/exponent256.rs | 6 +-- libraries/crypto/src/ec/gfp256.rs | 1 - libraries/crypto/src/ec/int256.rs | 1 - libraries/crypto/src/ec/point.rs | 2 - libraries/crypto/src/ecdh.rs | 2 +- libraries/crypto/src/ecdsa.rs | 5 +- src/ctap/apdu.rs | 15 ++---- src/ctap/command.rs | 18 +++---- src/ctap/ctap1.rs | 5 +- src/ctap/data_formats.rs | 65 +++++++++----------------- src/ctap/response.rs | 25 ++++------ 11 files changed, 51 insertions(+), 94 deletions(-) diff --git a/libraries/crypto/src/ec/exponent256.rs b/libraries/crypto/src/ec/exponent256.rs index 8638eaa..4a31aee 100644 --- a/libraries/crypto/src/ec/exponent256.rs +++ b/libraries/crypto/src/ec/exponent256.rs @@ -18,11 +18,10 @@ use core::ops::Mul; use subtle::{self, Choice, ConditionallySelectable, CtOption}; // An exponent on the elliptic curve, that is an element modulo the curve order N. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] // TODO: remove this Default once https://github.com/dalek-cryptography/subtle/issues/63 is // resolved. #[derive(Default)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] pub struct ExponentP256 { int: Int256, } @@ -92,11 +91,10 @@ impl Mul for &ExponentP256 { } // A non-zero exponent on the elliptic curve. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] // TODO: remove this Default once https://github.com/dalek-cryptography/subtle/issues/63 is // resolved. #[derive(Default)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] pub struct NonZeroExponentP256 { e: ExponentP256, } diff --git a/libraries/crypto/src/ec/gfp256.rs b/libraries/crypto/src/ec/gfp256.rs index bb3232c..0e6179f 100644 --- a/libraries/crypto/src/ec/gfp256.rs +++ b/libraries/crypto/src/ec/gfp256.rs @@ -111,7 +111,6 @@ impl Mul for &GFP256 { } } -#[cfg(feature = "derive_debug")] impl core::fmt::Debug for GFP256 { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "GFP256::{:?}", self.int) diff --git a/libraries/crypto/src/ec/int256.rs b/libraries/crypto/src/ec/int256.rs index 9954c37..8927b19 100644 --- a/libraries/crypto/src/ec/int256.rs +++ b/libraries/crypto/src/ec/int256.rs @@ -636,7 +636,6 @@ impl SubAssign<&Int256> for Int256 { } } -#[cfg(feature = "derive_debug")] impl core::fmt::Debug for Int256 { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "Int256 {{ digits: {:08x?} }}", self.digits) diff --git a/libraries/crypto/src/ec/point.rs b/libraries/crypto/src/ec/point.rs index 11c6cde..1038808 100644 --- a/libraries/crypto/src/ec/point.rs +++ b/libraries/crypto/src/ec/point.rs @@ -542,7 +542,6 @@ impl Add for &PointProjective { } } -#[cfg(feature = "derive_debug")] impl core::fmt::Debug for PointP256 { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { f.debug_struct("PointP256") @@ -552,7 +551,6 @@ impl core::fmt::Debug for PointP256 { } } -#[cfg(feature = "derive_debug")] impl PartialEq for PointP256 { fn eq(&self, other: &PointP256) -> bool { self.x == other.x && self.y == other.y diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index a1e3736..705aee0 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -26,7 +26,7 @@ pub struct SecKey { a: NonZeroExponentP256, } -#[cfg_attr(feature = "derive_debug", derive(Clone, PartialEq, Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PubKey { p: PointP256, } diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index b6a1708..eb61365 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -30,8 +30,7 @@ use core::marker::PhantomData; pub const NBYTES: usize = int256::NBYTES; -#[derive(Clone, PartialEq)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct SecKey { k: NonZeroExponentP256, } @@ -41,7 +40,7 @@ pub struct Signature { s: NonZeroExponentP256, } -#[cfg_attr(feature = "derive_debug", derive(Clone))] +#[derive(Clone)] pub struct PubKey { p: PointP256, } diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index f475308..f12ded2 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -18,9 +18,8 @@ use core::convert::TryFrom; const APDU_HEADER_LEN: usize = 4; -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, PartialEq)] #[allow(non_camel_case_types, dead_code)] -#[derive(PartialEq)] pub enum ApduStatusCode { SW_SUCCESS = 0x90_00, /// Command successfully executed; 'XX' bytes of data are @@ -51,9 +50,8 @@ pub enum ApduInstructions { GetResponse = 0xC0, } -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, Default, PartialEq)] #[allow(dead_code)] -#[derive(Default, PartialEq)] pub struct ApduHeader { pub cla: u8, pub ins: u8, @@ -72,8 +70,7 @@ impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader { } } -#[cfg_attr(test, derive(Clone, Debug))] -#[derive(PartialEq)] +#[derive(Clone, Debug, PartialEq)] /// The APDU cases pub enum Case { Le1, @@ -85,18 +82,16 @@ pub enum Case { Le3, } -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, PartialEq)] #[allow(dead_code)] -#[derive(PartialEq)] pub enum ApduType { Instruction, Short(Case), Extended(Case), } -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, PartialEq)] #[allow(dead_code)] -#[derive(PartialEq)] pub struct APDU { pub header: ApduHeader, pub lc: u16, diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 8a2cade..a76254a 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -38,7 +38,7 @@ pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; const MIN_LARGE_BLOB_LEN: usize = 17; // CTAP specification (version 20190130) section 6.1 -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub enum Command { AuthenticatorMakeCredential(AuthenticatorMakeCredentialParameters), AuthenticatorGetAssertion(AuthenticatorGetAssertionParameters), @@ -148,7 +148,7 @@ impl Command { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, @@ -236,7 +236,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, @@ -307,7 +307,7 @@ impl TryFrom for AuthenticatorGetAssertionParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { pub pin_protocol: u64, pub sub_command: ClientPinSubCommand, @@ -363,7 +363,7 @@ impl TryFrom for AuthenticatorClientPinParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorLargeBlobsParameters { pub get: Option, pub set: Option>, @@ -438,7 +438,7 @@ impl TryFrom for AuthenticatorLargeBlobsParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorConfigParameters { pub sub_command: ConfigSubCommand, pub sub_command_params: Option, @@ -478,7 +478,7 @@ impl TryFrom for AuthenticatorConfigParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorAttestationMaterial { pub certificate: Vec, pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], @@ -507,7 +507,7 @@ impl TryFrom for AuthenticatorAttestationMaterial { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorCredentialManagementParameters { pub sub_command: CredentialManagementSubCommand, pub sub_command_params: Option, @@ -544,7 +544,7 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorVendorConfigureParameters { pub lockdown: bool, pub attestation_material: Option, diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 0bf43b5..09a3c6c 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -29,8 +29,7 @@ pub type Ctap1StatusCode = ApduStatusCode; // The specification referenced in this file is at: // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug))] -#[derive(PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Ctap1Flags { CheckOnly = 0x07, EnforceUpAndSign = 0x03, @@ -56,7 +55,7 @@ impl Into for Ctap1Flags { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] // TODO: remove #allow when https://github.com/rust-lang/rust/issues/64362 is fixed enum U2fCommand { #[allow(dead_code)] diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ba9ca1a..1992469 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,8 +27,7 @@ use enum_iterator::IntoEnumIterator; const ES256_ALGORITHM: i64 = -7; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialRpEntity { pub rp_id: String, pub rp_name: Option, @@ -70,8 +69,7 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -118,8 +116,7 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype -#[derive(Clone, PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub enum PublicKeyCredentialType { PublicKey, // This is the default for all strings not covered above. @@ -151,8 +148,7 @@ impl TryFrom for PublicKeyCredentialType { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -185,8 +181,7 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -223,8 +218,7 @@ impl TryFrom for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -275,8 +269,7 @@ impl From for cbor::Value { } } -#[derive(Default)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, @@ -321,8 +314,7 @@ impl TryFrom for MakeCredentialExtensions { } } -#[derive(Clone, Default)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct GetAssertionExtensions { pub hmac_secret: Option, pub cred_blob: bool, @@ -359,8 +351,7 @@ impl TryFrom for GetAssertionExtensions { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, @@ -391,8 +382,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. -#[derive(Default)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -425,7 +415,7 @@ impl TryFrom for MakeCredentialOptions { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct GetAssertionOptions { pub up: bool, pub uv: bool, @@ -470,8 +460,7 @@ impl TryFrom for GetAssertionOptions { } // https://www.w3.org/TR/webauthn/#packed-attestation -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct PackedAttestationStatement { pub alg: i64, pub sig: Vec, @@ -490,8 +479,7 @@ impl From for cbor::Value { } } -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. @@ -516,8 +504,7 @@ impl TryFrom for SignatureAlgorithm { } } -#[derive(Clone, Copy, PartialEq, PartialOrd)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialProtectionPolicy { UserVerificationOptional = 0x01, @@ -548,9 +535,7 @@ impl TryFrom for CredentialProtectionPolicy { // // Note that we only use the WebAuthn definition as an example. This data-structure is not specified // by FIDO. In particular we may choose how we serialize and deserialize it. -#[derive(Clone)] -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialSource { // TODO function to convert to / from Vec pub key_type: PublicKeyCredentialType, @@ -688,8 +673,7 @@ impl PublicKeyCredentialSource { } // The COSE key is used for both ECDH and ECDSA public keys for transmission. -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct CoseKey { x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES], @@ -818,7 +802,7 @@ impl TryFrom for ecdh::PubKey { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { GetPinRetries = 0x01, @@ -856,8 +840,7 @@ impl TryFrom for ClientPinSubCommand { } } -#[derive(Clone, Copy)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ConfigSubCommand { EnableEnterpriseAttestation = 0x01, @@ -887,8 +870,7 @@ impl TryFrom for ConfigSubCommand { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub enum ConfigSubCommandParams { SetMinPinLength(SetMinPinLengthParams), } @@ -903,8 +885,7 @@ impl From for cbor::Value { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct SetMinPinLengthParams { pub new_min_pin_length: Option, pub min_pin_length_rp_ids: Option>, @@ -958,8 +939,7 @@ impl From for cbor::Value { } } -#[derive(Clone, Copy)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialManagementSubCommand { GetCredsMetadata = 0x01, @@ -995,8 +975,7 @@ impl TryFrom for CredentialManagementSubCommand { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct CredentialManagementSubCommandParameters { pub rp_id_hash: Option>, pub credential_id: Option, diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 346a348..093d4c9 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -22,8 +22,7 @@ use alloc::string::String; use alloc::vec::Vec; use cbor::{cbor_array_vec, cbor_bool, cbor_map_btree, cbor_map_options, cbor_text}; -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub enum ResponseData { AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse), AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse), @@ -57,8 +56,7 @@ impl From for Option { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, @@ -84,8 +82,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetAssertionResponse { pub credential: Option, pub auth_data: Vec, @@ -117,8 +114,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetInfoResponse { pub versions: Vec, pub extensions: Option>, @@ -191,8 +187,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinResponse { pub key_agreement: Option, pub pin_token: Option>, @@ -215,8 +210,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorLargeBlobsResponse { pub config: Vec, } @@ -231,9 +225,7 @@ impl From for cbor::Value { } } -#[derive(Default)] -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, Default, PartialEq)] pub struct AuthenticatorCredentialManagementResponse { pub existing_resident_credentials_count: Option, pub max_possible_remaining_resident_credentials_count: Option, @@ -280,8 +272,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorVendorResponse { pub cert_programmed: bool, pub pkey_programmed: bool, From 9270afbc21022f862a7d3c15802754b60a4ac66a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 06:42:49 +0100 Subject: [PATCH 56/86] remove derive_debug feature --- Cargo.toml | 4 ++-- libraries/crypto/Cargo.toml | 1 - run_desktop_tests.sh | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bca9210..b7b0360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,9 @@ subtle = { version = "2.2", default-features = false, features = ["nightly"] } [features] debug_allocations = ["lang_items/debug_allocations"] -debug_ctap = ["crypto/derive_debug", "libtock_drivers/debug_ctap"] +debug_ctap = ["libtock_drivers/debug_ctap"] panic_console = ["lang_items/panic_console"] -std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] +std = ["cbor/std", "crypto/std", "lang_items/std", "persistent_store/std"] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] with_ctap1 = ["crypto/with_ctap1"] with_nfc = ["libtock_drivers/with_nfc"] diff --git a/libraries/crypto/Cargo.toml b/libraries/crypto/Cargo.toml index ead1294..aa1a597 100644 --- a/libraries/crypto/Cargo.toml +++ b/libraries/crypto/Cargo.toml @@ -25,5 +25,4 @@ regex = { version = "1", optional = true } [features] std = ["cbor/std", "hex", "rand", "ring", "untrusted", "serde", "serde_json", "regex"] -derive_debug = [] with_ctap1 = [] diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 24771f7..b54d3a8 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -91,7 +91,7 @@ then cargo test --release --features std cd ../.. cd libraries/crypto - RUSTFLAGS='-C target-feature=+aes' cargo test --release --features std,derive_debug + RUSTFLAGS='-C target-feature=+aes' cargo test --release --features std cd ../.. cd libraries/persistent_store cargo test --release --features std @@ -103,7 +103,7 @@ then cargo test --features std cd ../.. cd libraries/crypto - RUSTFLAGS='-C target-feature=+aes' cargo test --features std,derive_debug + RUSTFLAGS='-C target-feature=+aes' cargo test --features std cd ../.. cd libraries/persistent_store cargo test --features std From f64567febc45fc738b2fc16d3b2e79b4faad3a72 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 06:52:01 +0100 Subject: [PATCH 57/86] fix crypto workflow --- .github/workflows/crypto_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crypto_test.yml b/.github/workflows/crypto_test.yml index 50fdf88..5abfce9 100644 --- a/.github/workflows/crypto_test.yml +++ b/.github/workflows/crypto_test.yml @@ -33,10 +33,10 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --manifest-path libraries/crypto/Cargo.toml --release --features std,derive_debug + args: --manifest-path libraries/crypto/Cargo.toml --release --features std - name: Unit testing of crypto library (debug mode) uses: actions-rs/cargo@v1 with: command: test - args: --manifest-path libraries/crypto/Cargo.toml --features std,derive_debug + args: --manifest-path libraries/crypto/Cargo.toml --features std From db7ed10f5f45aaa8173a43d4f8560bac9cd4dfe0 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 18:04:29 +0100 Subject: [PATCH 58/86] changes the handling of 0 credentials --- src/ctap/credential_management.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index acc9278..f0721ac 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -46,16 +46,15 @@ fn get_stored_rp_ids( /// Generates the response for subcommands enumerating RPs. fn enumerate_rps_response( - rp_id: Option, + rp_id: String, total_rps: Option, ) -> Result { - let rp = rp_id.clone().map(|rp_id| PublicKeyCredentialRpEntity { + let rp_id_hash = Some(Sha256::hash(rp_id.as_bytes()).to_vec()); + let rp = Some(PublicKeyCredentialRpEntity { rp_id, rp_name: None, rp_icon: None, }); - let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); - Ok(AuthenticatorCredentialManagementResponse { rp, rp_id_hash, @@ -128,12 +127,15 @@ fn process_enumerate_rps_begin( let rp_set = get_stored_rp_ids(persistent_store)?; let total_rps = rp_set.len(); - // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1)); } // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() - enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) + let rp_id = rp_set + .into_iter() + .next() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + enumerate_rps_response(rp_id, Some(total_rps as u64)) } /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. @@ -148,7 +150,7 @@ fn process_enumerate_rps_get_next_rp( .into_iter() .nth(rp_id_index) .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - enumerate_rps_response(Some(rp_id), None) + enumerate_rps_response(rp_id, None) } /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. From e3148319c5dc60dcacbefac49b79c57ce902a0db Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 4 Feb 2021 16:06:25 +0100 Subject: [PATCH 59/86] allow RP ID permissions for some subcommands --- src/ctap/credential_management.rs | 47 ++++++++++---- src/ctap/mod.rs | 4 +- src/ctap/pin_protocol_v1.rs | 101 +++++++++++++++++++++++++----- src/ctap/storage.rs | 2 +- 4 files changed, 123 insertions(+), 31 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index acc9278..b4ecf6f 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -106,6 +106,22 @@ fn enumerate_credentials_response( }) } +/// Check if the token permissions have the correct associated RP ID. +/// +/// Either no RP ID is associated, or the RP ID matches the stored credential. +fn check_rp_id_permissions( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + credential_id: &[u8], +) -> Result<(), Ctap2StatusCode> { + // Pre-check a sufficient condition before calling the store. + if pin_protocol_v1.has_no_rp_id_permission().is_ok() { + return Ok(()); + } + let (_, credential) = persistent_store.find_credential_item(credential_id)?; + pin_protocol_v1.has_no_or_rp_id_permission(&credential.rp_id) +} + /// Processes the subcommand getCredsMetadata for CredentialManagement. fn process_get_creds_metadata( persistent_store: &PersistentStore, @@ -155,12 +171,14 @@ fn process_enumerate_rps_get_next_rp( fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { let rp_id_hash = sub_command_params .rp_id_hash .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?; let mut iter_result = Ok(()); let iter = persistent_store.iter_credentials(&mut iter_result)?; let mut rp_credentials: Vec = iter @@ -199,18 +217,21 @@ fn process_enumerate_credentials_get_next_credential( /// Processes the subcommand deleteCredential for CredentialManagement. fn process_delete_credential( persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params .credential_id .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? .key_id; + check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; persistent_store.delete_credential(&credential_id) } /// Processes the subcommand updateUserInformation for CredentialManagement. fn process_update_user_information( persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params @@ -220,6 +241,7 @@ fn process_update_user_information( let user = sub_command_params .user .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; persistent_store.update_credential(&credential_id, user) } @@ -255,13 +277,10 @@ pub fn process_credential_management( match sub_command { CredentialManagementSubCommand::GetCredsMetadata | CredentialManagementSubCommand::EnumerateRpsBegin - | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { check_pin_uv_auth_protocol(pin_protocol)?; - persistent_store - .pin_hash()? - .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let mut management_data = vec![sub_command as u8]; if let Some(sub_command_params) = sub_command_params.clone() { @@ -272,9 +291,8 @@ pub fn process_credential_management( if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } + // The RP ID permission is handled differently per subcommand below. pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; - pin_protocol_v1.has_no_permission_rp_id()?; - // TODO(kaczmarczyck) sometimes allow a RP ID } CredentialManagementSubCommand::EnumerateRpsGetNextRp | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} @@ -282,13 +300,17 @@ pub fn process_credential_management( let response = match sub_command { CredentialManagementSubCommand::GetCredsMetadata => { + pin_protocol_v1.has_no_rp_id_permission()?; Some(process_get_creds_metadata(persistent_store)?) } - CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( - persistent_store, - stateful_command_permission, - now, - )?), + CredentialManagementSubCommand::EnumerateRpsBegin => { + pin_protocol_v1.has_no_rp_id_permission()?; + Some(process_enumerate_rps_begin( + persistent_store, + stateful_command_permission, + now, + )?) + } CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some( process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?, ), @@ -296,6 +318,7 @@ pub fn process_credential_management( Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -309,6 +332,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::DeleteCredential => { process_delete_credential( persistent_store, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -316,6 +340,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::UpdateUserInformation => { process_update_user_information( persistent_store, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ab66177..0579f62 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -638,7 +638,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -923,7 +923,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; UV_FLAG } None => { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 6ec5644..d95b5d1 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -501,6 +501,7 @@ impl PinProtocolV1 { encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } + /// Check if the required command's token permission is granted. pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { // Relies on the fact that all permissions are represented by powers of two. if permission as u8 & self.permissions != 0 { @@ -510,22 +511,47 @@ impl PinProtocolV1 { } } - pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> { + /// Check if no RP ID is associated with the token permission. + pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { if self.permissions_rp_id.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } Ok(()) } - pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - if let Some(permissions_rp_id) = &self.permissions_rp_id { - if rp_id != permissions_rp_id { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } else { - self.permissions_rp_id = Some(String::from(rp_id)); + /// Check if no or the passed RP ID is associated with the token permission. + pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + _ => Ok(()), + } + } + + /// Check if no RP ID is associated with the token permission, or it matches the hash. + pub fn has_no_or_rp_id_hash_permission( + &self, + rp_id_hash: &[u8], + ) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id_hash != Sha256::hash(p.as_bytes()) => { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + _ => Ok(()), + } + } + + /// Check if the passed RP ID is associated with the token permission. + /// + /// If no RP ID is associated, associate the passed RP ID as a side effect. + pub fn require_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => { + self.permissions_rp_id = Some(String::from(rp_id)); + Ok(()) + } + _ => Ok(()), } - Ok(()) } #[cfg(test)] @@ -1150,24 +1176,65 @@ mod test { } #[test] - fn test_has_no_permission_rp_id() { + fn test_has_no_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(())); - assert_eq!(pin_protocol_v1.permissions_rp_id, None,); + assert_eq!(pin_protocol_v1.has_no_rp_id_permission(), Ok(())); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_permission_rp_id(), + pin_protocol_v1.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] - fn test_has_permission_for_rp_id() { + fn test_has_no_or_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), + pin_protocol_v1.has_no_or_rp_id_permission("example.com"), + Ok(()) + ); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_permission("example.com"), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_permission("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_has_no_or_rp_id_hash_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let rp_id_hash = Sha256::hash(b"example.com"); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&[0x4A; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_require_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!( + pin_protocol_v1.require_rp_id_permission("example.com"), Ok(()) ); assert_eq!( @@ -1175,11 +1242,11 @@ mod test { Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), + pin_protocol_v1.require_rp_id_permission("example.com"), Ok(()) ); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("counter-example.com"), + pin_protocol_v1.require_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b982922..6231e3c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -151,7 +151,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - fn find_credential_item( + pub fn find_credential_item( &self, credential_id: &[u8], ) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> { From 44b7c3cdc1e4926531f19332be4b5512ce6895ba Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 4 Feb 2021 21:26:00 +0100 Subject: [PATCH 60/86] dummy implementation for enterprise attestation --- src/ctap/command.rs | 8 +++--- src/ctap/config_command.rs | 45 ++++++++++++++++++++++++++++- src/ctap/data_formats.rs | 34 ++++++++++++++++++++++ src/ctap/mod.rs | 58 +++++++++++++++++++++++++++++++++----- src/ctap/response.rs | 5 ++++ src/ctap/storage.rs | 29 +++++++++++++++++++ src/ctap/storage/key.rs | 5 +++- 7 files changed, 171 insertions(+), 13 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index a76254a..eb16a1f 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -161,7 +161,7 @@ pub struct AuthenticatorMakeCredentialParameters { pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, - pub enterprise_attestation: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -219,7 +219,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { 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()?; - let enterprise_attestation = enterprise_attestation.map(extract_bool).transpose()?; + let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -601,7 +601,7 @@ mod test { 0x05 => cbor_array![], 0x08 => vec![0x12, 0x34], 0x09 => 1, - 0x0A => true, + 0x0A => 2, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -635,7 +635,7 @@ mod test { options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), - enterprise_attestation: Some(true), + enterprise_attestation: Some(2), }; assert_eq!( diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 5e4daf3..351ac1e 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,15 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::command::AuthenticatorConfigParameters; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::{check_pin_uv_auth_protocol, ENTERPRISE_ATTESTATION_MODE}; use alloc::vec; +/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig. +fn process_enable_enterprise_attestation( + persistent_store: &mut PersistentStore, +) -> Result { + if ENTERPRISE_ATTESTATION_MODE.is_some() { + persistent_store.enable_enterprise_attestation()?; + Ok(ResponseData::AuthenticatorConfig) + } else { + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } +} + /// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, @@ -85,6 +97,9 @@ pub fn process_config( } match sub_command { + ConfigSubCommand::EnableEnterpriseAttestation => { + process_enable_enterprise_attestation(persistent_store) + } ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { process_set_min_pin_length(persistent_store, params) @@ -101,6 +116,34 @@ mod test { use super::*; use crypto::rng256::ThreadRng256; + #[test] + fn test_process_enable_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::EnableEnterpriseAttestation, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + + if ENTERPRISE_ATTESTATION_MODE.is_some() { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.enterprise_attestation(), Ok(true)); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 1992469..9f4b68c 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -939,6 +939,24 @@ impl From for cbor::Value { } } +#[derive(Debug, PartialEq)] +pub enum EnterpriseAttestationMode { + VendorFacilitated = 0x01, + PlatformManaged = 0x02, +} + +impl TryFrom for EnterpriseAttestationMode { + type Error = Ctap2StatusCode; + + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(EnterpriseAttestationMode::VendorFacilitated), + 2 => Ok(EnterpriseAttestationMode::PlatformManaged), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialManagementSubCommand { @@ -1795,6 +1813,22 @@ mod test { assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); } + #[test] + fn test_from_enterprise_attestation_mode() { + assert_eq!( + EnterpriseAttestationMode::try_from(1), + Ok(EnterpriseAttestationMode::VendorFacilitated), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(2), + Ok(EnterpriseAttestationMode::PlatformManaged), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(3), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); + } + #[test] fn test_from_into_cred_management_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ab66177..eeb43bc 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -36,10 +36,10 @@ use self::command::{ use self::config_command::process_config; use self::credential_management::process_credential_management; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions, - PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, - PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, - SignatureAlgorithm, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, + GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; @@ -61,6 +61,7 @@ use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; use cbor::cbor_map_options; +use core::convert::TryFrom; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; @@ -86,6 +87,18 @@ const USE_BATCH_ATTESTATION: bool = false; // solution is a compromise to be compatible with U2F and not wasting storage. const USE_SIGNATURE_COUNTER: bool = true; pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; +// This flag allows usage of enterprise attestation. For privacy reasons, it is +// disabled by default. You can choose between +// - EnterpriseAttestationMode::VendorFacilitated, +// - EnterpriseAttestationMode::PlatformManaged. +// For VendorFacilitated, choose an appriopriate ENTERPRISE_RP_ID_LIST. +// To enable the feature, send the subcommand enableEnterpriseAttestation in +// AuthenticatorConfig. An enterprise might want to customize the type of +// attestation that is used. OpenSK defaults to batch attestation. Configuring +// individual certificates then makes authenticators identifiable. Do NOT set +// USE_BATCH_ATTESTATION to true at the same time in this case! +pub const ENTERPRISE_ATTESTATION_MODE: Option = None; +const ENTERPRISE_RP_ID_LIST: Vec = Vec::new(); // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -562,7 +575,7 @@ where options, pin_uv_auth_param, pin_uv_auth_protocol, - enterprise_attestation: _, + enterprise_attestation, } = make_credential_params; self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; @@ -572,6 +585,26 @@ where } let rp_id = rp.rp_id; + let ep_att = if let Some(enterprise_attestation) = enterprise_attestation { + let authenticator_mode = + ENTERPRISE_ATTESTATION_MODE.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + if !self.persistent_store.enterprise_attestation()? { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + match ( + EnterpriseAttestationMode::try_from(enterprise_attestation)?, + authenticator_mode, + ) { + ( + EnterpriseAttestationMode::PlatformManaged, + EnterpriseAttestationMode::PlatformManaged, + ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id), + _ => true, + } + } else { + false + }; + let mut cred_protect_policy = extensions.cred_protect; if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) @@ -723,7 +756,7 @@ where let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - let (signature, x5c) = if USE_BATCH_ATTESTATION { + let (signature, x5c) = if USE_BATCH_ATTESTATION || ep_att { let attestation_private_key = self .persistent_store .attestation_private_key()? @@ -750,11 +783,13 @@ where x5c, ecdaa_key_id: None, }; + let ep_att = if ep_att { Some(true) } else { None }; Ok(ResponseData::AuthenticatorMakeCredential( AuthenticatorMakeCredentialResponse { fmt: String::from("packed"), auth_data, att_stmt: attestation_statement, + ep_att, large_blob_key, }, )) @@ -1026,6 +1061,12 @@ where options_map.insert(String::from("up"), true); options_map.insert(String::from("pinUvAuthToken"), true); options_map.insert(String::from("largeBlobs"), true); + if ENTERPRISE_ATTESTATION_MODE.is_some() { + options_map.insert( + String::from("ep"), + self.persistent_store.enterprise_attestation()?, + ); + } options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); @@ -1227,6 +1268,7 @@ mod test { fmt, auth_data, att_stmt, + ep_att, large_blob_key, } = make_credential_response; // The expected response is split to only assert the non-random parts. @@ -1247,6 +1289,7 @@ mod test { &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], expected_extension_cbor ); + assert!(ep_att.is_none()); assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); assert_eq!(large_blob_key, None); } @@ -1276,12 +1319,13 @@ mod test { String::from("largeBlobKey"), ]], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), - 0x04 => cbor_map! { + 0x04 => cbor_map_options! { "rk" => true, "clientPin" => false, "up" => true, "pinUvAuthToken" => true, "largeBlobs" => true, + "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), "authnrCfg" => true, "credMgmt" => true, "setMinPINLength" => true, diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 093d4c9..b6b3d25 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -61,6 +61,7 @@ pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, pub att_stmt: PackedAttestationStatement, + pub ep_att: Option, pub large_blob_key: Option>, } @@ -70,6 +71,7 @@ impl From for cbor::Value { fmt, auth_data, att_stmt, + ep_att, large_blob_key, } = make_credential_response; @@ -77,6 +79,7 @@ impl From for cbor::Value { 0x01 => fmt, 0x02 => auth_data, 0x03 => att_stmt, + 0x04 => ep_att, 0x05 => large_blob_key, } } @@ -320,6 +323,7 @@ mod test { fmt: "packed".to_string(), auth_data: vec![0xAD], att_stmt, + ep_att: Some(true), large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = @@ -328,6 +332,7 @@ mod test { 0x01 => "packed", 0x02 => vec![0xAD], 0x03 => cbor_packed_attestation_statement, + 0x04 => true, 0x05 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b982922..c38146a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,6 +610,23 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + + /// Returns whether enterprise attestation is enabled. + pub fn enterprise_attestation(&self) -> Result { + match self.store.find(key::ENTERPRISE_ATTESTATION)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks enterprise attestation as enabled. + pub fn enable_enterprise_attestation(&mut self) -> Result<(), Ctap2StatusCode> { + if !self.enterprise_attestation()? { + self.store.insert(key::ENTERPRISE_ATTESTATION, &[])?; + } + Ok(()) + } } impl From for Ctap2StatusCode { @@ -1308,6 +1325,18 @@ mod test { assert!(!persistent_store.has_force_pin_change().unwrap()); } + #[test] + fn test_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.enterprise_attestation().unwrap()); + assert_eq!(persistent_store.enable_enterprise_attestation(), Ok(())); + assert!(persistent_store.enterprise_attestation().unwrap()); + persistent_store.reset(&mut rng).unwrap(); + assert!(!persistent_store.enterprise_attestation().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 2093685..dd9f67e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -93,7 +93,10 @@ make_partition! { /// The stored large blob can be too big for one key, so it has to be sharded. LARGE_BLOB_SHARDS = 2000..2004; - /// If this entry exists and equals 1, the PIN needs to be changed. + /// If this entry exists and is empty, enterprise attestation is enabled. + ENTERPRISE_ATTESTATION = 2039; + + /// If this entry exists and is empty, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; /// The secret of the CredRandom feature. From 53e05913634fe93bfb9887b2eb354fa56baeba86 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 4 Feb 2021 21:33:01 +0100 Subject: [PATCH 61/86] adds some documenation for enterprise attestation --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index da46d7f..92ddac0 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,9 @@ a few things you can personalize: length. 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. +1. Implement enterprise attestation. This can be as easy as setting + ENTERPRISE_ATTESTATION_MODE in `ctap/mod.rs`. If you want to use a different + attestation type than batch attestation, you have to implement it first. ### 3D printed enclosure From 49cccfd270aa7fe0bfdca13ac9959d580e1ec8db Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 11:23:12 +0100 Subject: [PATCH 62/86] correct const arrays of strings --- src/ctap/data_formats.rs | 4 ++++ src/ctap/mod.rs | 4 ++-- src/ctap/storage.rs | 25 ++++++++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9f4b68c..04e9a36 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -1815,6 +1815,10 @@ mod test { #[test] fn test_from_enterprise_attestation_mode() { + assert_eq!( + EnterpriseAttestationMode::try_from(0), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); assert_eq!( EnterpriseAttestationMode::try_from(1), Ok(EnterpriseAttestationMode::VendorFacilitated), diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index eeb43bc..b9a88b4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -98,7 +98,7 @@ pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // individual certificates then makes authenticators identifiable. Do NOT set // USE_BATCH_ATTESTATION to true at the same time in this case! pub const ENTERPRISE_ATTESTATION_MODE: Option = None; -const ENTERPRISE_RP_ID_LIST: Vec = Vec::new(); +const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -598,7 +598,7 @@ where ( EnterpriseAttestationMode::PlatformManaged, EnterpriseAttestationMode::PlatformManaged, - ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id), + ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id.as_str()), _ => true, } } else { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index c38146a..777f71f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -56,7 +56,7 @@ const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); +const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[]; // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; @@ -439,12 +439,17 @@ impl PersistentStore { /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { - let rp_ids = self - .store - .find(key::MIN_PIN_LENGTH_RP_IDS)? - .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { - deserialize_min_pin_length_rp_ids(&value) - }); + let rp_ids = self.store.find(key::MIN_PIN_LENGTH_RP_IDS)?.map_or_else( + || { + Some( + DEFAULT_MIN_PIN_LENGTH_RP_IDS + .iter() + .map(|&s| String::from(s)) + .collect(), + ) + }, + |value| deserialize_min_pin_length_rp_ids(&value), + ); debug_assert!(rp_ids.is_some()); Ok(rp_ids.unwrap_or_default()) } @@ -455,7 +460,8 @@ impl PersistentStore { min_pin_length_rp_ids: Vec, ) -> Result<(), Ctap2StatusCode> { let mut min_pin_length_rp_ids = min_pin_length_rp_ids; - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(*rp_id); if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } @@ -1203,7 +1209,8 @@ mod test { persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = rp_id.to_string().to_string(); if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } From 502006e29ead0e39425e4707b3b323a950c38c7f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 11:57:47 +0100 Subject: [PATCH 63/86] fix string conversion style --- src/ctap/storage.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 777f71f..f2f8220 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -460,8 +460,8 @@ impl PersistentStore { min_pin_length_rp_ids: Vec, ) -> Result<(), Ctap2StatusCode> { let mut min_pin_length_rp_ids = min_pin_length_rp_ids; - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { - let rp_id = String::from(*rp_id); + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } @@ -1209,8 +1209,8 @@ mod test { persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { - let rp_id = rp_id.to_string().to_string(); + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } From 604f0848157f660dff850b190f1fc42375e4fcb4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 14:52:38 +0100 Subject: [PATCH 64/86] rename require_ to ensure --- src/ctap/mod.rs | 4 ++-- src/ctap/pin_protocol_v1.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 0579f62..4d153f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -638,7 +638,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; + self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -923,7 +923,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; + self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; UV_FLAG } None => { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index d95b5d1..3b00a35 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -543,7 +543,7 @@ impl PinProtocolV1 { /// Check if the passed RP ID is associated with the token permission. /// /// If no RP ID is associated, associate the passed RP ID as a side effect. - pub fn require_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { match &self.permissions_rp_id { Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), None => { @@ -1230,11 +1230,11 @@ mod test { } #[test] - fn test_require_rp_id_permission() { + fn test_ensure_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); assert_eq!( - pin_protocol_v1.require_rp_id_permission("example.com"), + pin_protocol_v1.ensure_rp_id_permission("example.com"), Ok(()) ); assert_eq!( @@ -1242,11 +1242,11 @@ mod test { Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.require_rp_id_permission("example.com"), + pin_protocol_v1.ensure_rp_id_permission("example.com"), Ok(()) ); assert_eq!( - pin_protocol_v1.require_rp_id_permission("counter-example.com"), + pin_protocol_v1.ensure_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } From f90d43a6a1e8c27e7bef162f0d67d84e732d12e2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 18:43:27 +0100 Subject: [PATCH 65/86] implements alwaysUv and makeCredUvNotRqd --- src/ctap/apdu.rs | 1 + src/ctap/config_command.rs | 105 ++++++++++++++++++++++++++++++++++- src/ctap/ctap1.rs | 21 +++++++ src/ctap/hid/mod.rs | 2 +- src/ctap/large_blobs.rs | 2 +- src/ctap/mod.rs | 110 ++++++++++++++++++++++++++++++++++--- src/ctap/storage.rs | 30 ++++++++++ src/ctap/storage/key.rs | 3 + 8 files changed, 262 insertions(+), 12 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index f12ded2..455e574 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -29,6 +29,7 @@ pub enum ApduStatusCode { SW_WRONG_DATA = 0x6a_80, SW_WRONG_LENGTH = 0x67_00, SW_COND_USE_NOT_SATISFIED = 0x69_85, + SW_COMMAND_NOT_ALLOWED = 0x69_86, SW_FILE_NOT_FOUND = 0x6a_82, SW_INCORRECT_P1P2 = 0x6a_86, /// Instruction code not supported or invalid diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 5e4daf3..edff8f0 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -21,6 +21,21 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use alloc::vec; +/// The specification mandates that authenticators support users disabling +/// alwaysUv unless required not to by specific external certifications. +const CAN_DISABLE_ALWAYS_UV: bool = true; + +/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig. +fn process_toggle_always_uv( + persistent_store: &mut PersistentStore, +) -> Result { + if !CAN_DISABLE_ALWAYS_UV && persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); + } + persistent_store.toggle_always_uv()?; + Ok(ResponseData::AuthenticatorConfig) +} + /// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, @@ -66,7 +81,11 @@ pub fn process_config( pin_uv_auth_protocol, } = params; - if persistent_store.pin_hash()?.is_some() { + let enforce_uv = match sub_command { + ConfigSubCommand::ToggleAlwaysUv => false, + _ => true, + } && persistent_store.has_always_uv()?; + if persistent_store.pin_hash()?.is_some() || enforce_uv { // TODO(kaczmarczyck) The error code is specified inconsistently with other commands. check_pin_uv_auth_protocol(pin_uv_auth_protocol) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; @@ -85,6 +104,7 @@ pub fn process_config( } match sub_command { + ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(persistent_store), ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { process_set_min_pin_length(persistent_store, params) @@ -101,6 +121,89 @@ mod test { use super::*; use crypto::rng256::ThreadRng256; + #[test] + fn test_process_toggle_always_uv() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(persistent_store.has_always_uv().unwrap()); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + if CAN_DISABLE_ALWAYS_UV { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + assert!(persistent_store.has_always_uv().unwrap()); + } + } + + #[test] + fn test_process_toggle_always_uv_with_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let pin_uv_auth_param = Some(vec![ + 0x99, 0xBA, 0x0A, 0x57, 0x9D, 0x95, 0x5A, 0x44, 0xE3, 0x77, 0xCF, 0x95, 0x51, 0x3F, + 0xFD, 0xBE, + ]); + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param: pin_uv_auth_param.clone(), + pin_uv_auth_protocol: Some(1), + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(persistent_store.has_always_uv().unwrap()); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + if CAN_DISABLE_ALWAYS_UV { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + assert!(persistent_store.has_always_uv().unwrap()); + } + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 09a3c6c..d5e60ee 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -189,6 +189,12 @@ impl Ctap1Command { R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>, { + if !ctap_state + .allows_ctap1() + .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)? + { + return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED); + } let command = U2fCommand::try_from(message)?; match command { U2fCommand::Register { @@ -398,6 +404,21 @@ mod test { message } + #[test] + fn test_process_allowed() { + let mut rng = ThreadRng256 {}; + let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); + ctap_state.persistent_store.toggle_always_uv().unwrap(); + + let application = [0x0A; 32]; + let message = create_register_message(&application); + ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); + ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED)); + } + #[test] fn test_process_register() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 71bd7c8..03cb637 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -177,7 +177,7 @@ impl CtapHid { match message.cmd { // CTAP specification (version 20190130) section 8.1.9.1.1 CtapHid::COMMAND_MSG => { - // If we don't have CTAP1 backward compatibilty, this command in invalid. + // If we don't have CTAP1 backward compatibilty, this command is invalid. #[cfg(not(feature = "with_ctap1"))] return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index ab38df0..f84c1a9 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -88,7 +88,7 @@ impl LargeBlobs { if offset != self.expected_next_offset { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ); } - if persistent_store.pin_hash()?.is_some() { + if persistent_store.pin_hash()?.is_some() || persistent_store.has_always_uv()? { let pin_uv_auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; // TODO(kaczmarczyck) Error codes for PIN protocol differ across commands. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ab66177..8b90b68 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -341,6 +341,14 @@ where Ok(()) } + // Returns whether CTAP1 commands are currently supported. + // If alwaysUv is enabled and the authenticator does not support internal UV, + // CTAP1 needs to be disabled. + #[cfg(feature = "with_ctap1")] + pub fn allows_ctap1(&self) -> Result { + Ok(!self.persistent_store.has_always_uv()?) + } + // Encrypts the private key and relying party ID hash into a credential ID. Other // information, such as a user name, are not stored, because encrypted credential IDs // are used for credentials stored server-side. Also, we want the key handle to be @@ -642,7 +650,11 @@ where UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { - if self.persistent_store.pin_hash()?.is_some() { + if self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } + // Corresponds to makeCredUvNotRqd set to true. + if options.rk && self.persistent_store.pin_hash()?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); } if options.uv { @@ -927,8 +939,10 @@ where UV_FLAG } None => { + if self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } if options.uv { - // The specification (inconsistently) wants CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } 0x00 @@ -1017,6 +1031,23 @@ where } fn process_get_info(&self) -> Result { + let has_always_uv = self.persistent_store.has_always_uv()?; + #[cfg(feature = "with_ctap1")] + let mut versions = vec![ + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]; + #[cfg(feature = "with_ctap1")] + { + if !has_always_uv { + versions.insert(0, String::from(U2F_VERSION_STRING)) + } + } + #[cfg(not(feature = "with_ctap1"))] + let versions = vec![ + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]; let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); options_map.insert( @@ -1029,15 +1060,11 @@ where options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); - options_map.insert(String::from("makeCredUvNotRqd"), true); + options_map.insert(String::from("makeCredUvNotRqd"), !has_always_uv); + options_map.insert(String::from("alwaysUv"), has_always_uv); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { - versions: vec![ - #[cfg(feature = "with_ctap1")] - String::from(U2F_VERSION_STRING), - String::from(FIDO2_VERSION_STRING), - String::from(FIDO2_1_VERSION_STRING), - ], + versions, extensions: Some(vec![ String::from("hmac-secret"), String::from("credProtect"), @@ -1286,6 +1313,7 @@ mod test { "credMgmt" => true, "setMinPINLength" => true, "makeCredUvNotRqd" => true, + "alwaysUv" => false, }, 0x05 => MAX_MSG_SIZE as u64, 0x06 => cbor_array_vec![vec![1]], @@ -1727,6 +1755,70 @@ mod test { assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); } + #[test] + fn test_non_resident_process_make_credential_with_pin() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x70, + &[], + ); + } + + #[test] + fn test_resident_process_make_credential_with_pin() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let make_credential_params = create_minimal_make_credential_parameters(); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) + ); + } + + #[test] + fn test_process_make_credential_with_pin_always_uv() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + ctap_state.persistent_store.toggle_always_uv().unwrap(); + let make_credential_params = create_minimal_make_credential_parameters(); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) + ); + + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.pin_uv_auth_param = Some(vec![0xA4; 16]); + make_credential_params.pin_uv_auth_protocol = Some(1); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b982922..b586d6d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,6 +610,24 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + + /// Returns whether alwaysUv is enabled. + pub fn has_always_uv(&self) -> Result { + match self.store.find(key::ALWAYS_UV)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Enables alwaysUv, when disabled, and vice versa. + pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { + if self.has_always_uv()? { + Ok(self.store.remove(key::ALWAYS_UV)?) + } else { + Ok(self.store.insert(key::ALWAYS_UV, &[])?) + } + } } impl From for Ctap2StatusCode { @@ -1308,6 +1326,18 @@ mod test { assert!(!persistent_store.has_force_pin_change().unwrap()); } + #[test] + fn test_always_uv() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(!persistent_store.has_always_uv().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 2093685..9387931 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -93,6 +93,9 @@ make_partition! { /// The stored large blob can be too big for one key, so it has to be sharded. LARGE_BLOB_SHARDS = 2000..2004; + /// If this entry exists and is empty, alwaysUv is enabled. + ALWAYS_UV = 2038; + /// If this entry exists and equals 1, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; From 842c592c9fab0d6f7f0147c4112bb5333f4934b9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 18:51:56 +0100 Subject: [PATCH 66/86] adds changes to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index da46d7f..14c3dc6 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,12 @@ a few things you can personalize: length. 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. +1. If a certification (additional to FIDO's) requires that all requests are + protected with user verification, set `CAN_DISABLE_ALWAYS_UV` in + `ctap/config_command.rs` to `false`. In that case, consider deploying + authenticators after calling `toggleAlwaysUv` to activate the feature. + Alternatively, you could change `ctap/storage.rs` to set `alwaysUv` in its + initialization. ### 3D printed enclosure From 54e9da7a5b265349f6e27f26ce04887954673740 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 07:49:58 +0100 Subject: [PATCH 67/86] conditional allow instead of cfg not --- src/ctap/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8b90b68..748afe6 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1032,7 +1032,7 @@ where fn process_get_info(&self) -> Result { let has_always_uv = self.persistent_store.has_always_uv()?; - #[cfg(feature = "with_ctap1")] + #[cfg_attr(not(feature = "with_ctap1"), allow(unused_mut))] let mut versions = vec![ String::from(FIDO2_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING), @@ -1043,11 +1043,6 @@ where versions.insert(0, String::from(U2F_VERSION_STRING)) } } - #[cfg(not(feature = "with_ctap1"))] - let versions = vec![ - String::from(FIDO2_VERSION_STRING), - String::from(FIDO2_1_VERSION_STRING), - ]; let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); options_map.insert( From e941073a3157520dcf653679a824f45c08d17519 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 13:10:18 +0100 Subject: [PATCH 68/86] new test for attestation configuration --- src/ctap/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index e89b15b..da6ba72 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -2764,4 +2764,19 @@ mod test { )) ); } + + #[test] + #[allow(clippy::assertions_on_constants)] + /// Make sure that privacy guarantees are uphold. + /// + /// The current enterprise attestation implementation reuses batch + /// attestation. Enterprise attestation would imply a batch size of 1, but + /// batch attestation needs a batch size of at least 100k. To prevent + /// accidential misconfiguration, this test allows only one of the constants + /// to be set. If you implement your own enterprise attestation mechanism, + /// and you want batch attestation at the same time, feel free to proceed + /// carefully and remove this test. + fn check_attestation_privacy() { + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + } } From 88a3c0fc803ce2444226c62eb43d7f309a3800f4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 16:30:14 +0100 Subject: [PATCH 69/86] assert correct const usage in code --- src/ctap/mod.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index da6ba72..f2c3ea6 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -75,9 +75,11 @@ use libtock_drivers::crp; use libtock_drivers::timer::{ClockValue, Duration}; // This flag enables or disables basic attestation for FIDO2. U2F is unaffected by -// this setting. The basic attestation uses the signing key from key_material.rs -// as a batch key. Turn it on if you want attestation. In this case, be aware that -// it is your responsibility to generate your own key material and keep it secret. +// this setting. The basic attestation uses the signing key configured with a +// vendor command as a batch key. If you turn batch attestation on, be aware that +// it is your responsibility to safely generate and store the key material. Also, +// the batches must have size of at least 100k authenticators before using new +// key material. const USE_BATCH_ATTESTATION: bool = false; // The signature counter is currently implemented as a global counter, if you set // this flag to true. The spec strongly suggests to have per-credential-counters, @@ -96,7 +98,10 @@ pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // AuthenticatorConfig. An enterprise might want to customize the type of // attestation that is used. OpenSK defaults to batch attestation. Configuring // individual certificates then makes authenticators identifiable. Do NOT set -// USE_BATCH_ATTESTATION to true at the same time in this case! +// USE_BATCH_ATTESTATION to true at the same time in this case! The code asserts +// that you don't use the same key material for batch and enterprise attestation. +// If you implement your own enterprise attestation mechanism, and you want batch +// attestation at the same time, proceed carefully and remove the assertion. pub const ENTERPRISE_ATTESTATION_MODE: Option = None; const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; // Our credential ID consists of @@ -321,6 +326,11 @@ where check_user_presence: CheckUserPresence, now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { + #[allow(clippy::assertions_on_constants)] + { + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + } + let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); CtapState { @@ -2764,19 +2774,4 @@ mod test { )) ); } - - #[test] - #[allow(clippy::assertions_on_constants)] - /// Make sure that privacy guarantees are uphold. - /// - /// The current enterprise attestation implementation reuses batch - /// attestation. Enterprise attestation would imply a batch size of 1, but - /// batch attestation needs a batch size of at least 100k. To prevent - /// accidential misconfiguration, this test allows only one of the constants - /// to be set. If you implement your own enterprise attestation mechanism, - /// and you want batch attestation at the same time, feel free to proceed - /// carefully and remove this test. - fn check_attestation_privacy() { - assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); - } } From 160c83d242bc7d7609d3297074aa33eb013d6e7a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 17:53:30 +0100 Subject: [PATCH 70/86] changes always uv constant to a clearer version --- src/ctap/config_command.rs | 36 +++++++++++++++--------------------- src/ctap/mod.rs | 3 +++ src/ctap/storage.rs | 22 ++++++++++++++++------ 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index edff8f0..0d55717 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,24 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::command::AuthenticatorConfigParameters; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::{check_pin_uv_auth_protocol, ENFORCE_ALWAYS_UV}; use alloc::vec; -/// The specification mandates that authenticators support users disabling -/// alwaysUv unless required not to by specific external certifications. -const CAN_DISABLE_ALWAYS_UV: bool = true; - /// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig. fn process_toggle_always_uv( persistent_store: &mut PersistentStore, ) -> Result { - if !CAN_DISABLE_ALWAYS_UV && persistent_store.has_always_uv()? { + if ENFORCE_ALWAYS_UV { return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); } persistent_store.toggle_always_uv()?; @@ -148,15 +144,14 @@ mod test { }; let config_response = process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); - if CAN_DISABLE_ALWAYS_UV { - assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); - assert!(!persistent_store.has_always_uv().unwrap()); - } else { + if ENFORCE_ALWAYS_UV { assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) ); - assert!(persistent_store.has_always_uv().unwrap()); + } else { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); } } @@ -181,6 +176,13 @@ mod test { }; let config_response = process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + if ENFORCE_ALWAYS_UV { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + return; + } assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(persistent_store.has_always_uv().unwrap()); @@ -192,16 +194,8 @@ mod test { }; let config_response = process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); - if CAN_DISABLE_ALWAYS_UV { - assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); - assert!(!persistent_store.has_always_uv().unwrap()); - } else { - assert_eq!( - config_response, - Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) - ); - assert!(persistent_store.has_always_uv().unwrap()); - } + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); } fn create_min_pin_config_params( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 748afe6..2180eb7 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -129,6 +129,9 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa const DEFAULT_CRED_PROTECT: Option = None; // Maximum size stored with the credBlob extension. Must be at least 32. const MAX_CRED_BLOB_LENGTH: usize = 32; +// Enforce the alwaysUv option. With this constant set to true, commands require +// a PIN to be set up. The command toggleAlwaysUv will fail to disable alwaysUv. +pub const ENFORCE_ALWAYS_UV: bool = false; // Checks the PIN protocol parameter against all supported versions. pub fn check_pin_uv_auth_protocol( diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b586d6d..3b0fb9e 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -18,10 +18,10 @@ use crate::ctap::data_formats::{ extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialUserEntity, }; -use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; +use crate::ctap::{key_material, ENFORCE_ALWAYS_UV}; use crate::embedded_flash::{new_storage, Storage}; use alloc::string::String; use alloc::vec; @@ -613,6 +613,9 @@ impl PersistentStore { /// Returns whether alwaysUv is enabled. pub fn has_always_uv(&self) -> Result { + if ENFORCE_ALWAYS_UV { + return Ok(true); + } match self.store.find(key::ALWAYS_UV)? { None => Ok(false), Some(value) if value.is_empty() => Ok(true), @@ -622,6 +625,9 @@ impl PersistentStore { /// Enables alwaysUv, when disabled, and vice versa. pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { + if ENFORCE_ALWAYS_UV { + return Ok(()); + } if self.has_always_uv()? { Ok(self.store.remove(key::ALWAYS_UV)?) } else { @@ -1331,11 +1337,15 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - assert!(!persistent_store.has_always_uv().unwrap()); - assert_eq!(persistent_store.toggle_always_uv(), Ok(())); - assert!(persistent_store.has_always_uv().unwrap()); - assert_eq!(persistent_store.toggle_always_uv(), Ok(())); - assert!(!persistent_store.has_always_uv().unwrap()); + if ENFORCE_ALWAYS_UV { + assert!(persistent_store.has_always_uv().unwrap()); + } else { + assert!(!persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(!persistent_store.has_always_uv().unwrap()); + } } #[test] From b9072047b3267bf166215454e11cdf442e2bd2c8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 17:56:27 +0100 Subject: [PATCH 71/86] update README to new constant --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 14c3dc6..41f5378 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,8 @@ a few things you can personalize: 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. 1. If a certification (additional to FIDO's) requires that all requests are - protected with user verification, set `CAN_DISABLE_ALWAYS_UV` in - `ctap/config_command.rs` to `false`. In that case, consider deploying - authenticators after calling `toggleAlwaysUv` to activate the feature. - Alternatively, you could change `ctap/storage.rs` to set `alwaysUv` in its - initialization. + protected with user verification, set `ENFORCE_ALWAYS_UV` in + `ctap/config_mod.rs` to `true`. ### 3D printed enclosure From 6a31e06a5557fbdf2bd701638418a5dd7bca5138 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 21:54:22 +0100 Subject: [PATCH 72/86] move some logic into storage.rs --- src/ctap/config_command.rs | 6 ++---- src/ctap/mod.rs | 2 +- src/ctap/storage.rs | 6 +++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index b4c7cc0..c65fc56 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -18,7 +18,7 @@ use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::{check_pin_uv_auth_protocol, ENFORCE_ALWAYS_UV, ENTERPRISE_ATTESTATION_MODE}; +use super::{check_pin_uv_auth_protocol, ENTERPRISE_ATTESTATION_MODE}; use alloc::vec; /// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig. @@ -37,9 +37,6 @@ fn process_enable_enterprise_attestation( fn process_toggle_always_uv( persistent_store: &mut PersistentStore, ) -> Result { - if ENFORCE_ALWAYS_UV { - return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); - } persistent_store.toggle_always_uv()?; Ok(ResponseData::AuthenticatorConfig) } @@ -130,6 +127,7 @@ pub fn process_config( #[cfg(test)] mod test { use super::*; + use crate::ctap::ENFORCE_ALWAYS_UV; use crypto::rng256::ThreadRng256; #[test] diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 85deafe..18b0345 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -148,7 +148,7 @@ const DEFAULT_CRED_PROTECT: Option = None; // Maximum size stored with the credBlob extension. Must be at least 32. const MAX_CRED_BLOB_LENGTH: usize = 32; // Enforce the alwaysUv option. With this constant set to true, commands require -// a PIN to be set up. The command toggleAlwaysUv will fail to disable alwaysUv. +// a PIN to be set up. alwaysUv can not be disabled by commands. pub const ENFORCE_ALWAYS_UV: bool = false; // Checks the PIN protocol parameter against all supported versions. diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a8be6f1..b1aa4ec 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -649,7 +649,7 @@ impl PersistentStore { /// Enables alwaysUv, when disabled, and vice versa. pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { if ENFORCE_ALWAYS_UV { - return Ok(()); + return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); } if self.has_always_uv()? { Ok(self.store.remove(key::ALWAYS_UV)?) @@ -1375,6 +1375,10 @@ mod test { if ENFORCE_ALWAYS_UV { assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!( + persistent_store.toggle_always_uv(), + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); } else { assert!(!persistent_store.has_always_uv().unwrap()); assert_eq!(persistent_store.toggle_always_uv(), Ok(())); From f11a838cc7d6ee916ed35cdbcde0862c7cbfba7b Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Fri, 19 Feb 2021 14:20:23 +0100 Subject: [PATCH 73/86] Improved documentation for customization (#287) * move constants to new file, and update documentation * documentation improvements, deploy checks tests * fix pylint * improved code style * swap build and check --- README.md | 52 ++----- deploy.py | 6 + src/ctap/command.rs | 7 +- src/ctap/config_command.rs | 2 +- src/ctap/customization.rs | 269 +++++++++++++++++++++++++++++++++++++ src/ctap/data_formats.rs | 12 ++ src/ctap/mod.rs | 52 +------ src/ctap/storage.rs | 46 ++----- src/ctap/storage/key.rs | 2 +- 9 files changed, 319 insertions(+), 129 deletions(-) create mode 100644 src/ctap/customization.rs diff --git a/README.md b/README.md index d6c47f5..6abf799 100644 --- a/README.md +++ b/README.md @@ -92,45 +92,19 @@ If you build your own security key, depending on the hardware you use, there are a few things you can personalize: 1. If you have multiple buttons, choose the buttons responsible for user - presence in `main.rs`. -1. Decide whether you want to use batch attestation. There is a boolean flag in - `ctap/mod.rs`. It is mandatory for U2F, and you can create your own - self-signed certificate. The flag is used for FIDO2 and has some privacy - implications. Please check - [WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more - information. -1. Decide whether you want to use signature counters. Currently, only global - signature counters are implemented, as they are the default option for U2F. - The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy - preserving solution is individual or no signature counters. Again, please - check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for - documentation. -1. Depending on your available flash storage, choose an appropriate maximum - number of supported resident keys and number of pages in `ctap/storage.rs`. -1. Change the default level for the credProtect extension in `ctap/mod.rs`. - When changing the default, resident credentials become undiscoverable without - user verification. This helps privacy, but can make usage less comfortable - for credentials that need less protection. -1. Increase the default minimum length for PINs in `ctap/storage.rs`. - The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer - PINs can help establish trust between users and relying parties. It makes - user verification harder to break, but less convenient. - NIST recommends at least 6-digit PINs in section 5.1.9.1: - https://pages.nist.gov/800-63-3/sp800-63b.html - You can add relying parties to the list of readers of the minimum PIN length. -1. In an enterprise setting, you can adapt `DEFAULT_MIN_PIN_LENGTH_RP_IDS` and - `MAX_RP_IDS_LENGTH` for tuning the `minPinLength` extension. The former - allows some relying parties to read the minimum PIN length by default. The - latter allows storing more relying parties that may check the minimum PIN - length. -1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs - bigger than the default value. -1. Implement enterprise attestation. This can be as easy as setting - ENTERPRISE_ATTESTATION_MODE in `ctap/mod.rs`. If you want to use a different - attestation type than batch attestation, you have to implement it first. -1. If a certification (additional to FIDO's) requires that all requests are - protected with user verification, set `ENFORCE_ALWAYS_UV` in - `ctap/config_mod.rs` to `true`. + presence in `src/main.rs`. +1. If you have colored LEDs, like different blinking patterns and want to play + around with the code in `src/main.rs` more, take a look at e.g. `wink_leds`. +1. You find more options and documentation in `src/ctap/customization.rs`, + including: + - The default level for the credProtect extension. + - The default minimum PIN length, and what relying parties can set it. + - Whether you want to enforce alwaysUv. + - Settings for enterprise attestation. + - The maximum PIN retries. + - Whether you want to use batch attestation. + - Whether you want to use signature counters. + - Various constants to adapt to different hardware. ### 3D printed enclosure diff --git a/deploy.py b/deploy.py index e8d5ffd..0c8d998 100755 --- a/deploy.py +++ b/deploy.py @@ -352,6 +352,7 @@ class OpenSKInstaller: def build_opensk(self): info("Building OpenSK application") + self._check_invariants() self._build_app_or_example(is_example=False) def _build_app_or_example(self, is_example): @@ -390,6 +391,11 @@ class OpenSKInstaller: # Create a TAB file self.create_tab_file({props.arch: app_path}) + def _check_invariants(self): + print("Testing invariants in customization.rs...") + self.checked_command_output( + ["cargo", "test", "--features=std", "--lib", "customization"]) + def generate_crypto_materials(self, force_regenerate): has_error = subprocess.call([ os.path.join("tools", "gen_key_materials.sh"), diff --git a/src/ctap/command.rs b/src/ctap/command.rs index eb16a1f..78a80aa 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::customization::{MAX_CREDENTIAL_COUNT_IN_LIST, MAX_LARGE_BLOB_ARRAY_SIZE}; use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, @@ -22,18 +23,12 @@ use super::data_formats::{ }; use super::key_material; use super::status_code::Ctap2StatusCode; -use super::storage::MAX_LARGE_BLOB_ARRAY_SIZE; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; use cbor::destructure_cbor_map; use core::convert::TryFrom; -// Depending on your memory, you can use Some(n) to limit request sizes in -// MakeCredential and GetAssertion. This affects allowList and excludeList. -// You might also want to set the max credential size in process_get_info then. -pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; - // This constant is a consequence of the structure of messages. const MIN_LARGE_BLOB_LEN: usize = 17; diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index c65fc56..cf98889 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -127,7 +127,7 @@ pub fn process_config( #[cfg(test)] mod test { use super::*; - use crate::ctap::ENFORCE_ALWAYS_UV; + use crate::ctap::customization::ENFORCE_ALWAYS_UV; use crypto::rng256::ThreadRng256; #[test] diff --git a/src/ctap/customization.rs b/src/ctap/customization.rs new file mode 100644 index 0000000..1aebadf --- /dev/null +++ b/src/ctap/customization.rs @@ -0,0 +1,269 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This file contains all customizable constants. +//! +//! If you adapt them, make sure to run the tests before flashing the firmware. +//! Our deploy script enforces the invariants. + +use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode}; + +// ########################################################################### +// Constants for adjusting privacy and protection levels. +// ########################################################################### + +/// Changes the default level for the credProtect extension. +/// +/// You can change this value to one of the following for more privacy: +/// - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList +/// - CredentialProtectionPolicy::UserVerificationRequired +/// +/// UserVerificationOptionalWithCredentialIdList +/// Resident credentials are discoverable with +/// - an allowList, +/// - an excludeList, +/// - user verification. +/// +/// UserVerificationRequired +/// Resident credentials are discoverable with user verification only. +/// +/// This can improve privacy, but can make usage less comfortable. +pub const DEFAULT_CRED_PROTECT: Option = None; + +/// Sets the initial minimum PIN length in code points. +/// +/// # Invariant +/// +/// - The minimum PIN length must be at least 4. +/// - The minimum PIN length must be at most 63. +/// - DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty if MAX_RP_IDS_LENGTH is 0. +/// +/// Requiring longer PINs can help establish trust between users and relying +/// parties. It makes user verification harder to break, but less convenient. +/// NIST recommends at least 6-digit PINs in section 5.1.9.1: +/// https://pages.nist.gov/800-63-3/sp800-63b.html +/// +/// Reset reverts the minimum PIN length to this DEFAULT_MIN_PIN_LENGTH. +pub const DEFAULT_MIN_PIN_LENGTH: u8 = 4; + +/// Lists relying parties that can read the minimum PIN length. +/// +/// # Invariant +/// +/// - DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty if MAX_RP_IDS_LENGTH is 0 +/// +/// Only the RP IDs listed in DEFAULT_MIN_PIN_LENGTH_RP_IDS are allowed to read +/// the minimum PIN length with the minPinLength extension. +pub const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[]; + +/// Enforces the alwaysUv option. +/// +/// When setting to true, commands require a PIN. +/// Also, alwaysUv can not be disabled by commands. +/// +/// A certification (additional to FIDO Alliance's) might require enforcing +/// alwaysUv. Otherwise, users should have the choice to configure alwaysUv. +/// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here. +pub const ENFORCE_ALWAYS_UV: bool = false; + +/// Allows usage of enterprise attestation. +/// +/// # Invariant +/// +/// - Enterprise and batch attestation can not both be active. +/// - If the mode is VendorFacilitated, ENTERPRISE_RP_ID_LIST must be non-empty. +/// +/// For privacy reasons, it is disabled by default. You can choose between: +/// - EnterpriseAttestationMode::VendorFacilitated +/// - EnterpriseAttestationMode::PlatformManaged +/// +/// VendorFacilitated +/// Enterprise attestation is restricted to ENTERPRISE_RP_ID_LIST. Add your +/// enterprises domain, e.g. "example.com", to the list below. +/// +/// PlatformManaged +/// All relying parties can request an enterprise attestation. The authenticator +/// trusts the platform to filter requests. +/// +/// To enable the feature, send the subcommand enableEnterpriseAttestation in +/// AuthenticatorConfig. An enterprise might want to customize the type of +/// attestation that is used. OpenSK defaults to batch attestation. Configuring +/// individual certificates then makes authenticators identifiable. +/// +/// OpenSK prevents activating batch and enterprise attestation together. The +/// current implementation uses the same key material at the moment, and these +/// two modes have conflicting privacy guarantees. +/// If you implement your own enterprise attestation mechanism, and you want +/// batch attestation at the same time, proceed carefully and remove the +/// assertion. +pub const ENTERPRISE_ATTESTATION_MODE: Option = None; + +/// Lists relying party IDs that can perform enterprise attestation. +/// +/// # Invariant +/// +/// - If the mode is VendorFacilitated, ENTERPRISE_RP_ID_LIST must be non-empty. +/// +/// This list is only considered if the enterprise attestation mode is +/// VendorFacilitated. +pub const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; + +/// Sets the number of consecutive failed PINs before blocking interaction. +/// +/// # Invariant +/// +/// - CTAP2.0: Maximum PIN retries must be 8. +/// - CTAP2.1: Maximum PIN retries must be 8 at most. +/// +/// The fail retry counter is reset after entering the correct PIN. +pub const MAX_PIN_RETRIES: u8 = 8; + +/// Enables or disables basic attestation for FIDO2. +/// +/// # Invariant +/// +/// - Enterprise and batch attestation can not both be active (see above). +/// +/// The basic attestation uses the signing key configured with a vendor command +/// as a batch key. If you turn batch attestation on, be aware that it is your +/// responsibility to safely generate and store the key material. Also, the +/// batches must have size of at least 100k authenticators before using new key +/// material. +/// U2F is unaffected by this setting. +/// +/// https://www.w3.org/TR/webauthn/#attestation +pub const USE_BATCH_ATTESTATION: bool = false; + +/// Enables or disables signature counters. +/// +/// The signature counter is currently implemented as a global counter. +/// The specification strongly suggests to have per-credential counters. +/// Implementing those means you can't have an infinite amount of server-side +/// credentials anymore. Also, since counters need frequent writes on the +/// persistent storage, we might need a flash friendly implementation. This +/// solution is a compromise to be compatible with U2F and not wasting storage. +/// +/// https://www.w3.org/TR/webauthn/#signature-counter +pub const USE_SIGNATURE_COUNTER: bool = true; + +// ########################################################################### +// Constants for performance optimization or adapting to different hardware. +// +// Those constants may be modified before compilation to tune the behavior of +// the key. +// ########################################################################### + +/// Sets the maximum blob size stored with the credBlob extension. +/// +/// # Invariant +/// +/// - The length must be at least 32. +pub const MAX_CRED_BLOB_LENGTH: usize = 32; + +/// Limits the number of considered entries in credential lists. +/// +/// # Invariant +/// +/// - This value, if present, must be at least 1 (more is preferred). +/// +/// Depending on your memory, you can use Some(n) to limit request sizes in +/// MakeCredential and GetAssertion. This affects allowList and excludeList. +pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; + +/// Limits the size of largeBlobs the authenticator stores. +/// +/// # Invariant +/// +/// - The allowed size must be at least 1024. +/// - The array must fit into the shards reserved in storage/key.rs. +pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048; + +/// Limits the number of RP IDs that can change the minimum PIN length. +/// +/// # Invariant +/// +/// - If this value is 0, DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty. +/// +/// You can use this constant to have an upper limit in storage requirements. +/// This might be useful if you want to more reliably predict the remaining +/// storage. Stored string can still be of arbitrary length though, until RP ID +/// truncation is implemented. +/// Outside of memory considerations, you can set this value to 0 if only RP IDs +/// in DEFAULT_MIN_PIN_LENGTH_RP_IDS should be allowed to change the minimum PIN +/// length. +pub const MAX_RP_IDS_LENGTH: usize = 8; + +/// Sets the number of resident keys you can store. +/// +/// # Invariant +/// +/// - The storage key CREDENTIALS must fit at least this number of credentials. +/// +/// This value has implications on the flash lifetime, please see the +/// documentation for NUM_PAGES below. +pub const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; + +/// Sets the number of pages used for persistent storage. +/// +/// The number of pages should be at least 3 and at most what the flash can +/// hold. There should be no reason to put a small number here, except that the +/// latency of flash operations is linear in the number of pages. This may +/// improve in the future. Currently, using 20 pages gives between 20ms and +/// 240ms per operation. The rule of thumb is between 1ms and 12ms per +/// additional page. +/// +/// Limiting the number of resident keys permits to ensure a minimum number of +/// counter increments. +/// Let: +/// - P the number of pages (NUM_PAGES) +/// - K the maximum number of resident keys (MAX_SUPPORTED_RESIDENT_KEYS) +/// - S the maximum size of a resident key (about 500) +/// - C the number of erase cycles (10000) +/// - I the minimum number of counter increments +/// +/// We have: I = (P * 4084 - 5107 - K * S) / 8 * C +/// +/// With P=20 and K=150, we have I=2M which is enough for 500 increments per day +/// for 10 years. +pub const NUM_PAGES: usize = 20; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[allow(clippy::assertions_on_constants)] + fn test_invariants() { + // Two invariants are currently tested in different files: + // - storage.rs: if MAX_LARGE_BLOB_ARRAY_SIZE fits the shards + // - storage/key.rs: if MAX_SUPPORTED_RESIDENT_KEYS fits CREDENTIALS + assert!(DEFAULT_MIN_PIN_LENGTH >= 4); + assert!(DEFAULT_MIN_PIN_LENGTH <= 63); + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + if let Some(EnterpriseAttestationMode::VendorFacilitated) = ENTERPRISE_ATTESTATION_MODE { + assert!(!ENTERPRISE_RP_ID_LIST.is_empty()); + } else { + assert!(ENTERPRISE_RP_ID_LIST.is_empty()); + } + assert!(MAX_PIN_RETRIES <= 8); + assert!(MAX_CRED_BLOB_LENGTH >= 32); + if let Some(count) = MAX_CREDENTIAL_COUNT_IN_LIST { + assert!(count >= 1); + } + assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + if MAX_RP_IDS_LENGTH == 0 { + assert!(!DEFAULT_MIN_PIN_LENGTH_RP_IDS.is_empty()); + } + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 04e9a36..97ee858 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -504,11 +504,18 @@ impl TryFrom for SignatureAlgorithm { } } +/// The credProtect extension's policies for resident credentials. #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialProtectionPolicy { + /// The credential is always discoverable, as if it had no protection level. UserVerificationOptional = 0x01, + /// The credential is discoverable with + /// - an allowList, + /// - an excludeList, + /// - user verification. UserVerificationOptionalWithCredentialIdList = 0x02, + /// The credentials is discoverable with user verification only. UserVerificationRequired = 0x03, } @@ -939,9 +946,14 @@ impl From for cbor::Value { } } +/// The level of enterprise attestation allowed in MakeCredential. #[derive(Debug, PartialEq)] pub enum EnterpriseAttestationMode { + /// Enterprise attestation is restricted to a list of RP IDs. Add your + /// enterprises domain, e.g. "example.com", to the list below. VendorFacilitated = 0x01, + /// All relying parties can request an enterprise attestation. The authenticator + /// trusts the platform to filter requests. PlatformManaged = 0x02, } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 18b0345..f74b16e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -18,6 +18,7 @@ mod config_command; mod credential_management; #[cfg(feature = "with_ctap1")] mod ctap1; +mod customization; pub mod data_formats; pub mod hid; mod key_material; @@ -31,10 +32,14 @@ mod timed_permission; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, - MAX_CREDENTIAL_COUNT_IN_LIST, }; use self::config_command::process_config; use self::credential_management::process_credential_management; +use self::customization::{ + DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, + MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, + MAX_RP_IDS_LENGTH, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, +}; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, @@ -49,7 +54,7 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::{PersistentStore, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_RP_IDS_LENGTH}; +use self::storage::PersistentStore; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; @@ -74,36 +79,7 @@ use libtock_drivers::console::Console; use libtock_drivers::crp; use libtock_drivers::timer::{ClockValue, Duration}; -// This flag enables or disables basic attestation for FIDO2. U2F is unaffected by -// this setting. The basic attestation uses the signing key configured with a -// vendor command as a batch key. If you turn batch attestation on, be aware that -// it is your responsibility to safely generate and store the key material. Also, -// the batches must have size of at least 100k authenticators before using new -// key material. -const USE_BATCH_ATTESTATION: bool = false; -// The signature counter is currently implemented as a global counter, if you set -// this flag to true. The spec strongly suggests to have per-credential-counters, -// but it means you can't have an infinite amount of credentials anymore. Also, -// since this is the only piece of information that needs writing often, we might -// need a flash storage friendly way to implement this feature. The implemented -// solution is a compromise to be compatible with U2F and not wasting storage. -const USE_SIGNATURE_COUNTER: bool = true; pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; -// This flag allows usage of enterprise attestation. For privacy reasons, it is -// disabled by default. You can choose between -// - EnterpriseAttestationMode::VendorFacilitated, -// - EnterpriseAttestationMode::PlatformManaged. -// For VendorFacilitated, choose an appriopriate ENTERPRISE_RP_ID_LIST. -// To enable the feature, send the subcommand enableEnterpriseAttestation in -// AuthenticatorConfig. An enterprise might want to customize the type of -// attestation that is used. OpenSK defaults to batch attestation. Configuring -// individual certificates then makes authenticators identifiable. Do NOT set -// USE_BATCH_ATTESTATION to true at the same time in this case! The code asserts -// that you don't use the same key material for batch and enterprise attestation. -// If you implement your own enterprise attestation mechanism, and you want batch -// attestation at the same time, proceed carefully and remove the assertion. -pub const ENTERPRISE_ATTESTATION_MODE: Option = None; -const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -141,15 +117,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa cred_type: PublicKeyCredentialType::PublicKey, alg: SignatureAlgorithm::ES256, }; -// You can change this value to one of the following for more privacy. -// - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) -// - Some(CredentialProtectionPolicy::UserVerificationRequired) -const DEFAULT_CRED_PROTECT: Option = None; -// Maximum size stored with the credBlob extension. Must be at least 32. -const MAX_CRED_BLOB_LENGTH: usize = 32; -// Enforce the alwaysUv option. With this constant set to true, commands require -// a PIN to be set up. alwaysUv can not be disabled by commands. -pub const ENFORCE_ALWAYS_UV: bool = false; // Checks the PIN protocol parameter against all supported versions. pub fn check_pin_uv_auth_protocol( @@ -329,11 +296,6 @@ where check_user_presence: CheckUserPresence, now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { - #[allow(clippy::assertions_on_constants)] - { - assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); - } - let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); CtapState { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b1aa4ec..0f6657f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,14 +14,19 @@ mod key; +use crate::ctap::customization::{ + DEFAULT_MIN_PIN_LENGTH, DEFAULT_MIN_PIN_LENGTH_RP_IDS, ENFORCE_ALWAYS_UV, + MAX_LARGE_BLOB_ARRAY_SIZE, MAX_PIN_RETRIES, MAX_RP_IDS_LENGTH, MAX_SUPPORTED_RESIDENT_KEYS, + NUM_PAGES, +}; use crate::ctap::data_formats::{ extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialUserEntity, }; +use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; -use crate::ctap::{key_material, ENFORCE_ALWAYS_UV}; use crate::embedded_flash::{new_storage, Storage}; use alloc::string::String; use alloc::vec; @@ -33,35 +38,6 @@ use core::convert::TryInto; use crypto::rng256::Rng256; use persistent_store::{fragment, StoreUpdate}; -// Those constants may be modified before compilation to tune the behavior of the key. -// -// The number of pages should be at least 3 and at most what the flash can hold. There should be no -// reason to put a small number here, except that the latency of flash operations is linear in the -// number of pages. This may improve in the future. Currently, using 20 pages gives between 20ms and -// 240ms per operation. The rule of thumb is between 1ms and 12ms per additional page. -// -// Limiting the number of resident keys permits to ensure a minimum number of counter increments. -// Let: -// - P the number of pages (NUM_PAGES) -// - K the maximum number of resident keys (MAX_SUPPORTED_RESIDENT_KEYS) -// - S the maximum size of a resident key (about 500) -// - C the number of erase cycles (10000) -// - I the minimum number of counter increments -// -// We have: I = (P * 4084 - 5107 - K * S) / 8 * C -// -// With P=20 and K=150, we have I=2M which is enough for 500 increments per day for 10 years. -const NUM_PAGES: usize = 20; -const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; - -const MAX_PIN_RETRIES: u8 = 8; -const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[]; -// This constant is an attempt to limit storage requirements. If you don't set it to 0, -// the stored strings can still be unbounded, but that is true for all RP IDs. -pub const MAX_RP_IDS_LENGTH: usize = 8; -pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048; - /// Wrapper for master keys. pub struct MasterKeys { /// Master encryption key. @@ -825,7 +801,7 @@ mod test { let mut credential_ids = vec![]; for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { - let user_handle = i.to_ne_bytes().to_vec(); + let user_handle = (i as u32).to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); credential_ids.push(credential_source.credential_id.clone()); assert!(persistent_store.store_credential(credential_source).is_ok()); @@ -899,7 +875,7 @@ mod test { assert_eq!(persistent_store.count_credentials().unwrap(), 0); for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { - let user_handle = i.to_ne_bytes().to_vec(); + let user_handle = (i as u32).to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); @@ -948,7 +924,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { - let user_handle = i.to_ne_bytes().to_vec(); + let user_handle = (i as u32).to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); @@ -1247,10 +1223,6 @@ mod test { let mut rng = ThreadRng256 {}; let persistent_store = PersistentStore::new(&mut rng); - #[allow(clippy::assertions_on_constants)] - { - assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); - } assert!( MAX_LARGE_BLOB_ARRAY_SIZE <= persistent_store.store.max_value_length() diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 7c6bea5..38a2a8b 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -141,7 +141,7 @@ mod test { #[test] fn enough_credentials() { - use super::super::MAX_SUPPORTED_RESIDENT_KEYS; + use crate::ctap::customization::MAX_SUPPORTED_RESIDENT_KEYS; assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start); } From 5e9c32dff53c07e621c0dd3447c956e1b3109dd0 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:33:25 +0100 Subject: [PATCH 74/86] HKDF for CTAP2.1 (#290) * implements hkdf, both regular and FIDO specific * improved documentation * constant usage in function return type --- libraries/crypto/src/hkdf.rs | 226 +++++++++++++++++++++++++++++++++++ libraries/crypto/src/lib.rs | 1 + 2 files changed, 227 insertions(+) create mode 100644 libraries/crypto/src/hkdf.rs diff --git a/libraries/crypto/src/hkdf.rs b/libraries/crypto/src/hkdf.rs new file mode 100644 index 0000000..ee276a3 --- /dev/null +++ b/libraries/crypto/src/hkdf.rs @@ -0,0 +1,226 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::hmac::hmac_256; +use super::{Hash256, HashBlockSize64Bytes}; + +const HASH_SIZE: usize = 32; + +/// Computes the HKDF with empty salt and 256 bit (one block) output. +/// +/// # Arguments +/// +/// * `ikm` - Input keying material +/// * `info` - Optional context and application specific information +/// +/// This implementation is equivalent to the below hkdf, with `salt` set to the +/// default block of zeros and the output length l as 32. +pub fn hkdf_empty_salt_256(ikm: &[u8], info: &[u8]) -> [u8; HASH_SIZE] +where + H: Hash256 + HashBlockSize64Bytes, +{ + // Salt is a zero block here. + let prk = hmac_256::(&[0; HASH_SIZE], ikm); + // l is implicitly the block size, so we iterate exactly once. + let mut t = info.to_vec(); + t.push(1); + hmac_256::(&prk, t.as_slice()) +} + +/// Computes the HKDF. +/// +/// # Arguments +/// +/// * `salt` - Optional salt value (a non-secret random value) +/// * `ikm` - Input keying material +/// * `l` - Length of output keying material in octets +/// * `info` - Optional context and application specific information +/// +/// Defined in RFC: https://tools.ietf.org/html/rfc5869 +/// +/// `salt` and `info` can be be empty. `salt` then defaults to one block of +/// zeros of size `HASH_SIZE`. Argument order is taken from: +/// https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#pinProto2 +#[cfg(test)] +pub fn hkdf(salt: &[u8], ikm: &[u8], l: u8, info: &[u8]) -> Vec +where + H: Hash256 + HashBlockSize64Bytes, +{ + let prk = if salt.is_empty() { + hmac_256::(&[0; HASH_SIZE], ikm) + } else { + hmac_256::(salt, ikm) + }; + let mut t = vec![]; + let mut okm = vec![]; + for i in 0..(l as usize + HASH_SIZE - 1) / HASH_SIZE { + t.extend_from_slice(info); + t.push((i + 1) as u8); + t = hmac_256::(&prk, t.as_slice()).to_vec(); + okm.extend_from_slice(t.as_slice()); + } + okm.truncate(l as usize); + okm +} + +#[cfg(test)] +mod test { + use super::super::sha256::Sha256; + use super::*; + use arrayref::array_ref; + + #[test] + fn test_hkdf_sha256_vectors() { + // Test vectors taken from https://tools.ietf.org/html/rfc5869. + let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(); + let salt = hex::decode("000102030405060708090a0b0c").unwrap(); + let info = hex::decode("f0f1f2f3f4f5f6f7f8f9").unwrap(); + let l = 42; + let okm = hex::decode( + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865", + ) + .unwrap(); + assert_eq!( + hkdf::(salt.as_slice(), ikm.as_slice(), l, info.as_slice()), + okm + ); + + let ikm = hex::decode( + "000102030405060708090a0b0c0d0e0f\ + 101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f\ + 303132333435363738393a3b3c3d3e3f\ + 404142434445464748494a4b4c4d4e4f", + ) + .unwrap(); + let salt = hex::decode( + "606162636465666768696a6b6c6d6e6f\ + 707172737475767778797a7b7c7d7e7f\ + 808182838485868788898a8b8c8d8e8f\ + 909192939495969798999a9b9c9d9e9f\ + a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + ) + .unwrap(); + let info = hex::decode( + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\ + c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\ + d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\ + e0e1e2e3e4e5e6e7e8e9eaebecedeeef\ + f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + ) + .unwrap(); + let l = 82; + let okm = hex::decode( + "b11e398dc80327a1c8e7f78c596a4934\ + 4f012eda2d4efad8a050cc4c19afa97c\ + 59045a99cac7827271cb41c65e590e09\ + da3275600c2f09b8367793a9aca3db71\ + cc30c58179ec3e87c14c01d5c1f3434f\ + 1d87", + ) + .unwrap(); + assert_eq!( + hkdf::(salt.as_slice(), ikm.as_slice(), l, info.as_slice()), + okm + ); + + let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(); + let salt = hex::decode("").unwrap(); + let info = hex::decode("").unwrap(); + let l = 42; + let okm = hex::decode( + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8", + ) + .unwrap(); + assert_eq!( + hkdf::(salt.as_slice(), ikm.as_slice(), l, info.as_slice()), + okm + ); + } + + #[test] + fn test_hkdf_empty_salt_256_sha256_vectors() { + // Test vectors generated by pycryptodome using: + // HKDF(b'0', 32, b'', SHA256, context=b'\x00').hex() + let test_okms = [ + hex::decode("f9be72116cb97f41828210289caafeabde1f3dfb9723bf43538ab18f3666783a") + .unwrap(), + hex::decode("f50f964f5b94d62fd1da9356ab8662b0a0f5b8e36e277178b69b6ffecf50cf44") + .unwrap(), + hex::decode("fc8772ceb5592d67442dcb4353cdd28519e82d6e55b4cf664b5685252c2d2998") + .unwrap(), + hex::decode("62831b924839a180f53be5461eeea1b89dc21779f50142b5a54df0f0cc86d61a") + .unwrap(), + hex::decode("6991f00a12946a4e3b8315cdcf0132c2ca508fd17b769f08d1454d92d33733e0") + .unwrap(), + hex::decode("0f9bb7dddd1ec61f91d8c4f5369b5870f9d44c4ceabccca1b83f06fec115e4e3") + .unwrap(), + hex::decode("235367e2ab6cca2aba1a666825458dba6b272a215a2537c05feebe4b80dab709") + .unwrap(), + hex::decode("96e8edad661da48d1a133b38c255d33e05555bc9aa442579dea1cd8d8b8d2aef") + .unwrap(), + ]; + for (i, okm) in test_okms.iter().enumerate() { + // String of number i. + let ikm = i.to_string(); + // Byte i. + let info = [i as u8]; + assert_eq!( + &hkdf_empty_salt_256::(&ikm.as_bytes(), &info[..]), + array_ref!(okm, 0, 32) + ); + } + } + + #[test] + fn test_hkdf_length() { + let salt = []; + let mut input = Vec::new(); + for l in 0..128 { + assert_eq!( + hkdf::(&salt, input.as_slice(), l, input.as_slice()).len(), + l as usize + ); + input.push(b'A'); + } + } + + #[test] + fn test_hkdf_empty_salt() { + let salt = []; + let mut input = Vec::new(); + for l in 0..128 { + assert_eq!( + hkdf::(&salt, input.as_slice(), l, input.as_slice()), + hkdf::(&[0; 32], input.as_slice(), l, input.as_slice()) + ); + input.push(b'A'); + } + } + + #[test] + fn test_hkdf_compare_implementations() { + let salt = []; + let l = 32; + + let mut input = Vec::new(); + for _ in 0..128 { + assert_eq!( + hkdf::(&salt, input.as_slice(), l, input.as_slice()), + hkdf_empty_salt_256::(input.as_slice(), input.as_slice()) + ); + input.push(b'A'); + } + } +} diff --git a/libraries/crypto/src/lib.rs b/libraries/crypto/src/lib.rs index 7b35e99..da13180 100644 --- a/libraries/crypto/src/lib.rs +++ b/libraries/crypto/src/lib.rs @@ -22,6 +22,7 @@ pub mod cbc; mod ec; pub mod ecdh; pub mod ecdsa; +pub mod hkdf; pub mod hmac; pub mod rng256; pub mod sha256; From 351e6c12c6e09e92efdeef94deaaf5568451c090 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 4 Mar 2021 10:37:19 +0100 Subject: [PATCH 75/86] renames PIN protocol related variables to prepare PIN protocol v2 (#291) --- .../{pin_protocol_v1.rs => client_pin.rs} | 194 ++++++++---------- src/ctap/command.rs | 20 +- src/ctap/config_command.rs | 67 +++--- src/ctap/credential_management.rs | 130 ++++++------ src/ctap/large_blobs.rs | 90 +++----- src/ctap/mod.rs | 63 +++--- src/ctap/storage.rs | 2 +- 7 files changed, 246 insertions(+), 320 deletions(-) rename src/ctap/{pin_protocol_v1.rs => client_pin.rs} (88%) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/client_pin.rs similarity index 88% rename from src/ctap/pin_protocol_v1.rs rename to src/ctap/client_pin.rs index 3b00a35..ec309ed 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/client_pin.rs @@ -166,7 +166,7 @@ pub enum PinPermission { AuthenticatorConfiguration = 0x20, } -pub struct PinProtocolV1 { +pub struct ClientPin { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], consecutive_pin_mismatches: u8, @@ -174,11 +174,11 @@ pub struct PinProtocolV1 { permissions_rp_id: Option, } -impl PinProtocolV1 { - pub fn new(rng: &mut impl Rng256) -> PinProtocolV1 { +impl ClientPin { + pub fn new(rng: &mut impl Rng256) -> ClientPin { let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); let pin_uv_auth_token = rng.gen_uniform_u8x32(); - PinProtocolV1 { + ClientPin { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, @@ -395,14 +395,14 @@ impl PinProtocolV1 { Ok(response) } - pub fn process_subcommand( + pub fn process_command( &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { let AuthenticatorClientPinParameters { - pin_protocol, + pin_uv_auth_protocol, sub_command, key_agreement, pin_auth, @@ -412,7 +412,7 @@ impl PinProtocolV1 { permissions_rp_id, } = client_pin_params; - if pin_protocol != 1 { + if pin_uv_auth_protocol != 1 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } @@ -558,8 +558,8 @@ impl PinProtocolV1 { pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], - ) -> PinProtocolV1 { - PinProtocolV1 { + ) -> ClientPin { + ClientPin { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, @@ -647,13 +647,13 @@ mod test { let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let pin_hash_enc = vec![ 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, 0x99, 0x66, ]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -664,7 +664,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -677,9 +677,9 @@ mod test { 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, 0x99, 0x66, ]; - pin_protocol_v1.consecutive_pin_mismatches = 3; + client_pin.consecutive_pin_mismatches = 3; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -687,11 +687,11 @@ mod test { ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) ); - pin_protocol_v1.consecutive_pin_mismatches = 0; + client_pin.consecutive_pin_mismatches = 0; let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -702,7 +702,7 @@ mod test { let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -716,14 +716,14 @@ mod test { fn test_process_get_pin_retries() { let mut rng = ThreadRng256 {}; let persistent_store = PersistentStore::new(&mut rng); - let pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let client_pin = ClientPin::new(&mut rng); let expected_response = Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - pin_protocol_v1.process_get_pin_retries(&persistent_store), + client_pin.process_get_pin_retries(&persistent_store), expected_response ); } @@ -731,36 +731,28 @@ mod test { #[test] fn test_process_get_key_agreement() { let mut rng = ThreadRng256 {}; - let pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); + let client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); let expected_response = Ok(AuthenticatorClientPinResponse { key_agreement: Some(CoseKey::from(pk)), pin_token: None, retries: None, }); - assert_eq!( - pin_protocol_v1.process_get_key_agreement(), - expected_response - ); + assert_eq!(client_pin.process_get_key_agreement(), expected_response); } #[test] fn test_process_set_pin() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); assert_eq!( - pin_protocol_v1.process_set_pin( - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc - ), + client_pin.process_set_pin(&mut persistent_store, key_agreement, pin_auth, new_pin_enc), Ok(()) ); } @@ -770,9 +762,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); @@ -780,7 +772,7 @@ mod test { auth_param_data.extend(&pin_hash_enc); let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); assert_eq!( - pin_protocol_v1.process_change_pin( + client_pin.process_change_pin( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -793,7 +785,7 @@ mod test { let bad_pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.process_change_pin( + client_pin.process_change_pin( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -808,7 +800,7 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - pin_protocol_v1.process_change_pin( + client_pin.process_change_pin( &mut rng, &mut persistent_store, key_agreement, @@ -825,12 +817,12 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 + assert!(client_pin .process_get_pin_token( &mut rng, &mut persistent_store, @@ -841,7 +833,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.process_get_pin_token( + client_pin.process_get_pin_token( &mut rng, &mut persistent_store, key_agreement, @@ -857,13 +849,13 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); assert_eq!(persistent_store.force_pin_change(), Ok(())); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( - pin_protocol_v1.process_get_pin_token( + client_pin.process_get_pin_token( &mut rng, &mut persistent_store, key_agreement, @@ -878,12 +870,12 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 + assert!(client_pin .process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, @@ -893,14 +885,14 @@ mod test { Some(String::from("example.com")), ) .is_ok()); - assert_eq!(pin_protocol_v1.permissions, 0x03); + assert_eq!(client_pin.permissions, 0x03); assert_eq!( - pin_protocol_v1.permissions_rp_id, + client_pin.permissions_rp_id, Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -912,7 +904,7 @@ mod test { ); assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -925,7 +917,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement, @@ -943,13 +935,13 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); assert_eq!(persistent_store.force_pin_change(), Ok(())); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement, @@ -965,9 +957,9 @@ mod test { fn test_process() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 1, + pin_uv_auth_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: None, pin_auth: None, @@ -976,12 +968,12 @@ mod test { permissions: None, permissions_rp_id: None, }; - assert!(pin_protocol_v1 - .process_subcommand(&mut rng, &mut persistent_store, client_pin_params) + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, client_pin_params) .is_ok()); let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 2, + pin_uv_auth_protocol: 2, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: None, pin_auth: None, @@ -992,7 +984,7 @@ mod test { }; let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; assert_eq!( - pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params), + client_pin.process_command(&mut rng, &mut persistent_store, client_pin_params), Err(error_code) ); } @@ -1161,15 +1153,15 @@ mod test { #[test] fn test_has_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - pin_protocol_v1.permissions = 0x7F; + let mut client_pin = ClientPin::new(&mut rng); + client_pin.permissions = 0x7F; for permission in PinPermission::into_enum_iter() { - assert_eq!(pin_protocol_v1.has_permission(permission), Ok(())); + assert_eq!(client_pin.has_permission(permission), Ok(())); } - pin_protocol_v1.permissions = 0x00; + client_pin.permissions = 0x00; for permission in PinPermission::into_enum_iter() { assert_eq!( - pin_protocol_v1.has_permission(permission), + client_pin.has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1178,12 +1170,12 @@ mod test { #[test] fn test_has_no_rp_id_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - assert_eq!(pin_protocol_v1.has_no_rp_id_permission(), Ok(())); - assert_eq!(pin_protocol_v1.permissions_rp_id, None); - pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + assert_eq!(client_pin.permissions_rp_id, None); + client_pin.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_rp_id_permission(), + client_pin.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1191,19 +1183,13 @@ mod test { #[test] fn test_has_no_or_rp_id_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); + assert_eq!(client_pin.permissions_rp_id, None); + client_pin.permissions_rp_id = Some("example.com".to_string()); + assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); assert_eq!( - pin_protocol_v1.has_no_or_rp_id_permission("example.com"), - Ok(()) - ); - assert_eq!(pin_protocol_v1.permissions_rp_id, None); - pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); - assert_eq!( - pin_protocol_v1.has_no_or_rp_id_permission("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_no_or_rp_id_permission("another.example.com"), + client_pin.has_no_or_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1211,20 +1197,20 @@ mod test { #[test] fn test_has_no_or_rp_id_hash_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let rp_id_hash = Sha256::hash(b"example.com"); assert_eq!( - pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); - assert_eq!(pin_protocol_v1.permissions_rp_id, None); - pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!(client_pin.permissions_rp_id, None); + client_pin.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); assert_eq!( - pin_protocol_v1.has_no_or_rp_id_hash_permission(&[0x4A; 32]), + client_pin.has_no_or_rp_id_hash_permission(&[0x4A; 32]), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1232,21 +1218,15 @@ mod test { #[test] fn test_ensure_rp_id_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - pin_protocol_v1.ensure_rp_id_permission("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.permissions_rp_id, + client_pin.permissions_rp_id, Some(String::from("example.com")) ); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - pin_protocol_v1.ensure_rp_id_permission("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.ensure_rp_id_permission("counter-example.com"), + client_pin.ensure_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 78a80aa..8c5aaec 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -304,7 +304,7 @@ impl TryFrom for AuthenticatorGetAssertionParameters { #[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { - pub pin_protocol: u64, + pub pin_uv_auth_protocol: u64, pub sub_command: ClientPinSubCommand, pub key_agreement: Option, pub pin_auth: Option>, @@ -320,7 +320,7 @@ impl TryFrom for AuthenticatorClientPinParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 0x01 => pin_protocol, + 0x01 => pin_uv_auth_protocol, 0x02 => sub_command, 0x03 => key_agreement, 0x04 => pin_auth, @@ -331,7 +331,7 @@ impl TryFrom for AuthenticatorClientPinParameters { } = extract_map(cbor_value)?; } - let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; + let pin_uv_auth_protocol = extract_unsigned(ok_or_missing(pin_uv_auth_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?; let pin_auth = pin_auth.map(extract_byte_string).transpose()?; @@ -346,7 +346,7 @@ impl TryFrom for AuthenticatorClientPinParameters { let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; Ok(AuthenticatorClientPinParameters { - pin_protocol, + pin_uv_auth_protocol, sub_command, key_agreement, pin_auth, @@ -506,7 +506,7 @@ impl TryFrom for AuthenticatorAttestationMaterial { pub struct AuthenticatorCredentialManagementParameters { pub sub_command: CredentialManagementSubCommand, pub sub_command_params: Option, - pub pin_protocol: Option, + pub pin_uv_auth_protocol: Option, pub pin_auth: Option>, } @@ -518,7 +518,7 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { let { 0x01 => sub_command, 0x02 => sub_command_params, - 0x03 => pin_protocol, + 0x03 => pin_uv_auth_protocol, 0x04 => pin_auth, } = extract_map(cbor_value)?; } @@ -527,13 +527,13 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { let sub_command_params = sub_command_params .map(CredentialManagementSubCommandParameters::try_from) .transpose()?; - let pin_protocol = pin_protocol.map(extract_unsigned).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; let pin_auth = pin_auth.map(extract_byte_string).transpose()?; Ok(AuthenticatorCredentialManagementParameters { sub_command, sub_command_params, - pin_protocol, + pin_uv_auth_protocol, pin_auth, }) } @@ -706,7 +706,7 @@ mod test { AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); let expected_client_pin_parameters = AuthenticatorClientPinParameters { - pin_protocol: 1, + pin_uv_auth_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), pin_auth: Some(vec![0xBB]), @@ -765,7 +765,7 @@ mod test { let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(params), - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth: Some(vec![0x9A; 16]), }; diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index cf98889..4a7f868 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::client_pin::ClientPin; use super::command::AuthenticatorConfigParameters; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; -use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; @@ -76,7 +76,7 @@ fn process_set_min_pin_length( /// Processes the AuthenticatorConfig command. pub fn process_config( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, params: AuthenticatorConfigParameters, ) -> Result { let AuthenticatorConfigParameters { @@ -103,7 +103,7 @@ pub fn process_config( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !pin_protocol_v1.verify_pin_auth_token(&config_data, &auth_param) { + if !client_pin.verify_pin_auth_token(&config_data, &auth_param) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } } @@ -136,7 +136,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::EnableEnterpriseAttestation, @@ -144,8 +144,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENTERPRISE_ATTESTATION_MODE.is_some() { assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); @@ -164,7 +163,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, @@ -172,8 +171,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(persistent_store.has_always_uv().unwrap()); @@ -183,8 +181,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENFORCE_ALWAYS_UV { assert_eq!( config_response, @@ -202,7 +199,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -215,8 +212,7 @@ mod test { pin_uv_auth_param: pin_uv_auth_param.clone(), pin_uv_auth_protocol: Some(1), }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENFORCE_ALWAYS_UV { assert_eq!( config_response, @@ -233,8 +229,7 @@ mod test { pin_uv_auth_param, pin_uv_auth_protocol: Some(1), }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(!persistent_store.has_always_uv().unwrap()); } @@ -264,13 +259,12 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); // First, increase minimum PIN length from 4 to 6 without PIN auth. let min_pin_length = 6; let config_params = create_min_pin_config_params(min_pin_length, None); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); @@ -284,8 +278,7 @@ mod test { 0xB2, 0xDE, ]; config_params.pin_uv_auth_param = Some(pin_auth); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); @@ -296,8 +289,7 @@ mod test { 0xA7, 0x71, ]; config_params.pin_uv_auth_param = Some(pin_auth); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) @@ -311,15 +303,14 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); // First, set RP IDs without PIN auth. let min_pin_length = 6; let min_pin_length_rp_ids = vec!["example.com".to_string()]; let config_params = create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone())); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); assert_eq!( @@ -339,8 +330,7 @@ mod test { 0xD6, 0xDA, ]; config_params.pin_uv_auth_param = Some(pin_auth.clone()); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); assert_eq!( @@ -353,8 +343,7 @@ mod test { let mut config_params = create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone())); config_params.pin_uv_auth_param = Some(pin_auth.clone()); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -372,8 +361,7 @@ mod test { Some(vec!["counter.example.com".to_string()]), ); config_params.pin_uv_auth_param = Some(pin_auth); - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -391,7 +379,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); // Increase min PIN, force PIN change. @@ -402,8 +390,7 @@ mod test { 0xA8, 0xC8, ]); config_params.pin_uv_auth_param = pin_uv_auth_param; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); @@ -415,7 +402,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -435,8 +422,7 @@ mod test { pin_uv_auth_param, pin_uv_auth_protocol: Some(1), }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); } @@ -447,7 +433,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::VendorPrototype, @@ -455,8 +441,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 5a2cc4b..e392ead 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorCredentialManagementParameters; use super::data_formats::{ CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource, PublicKeyCredentialUserEntity, }; -use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; @@ -110,15 +110,15 @@ fn enumerate_credentials_response( /// Either no RP ID is associated, or the RP ID matches the stored credential. fn check_rp_id_permissions( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, credential_id: &[u8], ) -> Result<(), Ctap2StatusCode> { // Pre-check a sufficient condition before calling the store. - if pin_protocol_v1.has_no_rp_id_permission().is_ok() { + if client_pin.has_no_rp_id_permission().is_ok() { return Ok(()); } let (_, credential) = persistent_store.find_credential_item(credential_id)?; - pin_protocol_v1.has_no_or_rp_id_permission(&credential.rp_id) + client_pin.has_no_or_rp_id_permission(&credential.rp_id) } /// Processes the subcommand getCredsMetadata for CredentialManagement. @@ -173,14 +173,14 @@ fn process_enumerate_rps_get_next_rp( fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { let rp_id_hash = sub_command_params .rp_id_hash .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; - pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?; + client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?; let mut iter_result = Ok(()); let iter = persistent_store.iter_credentials(&mut iter_result)?; let mut rp_credentials: Vec = iter @@ -219,21 +219,21 @@ fn process_enumerate_credentials_get_next_credential( /// Processes the subcommand deleteCredential for CredentialManagement. fn process_delete_credential( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params .credential_id .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? .key_id; - check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; + check_rp_id_permissions(persistent_store, client_pin, &credential_id)?; persistent_store.delete_credential(&credential_id) } /// Processes the subcommand updateUserInformation for CredentialManagement. fn process_update_user_information( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params @@ -243,7 +243,7 @@ fn process_update_user_information( let user = sub_command_params .user .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; - check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; + check_rp_id_permissions(persistent_store, client_pin, &credential_id)?; persistent_store.update_credential(&credential_id, user) } @@ -251,14 +251,14 @@ fn process_update_user_information( pub fn process_credential_management( persistent_store: &mut PersistentStore, stateful_command_permission: &mut StatefulPermission, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, cred_management_params: AuthenticatorCredentialManagementParameters, now: ClockValue, ) -> Result { let AuthenticatorCredentialManagementParameters { sub_command, sub_command_params, - pin_protocol, + pin_uv_auth_protocol, pin_auth, } = cred_management_params; @@ -282,7 +282,7 @@ pub fn process_credential_management( | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { - check_pin_uv_auth_protocol(pin_protocol)?; + check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let mut management_data = vec![sub_command as u8]; if let Some(sub_command_params) = sub_command_params.clone() { @@ -290,11 +290,11 @@ pub fn process_credential_management( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { + if !client_pin.verify_pin_auth_token(&management_data, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } // The RP ID permission is handled differently per subcommand below. - pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; + client_pin.has_permission(PinPermission::CredentialManagement)?; } CredentialManagementSubCommand::EnumerateRpsGetNextRp | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} @@ -302,11 +302,11 @@ pub fn process_credential_management( let response = match sub_command { CredentialManagementSubCommand::GetCredsMetadata => { - pin_protocol_v1.has_no_rp_id_permission()?; + client_pin.has_no_rp_id_permission()?; Some(process_get_creds_metadata(persistent_store)?) } CredentialManagementSubCommand::EnumerateRpsBegin => { - pin_protocol_v1.has_no_rp_id_permission()?; + client_pin.has_no_rp_id_permission()?; Some(process_enumerate_rps_begin( persistent_store, stateful_command_permission, @@ -320,7 +320,7 @@ pub fn process_credential_management( Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, - pin_protocol_v1, + client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -334,7 +334,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::DeleteCredential => { process_delete_credential( persistent_store, - pin_protocol_v1, + client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -342,7 +342,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::UpdateUserInformation => { process_update_user_information( persistent_store, - pin_protocol_v1, + client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -384,12 +384,12 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let pin_auth = Some(vec![ @@ -400,13 +400,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth: pin_auth.clone(), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -428,13 +428,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -455,14 +455,14 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let credential_source1 = create_credential_source(&mut rng); let mut credential_source2 = create_credential_source(&mut rng); credential_source2.rp_id = "another.example.com".to_string(); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; ctap_state .persistent_store @@ -482,13 +482,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -506,13 +506,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_protocol: None, pin_auth: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -531,13 +531,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_protocol: None, pin_auth: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -552,12 +552,12 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; const NUM_CREDENTIALS: usize = 20; for i in 0..NUM_CREDENTIALS { @@ -581,7 +581,7 @@ mod test { let mut cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; @@ -589,7 +589,7 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -611,7 +611,7 @@ mod test { cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_protocol: None, pin_auth: None, }; } @@ -619,7 +619,7 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -634,7 +634,7 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let credential_source1 = create_credential_source(&mut rng); let mut credential_source2 = create_credential_source(&mut rng); credential_source2.user_handle = vec![0x02]; @@ -644,7 +644,7 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; ctap_state .persistent_store @@ -671,13 +671,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(sub_command_params), - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -694,13 +694,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_protocol: None, pin_auth: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -718,13 +718,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_protocol: None, pin_auth: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -739,13 +739,13 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut credential_source = create_credential_source(&mut rng); credential_source.credential_id = vec![0x1D; 32]; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; ctap_state .persistent_store @@ -771,13 +771,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params.clone()), - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth: pin_auth.clone(), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -789,13 +789,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params), - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -810,13 +810,13 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut credential_source = create_credential_source(&mut rng); credential_source.credential_id = vec![0x1D; 32]; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; ctap_state .persistent_store @@ -848,13 +848,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::UpdateUserInformation, sub_command_params: Some(sub_command_params), - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -878,15 +878,15 @@ mod test { } #[test] - fn test_process_credential_management_invalid_pin_protocol() { + fn test_process_credential_management_invalid_pin_uv_auth_protocol() { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let pin_auth = Some(vec![ @@ -897,13 +897,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(123456), + pin_uv_auth_protocol: Some(123456), pin_auth, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -924,13 +924,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth: Some(vec![0u8; 16]), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index f84c1a9..b74f982 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -13,8 +13,8 @@ // limitations under the License. use super::check_pin_uv_auth_protocol; +use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorLargeBlobsParameters; -use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; @@ -51,7 +51,7 @@ impl LargeBlobs { pub fn process_command( &mut self, persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, large_blobs_params: AuthenticatorLargeBlobsParameters, ) -> Result { let AuthenticatorLargeBlobsParameters { @@ -94,14 +94,14 @@ impl LargeBlobs { // TODO(kaczmarczyck) Error codes for PIN protocol differ across commands. // Change to Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED for None? check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; - pin_protocol_v1.has_permission(PinPermission::LargeBlobWrite)?; + client_pin.has_permission(PinPermission::LargeBlobWrite)?; let mut message = vec![0xFF; 32]; message.extend(&[0x0C, 0x00]); let mut offset_bytes = [0u8; 4]; LittleEndian::write_u32(&mut offset_bytes, offset as u32); message.extend(&offset_bytes); message.extend(&Sha256::hash(set.as_slice())); - if !pin_protocol_v1.verify_pin_auth_token(&message, &pin_uv_auth_param) { + if !client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } } @@ -146,7 +146,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ @@ -161,11 +161,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); match large_blobs_response.unwrap() { ResponseData::AuthenticatorLargeBlobs(Some(response)) => { assert_eq!(response.config, large_blob); @@ -180,7 +177,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -196,11 +193,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -214,11 +208,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -232,11 +223,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); match large_blobs_response.unwrap() { ResponseData::AuthenticatorLargeBlobs(Some(response)) => { assert_eq!(response.config, large_blob); @@ -251,7 +239,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -267,11 +255,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -286,11 +271,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ), @@ -303,7 +285,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -320,11 +302,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -338,11 +317,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), @@ -355,7 +331,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -370,11 +346,8 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE), @@ -387,7 +360,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -409,11 +382,8 @@ mod test { pin_uv_auth_param, pin_uv_auth_protocol: Some(1), }; - let large_blobs_response = large_blobs.process_command( - &mut persistent_store, - &mut pin_protocol_v1, - large_blobs_params, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index f74b16e..d9606e8 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod apdu; +mod client_pin; pub mod command; mod config_command; mod credential_management; @@ -23,15 +24,15 @@ pub mod data_formats; pub mod hid; mod key_material; mod large_blobs; -mod pin_protocol_v1; pub mod response; pub mod status_code; mod storage; mod timed_permission; +use self::client_pin::{ClientPin, PinPermission}; use self::command::{ - AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, - AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, + AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, + AuthenticatorVendorConfigureParameters, Command, }; use self::config_command::process_config; use self::credential_management::process_credential_management; @@ -48,7 +49,6 @@ use self::data_formats::{ }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; -use self::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, @@ -278,7 +278,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( // false otherwise. check_user_presence: CheckUserPresence, persistent_store: PersistentStore, - pin_protocol_v1: PinProtocolV1, + client_pin: ClientPin, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. @@ -297,12 +297,12 @@ where now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { let persistent_store = PersistentStore::new(rng); - let pin_protocol_v1 = PinProtocolV1::new(rng); + let client_pin = ClientPin::new(rng); CtapState { rng, check_user_presence, persistent_store, - pin_protocol_v1, + client_pin, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( U2F_UP_PROMPT_TIMEOUT, @@ -473,13 +473,17 @@ where } Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(now), Command::AuthenticatorGetInfo => self.process_get_info(), - Command::AuthenticatorClientPin(params) => self.process_client_pin(params), + Command::AuthenticatorClientPin(params) => self.client_pin.process_command( + self.rng, + &mut self.persistent_store, + params, + ), Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorCredentialManagement(params) => { process_credential_management( &mut self.persistent_store, &mut self.stateful_command_permission, - &mut self.pin_protocol_v1, + &mut self.client_pin, params, now, ) @@ -487,14 +491,12 @@ where Command::AuthenticatorSelection => self.process_selection(cid), Command::AuthenticatorLargeBlobs(params) => self.large_blobs.process_command( &mut self.persistent_store, - &mut self.pin_protocol_v1, - params, - ), - Command::AuthenticatorConfig(params) => process_config( - &mut self.persistent_store, - &mut self.pin_protocol_v1, + &mut self.client_pin, params, ), + Command::AuthenticatorConfig(params) => { + process_config(&mut self.persistent_store, &mut self.client_pin, params) + } // Vendor specific commands Command::AuthenticatorVendorConfigure(params) => { self.process_vendor_configure(params, cid) @@ -647,14 +649,14 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } if !self - .pin_protocol_v1 + .client_pin .verify_pin_auth_token(&client_data_hash, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - self.pin_protocol_v1 + self.client_pin .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; + self.client_pin.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -815,7 +817,7 @@ where let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret { let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; Some( - self.pin_protocol_v1 + self.client_pin .process_hmac_secret(hmac_secret_input, &cred_random)?, ) } else { @@ -938,14 +940,14 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } if !self - .pin_protocol_v1 + .client_pin .verify_pin_auth_token(&client_data_hash, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - self.pin_protocol_v1 + self.client_pin .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; + self.client_pin.ensure_rp_id_permission(&rp_id)?; UV_FLAG } None => { @@ -1104,17 +1106,6 @@ where )) } - fn process_client_pin( - &mut self, - client_pin_params: AuthenticatorClientPinParameters, - ) -> Result { - self.pin_protocol_v1.process_subcommand( - self.rng, - &mut self.persistent_store, - client_pin_params, - ) - } - fn process_reset( &mut self, cid: ChannelID, @@ -1129,7 +1120,7 @@ where (self.check_user_presence)(cid)?; self.persistent_store.reset(self.rng)?; - self.pin_protocol_v1.reset(self.rng); + self.client_pin.reset(self.rng); #[cfg(feature = "with_ctap1")] { self.u2f_up_state = U2fUserPresenceState::new( @@ -2332,11 +2323,11 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x88; 32]; - let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.pin_protocol_v1 = pin_protocol_v1; + ctap_state.client_pin = client_pin; let mut make_credential_params = create_minimal_make_credential_parameters(); let user1 = PublicKeyCredentialUserEntity { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0f6657f..a650d4d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,6 +14,7 @@ mod key; +use crate::ctap::client_pin::PIN_AUTH_LENGTH; use crate::ctap::customization::{ DEFAULT_MIN_PIN_LENGTH, DEFAULT_MIN_PIN_LENGTH_RP_IDS, ENFORCE_ALWAYS_UV, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_PIN_RETRIES, MAX_RP_IDS_LENGTH, MAX_SUPPORTED_RESIDENT_KEYS, @@ -24,7 +25,6 @@ use crate::ctap::data_formats::{ PublicKeyCredentialUserEntity, }; use crate::ctap::key_material; -use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; use crate::embedded_flash::{new_storage, Storage}; From eb0a0770dda54ff76bd40467f3b0901a6f57ef1c Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 10 Mar 2021 13:20:29 +0100 Subject: [PATCH 76/86] adds the PIN protocol trait (#292) * adds the PIN protocol trait * improved documentation * SharedSecret not mutable --- libraries/crypto/src/ecdh.rs | 20 +- src/ctap/client_pin.rs | 460 +++++++++++++----------------- src/ctap/config_command.rs | 4 +- src/ctap/credential_management.rs | 4 +- src/ctap/large_blobs.rs | 4 +- src/ctap/mod.rs | 26 +- src/ctap/pin_protocol.rs | 443 ++++++++++++++++++++++++++++ 7 files changed, 660 insertions(+), 301 deletions(-) create mode 100644 src/ctap/pin_protocol.rs diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index 705aee0..4a03679 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -17,8 +17,6 @@ use super::ec::int256; use super::ec::int256::Int256; use super::ec::point::PointP256; use super::rng256::Rng256; -use super::sha256::Sha256; -use super::Hash256; pub const NBYTES: usize = int256::NBYTES; @@ -62,15 +60,15 @@ impl SecKey { // - https://www.secg.org/sec1-v2.pdf } - /// Creates a shared key using the Diffie Hellman key agreement. + /// Performs the handshake using the Diffie Hellman key agreement. /// - /// The key agreement is defined in the FIDO2 specification, - /// Section 6.5.5.4. "Obtaining the Shared Secret" - pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] { + /// This function generates the Z in the PIN protocol v1 specification. + /// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#pinProto1 + pub fn exchange_x(&self, other: &PubKey) -> [u8; 32] { let p = self.exchange_raw(other); let mut x: [u8; 32] = [Default::default(); 32]; p.getx().to_int().to_bin(&mut x); - Sha256::hash(&x) + x } } @@ -123,7 +121,7 @@ mod test { /** Test that the exchanged key is the same on both sides **/ #[test] - fn test_exchange_x_sha256_is_symmetric() { + fn test_exchange_x_is_symmetric() { let mut rng = ThreadRng256 {}; for _ in 0..ITERATIONS { @@ -131,12 +129,12 @@ mod test { let pk_a = sk_a.genpk(); let sk_b = SecKey::gensk(&mut rng); let pk_b = sk_b.genpk(); - assert_eq!(sk_a.exchange_x_sha256(&pk_b), sk_b.exchange_x_sha256(&pk_a)); + assert_eq!(sk_a.exchange_x(&pk_b), sk_b.exchange_x(&pk_a)); } } #[test] - fn test_exchange_x_sha256_bytes_is_symmetric() { + fn test_exchange_x_bytes_is_symmetric() { let mut rng = ThreadRng256 {}; for _ in 0..ITERATIONS { @@ -150,7 +148,7 @@ mod test { let pk_a = PubKey::from_bytes_uncompressed(&pk_bytes_a).unwrap(); let pk_b = PubKey::from_bytes_uncompressed(&pk_bytes_b).unwrap(); - assert_eq!(sk_a.exchange_x_sha256(&pk_b), sk_b.exchange_x_sha256(&pk_a)); + assert_eq!(sk_a.exchange_x(&pk_b), sk_b.exchange_x(&pk_a)); } } diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index ec309ed..245f020 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -14,17 +14,14 @@ use super::command::AuthenticatorClientPinParameters; use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; +use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use alloc::str; use alloc::string::String; -use alloc::vec; use alloc::vec::Vec; -use arrayref::array_ref; -use core::convert::TryInto; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; -use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits}; +use crypto::hmac::hmac_256; use crypto::rng256::Rng256; use crypto::sha256::Sha256; use crypto::Hash256; @@ -32,123 +29,82 @@ use crypto::Hash256; use enum_iterator::IntoEnumIterator; use subtle::ConstantTimeEq; -// Those constants have to be multiples of 16, the AES block size. +/// The prefix length of the PIN hash that is stored and compared. +/// +/// The code assumes that this value is a multiple of the AES block length, fits +/// an u8 and is at most as long as a SHA256. The value is fixed for all PIN +/// protocols. pub const PIN_AUTH_LENGTH: usize = 16; + +/// The length of the pinUvAuthToken used throughout PIN protocols. +/// +/// The code assumes that this value is a multiple of the AES block length. It +/// is fixed since CTAP2.1. +pub const PIN_TOKEN_LENGTH: usize = 32; + +/// The length of the encrypted PINs when received by SetPin or ChangePin. +/// +/// The code assumes that this value is a multiple of the AES block length. It +/// is fixed since CTAP2.1. const PIN_PADDED_LENGTH: usize = 64; -const PIN_TOKEN_LENGTH: usize = 32; -/// Checks the given pin_auth against the truncated output of HMAC-SHA256. -/// Returns LEFT(HMAC(hmac_key, hmac_contents), 16) == pin_auth). -fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - if pin_auth.len() != PIN_AUTH_LENGTH { - return false; - } - verify_hmac_256_first_128bits::( - hmac_key, - hmac_contents, - array_ref![pin_auth, 0, PIN_AUTH_LENGTH], - ) -} - -/// Encrypts the HMAC-secret outputs. To compute them, we first have to -/// decrypt the HMAC secret salt(s) that were encrypted with the shared secret. -/// The credRandom is used as a secret to HMAC those salts. +/// Computes and encrypts the HMAC-secret outputs. +/// +/// To compute them, we first have to decrypt the HMAC secret salt(s) that were +/// encrypted with the shared secret. The credRandom is used as a secret in HMAC +/// for those salts. fn encrypt_hmac_secret_output( - shared_secret: &[u8; 32], + rng: &mut impl Rng256, + shared_secret: &dyn SharedSecret, salt_enc: &[u8], cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { - if salt_enc.len() != 32 && salt_enc.len() != 64 { + let decrypted_salts = shared_secret.decrypt(salt_enc)?; + if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - // The specification specifically asks for a zero IV. - let iv = [0u8; 16]; - - // With the if clause restriction above, block_len can only be 2 or 4. - let block_len = salt_enc.len() / 16; - let mut blocks = vec![[0u8; 16]; block_len]; - for i in 0..block_len { - blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); + let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); + if decrypted_salts.len() == 64 { + let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); + output.append(&mut output2); } - cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); - - let mut decrypted_salt1 = [0u8; 32]; - decrypted_salt1[..16].copy_from_slice(&blocks[0]); - decrypted_salt1[16..].copy_from_slice(&blocks[1]); - let output1 = hmac_256::(&cred_random[..], &decrypted_salt1[..]); - for i in 0..2 { - blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); - } - - if block_len == 4 { - let mut decrypted_salt2 = [0u8; 32]; - decrypted_salt2[..16].copy_from_slice(&blocks[2]); - decrypted_salt2[16..].copy_from_slice(&blocks[3]); - let output2 = hmac_256::(&cred_random[..], &decrypted_salt2[..]); - for i in 0..2 { - blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]); - } - } - - cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]); - let mut encrypted_output = Vec::with_capacity(salt_enc.len()); - for b in &blocks[..block_len] { - encrypted_output.extend(b); - } - Ok(encrypted_output) + shared_secret.encrypt(rng, &output) } /// Decrypts the new_pin_enc and outputs the found PIN. fn decrypt_pin( - aes_dec_key: &crypto::aes256::DecryptionKey, + shared_secret: &dyn SharedSecret, new_pin_enc: Vec, -) -> Option> { - if new_pin_enc.len() != PIN_PADDED_LENGTH { - return None; +) -> Result, Ctap2StatusCode> { + let decrypted_pin = shared_secret.decrypt(&new_pin_enc)?; + if decrypted_pin.len() != PIN_PADDED_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let iv = [0u8; 16]; - // Assuming PIN_PADDED_LENGTH % block_size == 0 here. - const BLOCK_COUNT: usize = PIN_PADDED_LENGTH / 16; - let mut blocks = [[0u8; 16]; BLOCK_COUNT]; - for i in 0..BLOCK_COUNT { - blocks[i].copy_from_slice(&new_pin_enc[i * 16..(i + 1) * 16]); - } - cbc_decrypt(aes_dec_key, iv, &mut blocks); // In CTAP 2.1, the specification changed. The new wording might lead to // different behavior when there are non-zero bytes after zero bytes. // This implementation consistently ignores those degenerate cases. - Some( - blocks - .iter() - .flatten() - .cloned() - .take_while(|&c| c != 0) - .collect::>(), - ) + Ok(decrypted_pin.into_iter().take_while(|&c| c != 0).collect()) } -/// Stores the encrypted new PIN in the persistent storage, if it satisfies the -/// PIN policy. The PIN is decrypted and stripped from its padding. Next, the -/// length of the PIN is checked to fulfill policy requirements. Last, the PIN -/// is hashed, truncated to 16 bytes and persistently stored. +/// Stores a hash prefix of the new PIN in the persistent storage, if correct. +/// +/// The new PIN is passed encrypted, so it is first decrypted and stripped from +/// padding. Next, it is checked against the PIN policy. Last, it is hashed and +/// truncated for persistent storage. fn check_and_store_new_pin( persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, + shared_secret: &dyn SharedSecret, new_pin_enc: Vec, ) -> Result<(), Ctap2StatusCode> { - let pin = decrypt_pin(aes_dec_key, new_pin_enc) - .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - + let pin = decrypt_pin(shared_secret, new_pin_enc)?; let min_pin_length = persistent_store.min_pin_length()? as usize; let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count(); if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } - let mut pin_hash = [0u8; 16]; - pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - // The PIN length is always < 64. + let mut pin_hash = [0u8; PIN_AUTH_LENGTH]; + pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..PIN_AUTH_LENGTH]); + // The PIN length is always < PIN_PADDED_LENGTH < 256. persistent_store.set_pin(&pin_hash, pin_length as u8)?; Ok(()) } @@ -167,8 +123,7 @@ pub enum PinPermission { } pub struct ClientPin { - key_agreement_key: crypto::ecdh::SecKey, - pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + pin_protocol_v1: PinProtocol, consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, @@ -176,17 +131,16 @@ pub struct ClientPin { impl ClientPin { pub fn new(rng: &mut impl Rng256) -> ClientPin { - let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - let pin_uv_auth_token = rng.gen_uniform_u8x32(); ClientPin { - key_agreement_key, - pin_uv_auth_token, + pin_protocol_v1: PinProtocol::new(rng), consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, } } + /// Checks the given encrypted PIN hash against the stored PIN hash. + /// /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. /// Resets or decreases the PIN retries, depending on success or failure. /// Also, in case of failure, the key agreement key is randomly reset. @@ -194,7 +148,7 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, + shared_secret: &dyn SharedSecret, pin_hash_enc: Vec, ) -> Result<(), Ctap2StatusCode> { match persistent_store.pin_hash()? { @@ -206,14 +160,10 @@ impl ClientPin { if pin_hash_enc.len() != PIN_AUTH_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } + let pin_hash_dec = shared_secret.decrypt(&pin_hash_enc)?; - let iv = [0u8; 16]; - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash_enc); - cbc_decrypt(aes_dec_key, iv, &mut blocks); - - if !bool::from(pin_hash.ct_eq(&blocks[0])) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { + self.pin_protocol_v1.regenerate(rng); if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } @@ -232,26 +182,6 @@ impl ClientPin { Ok(()) } - /// Uses the self-owned and passed halves of the key agreement to generate the - /// shared secret for checking pin_auth and generating a decryption key. - fn exchange_decryption_key( - &self, - key_agreement: CoseKey, - pin_auth: &[u8], - authenticated_message: &[u8], - ) -> Result { - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - if !verify_pin_auth(&shared_secret, authenticated_message, pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - Ok(aes_dec_key) - } - fn process_get_pin_retries( &self, persistent_store: &PersistentStore, @@ -264,9 +194,8 @@ impl ClientPin { } fn process_get_key_agreement(&self) -> Result { - let pk = self.key_agreement_key.genpk(); Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), + key_agreement: Some(self.pin_protocol_v1.get_public_key()), pin_token: None, retries: None, }) @@ -282,9 +211,10 @@ impl ClientPin { if persistent_store.pin_hash()?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - let pin_decryption_key = - self.exchange_decryption_key(key_agreement, &pin_auth, &new_pin_enc)?; - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + shared_secret.verify(&new_pin_enc, &pin_auth)?; + + check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; persistent_store.reset_pin_retries()?; Ok(()) } @@ -301,14 +231,14 @@ impl ClientPin { if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); - let pin_decryption_key = - self.exchange_decryption_key(key_agreement, &pin_auth, &auth_param_data)?; - self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; + shared_secret.verify(&auth_param_data, &pin_auth)?; + self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); Ok(()) } @@ -322,26 +252,13 @@ impl ClientPin { if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - let token_encryption_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let pin_decryption_key = crypto::aes256::DecryptionKey::new(&token_encryption_key); - self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; - // TODO(kaczmarczyck) can this be moved up in the specification? + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. - let iv = [0u8; 16]; - let mut blocks = [[0u8; 16]; PIN_TOKEN_LENGTH / 16]; - for (i, item) in blocks.iter_mut().take(PIN_TOKEN_LENGTH / 16).enumerate() { - item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]); - } - cbc_encrypt(&token_encryption_key, iv, &mut blocks); - let pin_token: Vec = blocks.iter().flatten().cloned().collect(); - + let pin_token = shared_secret.encrypt(rng, self.pin_protocol_v1.get_pin_uv_auth_token())?; self.permissions = 0x03; self.permissions_rp_id = None; @@ -469,13 +386,23 @@ impl ClientPin { Ok(ResponseData::AuthenticatorClientPin(response)) } - pub fn verify_pin_auth_token(&self, hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - verify_pin_auth(&self.pin_uv_auth_token, &hmac_contents, &pin_auth) + pub fn verify_pin_auth_token( + &self, + hmac_contents: &[u8], + pin_auth: &[u8], + ) -> Result<(), Ctap2StatusCode> { + // TODO(kaczmarczyck) pass the protocol number + verify_pin_uv_auth_token( + self.pin_protocol_v1.get_pin_uv_auth_token(), + hmac_contents, + pin_auth, + 1, + ) } pub fn reset(&mut self, rng: &mut impl Rng256) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + self.pin_protocol_v1.regenerate(rng); + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; self.permissions = 0; self.permissions_rp_id = None; @@ -483,6 +410,7 @@ impl ClientPin { pub fn process_hmac_secret( &self, + rng: &mut impl Rng256, hmac_secret_input: GetAssertionHmacSecretInput, cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { @@ -491,14 +419,9 @@ impl ClientPin { salt_enc, salt_auth, } = hmac_secret_input; - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - // HMAC-secret does the same 16 byte truncated check. - if !verify_pin_auth(&shared_secret, &salt_enc, &salt_auth) { - // Hard to tell what the correct error code here is. - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + shared_secret.verify(&salt_enc, &salt_auth)?; + encrypt_hmac_secret_output(rng, shared_secret.as_ref(), &salt_enc[..], cred_random) } /// Check if the required command's token permission is granted. @@ -560,8 +483,7 @@ impl ClientPin { pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], ) -> ClientPin { ClientPin { - key_agreement_key, - pin_uv_auth_token, + pin_protocol_v1: PinProtocol::new_test(key_agreement_key, pin_uv_auth_token), consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, @@ -571,10 +493,12 @@ impl ClientPin { #[cfg(test)] mod test { + use super::super::pin_protocol::SharedSecretV1; use super::*; + use alloc::vec; use crypto::rng256::ThreadRng256; - // Stores a PIN hash corresponding to the dummy PIN "1234". + /// Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { let mut pin = [0u8; 64]; pin[..4].copy_from_slice(b"1234"); @@ -583,36 +507,20 @@ mod test { persistent_store.set_pin(&pin_hash, 4).unwrap(); } - // Encrypts the message with a zero IV and key derived from shared_secret. + /// Encrypts the message with a zero IV and key derived from shared_secret. fn encrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { - assert!(message.len() % 16 == 0); - let block_len = message.len() / 16; - let mut blocks = vec![[0u8; 16]; block_len]; - for i in 0..block_len { - blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); - } - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - blocks.iter().flatten().cloned().collect::>() + let mut rng = ThreadRng256 {}; + let shared_secret = SharedSecretV1::new_test(*shared_secret); + shared_secret.encrypt(&mut rng, message).unwrap() } - // Decrypts the message with a zero IV and key derived from shared_secret. + /// Decrypts the message with a zero IV and key derived from shared_secret. fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { - assert!(message.len() % 16 == 0); - let block_len = message.len() / 16; - let mut blocks = vec![[0u8; 16]; block_len]; - for i in 0..block_len { - blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); - } - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - let iv = [0u8; 16]; - cbc_decrypt(&aes_dec_key, iv, &mut blocks); - blocks.iter().flatten().cloned().collect::>() + let shared_secret = SharedSecretV1::new_test(*shared_secret); + shared_secret.decrypt(message).unwrap() } - // Fails on PINs bigger than 64 bytes. + /// Fails on PINs bigger than 64 bytes. fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { assert!(pin.len() <= 64); let mut padded_pin = [0u8; 64]; @@ -620,12 +528,12 @@ mod test { encrypt_message(shared_secret, &padded_pin) } - // Encrypts the dummy PIN "1234". + /// Encrypts the dummy PIN "1234". fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { encrypt_pin(shared_secret, b"1234".to_vec()) } - // Encrypts the PIN hash corresponding to the dummy PIN "1234". + /// Encrypts the PIN hash corresponding to the dummy PIN "1234". fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { let mut pin = [0u8; 64]; pin[..4].copy_from_slice(b"1234"); @@ -643,9 +551,7 @@ mod test { 0xC4, 0x12, ]; persistent_store.set_pin(&pin_hash, 4).unwrap(); - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let shared_secret = SharedSecretV1::new_test([0x88; 32]); let mut client_pin = ClientPin::new(&mut rng); let pin_hash_enc = vec![ @@ -656,7 +562,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Ok(()) @@ -667,7 +573,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -682,7 +588,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) @@ -694,7 +600,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -705,7 +611,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -731,8 +637,10 @@ mod test { #[test] fn test_process_get_key_agreement() { let mut rng = ThreadRng256 {}; - let client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let expected_response = Ok(AuthenticatorClientPinResponse { key_agreement: Some(CoseKey::from(pk)), pin_token: None, @@ -745,9 +653,12 @@ mod test { fn test_process_set_pin() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); @@ -762,14 +673,18 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); + let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); assert_eq!( client_pin.process_change_pin( @@ -817,10 +732,14 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin .process_get_pin_token( @@ -849,10 +768,14 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); assert_eq!(persistent_store.force_pin_change(), Ok(())); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( client_pin.process_get_pin_token( @@ -870,10 +793,14 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin .process_get_pin_uv_auth_token_using_pin_with_permissions( @@ -935,10 +862,14 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); assert_eq!(persistent_store.force_pin_change(), Ok(())); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( @@ -991,9 +922,7 @@ mod test { #[test] fn test_decrypt_pin() { - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let shared_secret = SharedSecretV1::new_test([0x88; 32]); // "1234" let new_pin_enc = vec![ @@ -1004,8 +933,8 @@ mod test { 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, ]; assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(b"1234".to_vec()), + decrypt_pin(&shared_secret, new_pin_enc), + Ok(b"1234".to_vec()), ); // "123" @@ -1017,26 +946,31 @@ mod test { 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, ]; assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(b"123".to_vec()), + decrypt_pin(&shared_secret, new_pin_enc), + Ok(b"123".to_vec()), ); // Encrypted PIN is too short. let new_pin_enc = vec![0x44; 63]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); + assert_eq!( + decrypt_pin(&shared_secret, new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); // Encrypted PIN is too long. let new_pin_enc = vec![0x44; 65]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); + assert_eq!( + decrypt_pin(&shared_secret, new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); } #[test] fn test_check_and_store_new_pin() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let shared_secret_hash = [0x88; 32]; + let shared_secret = SharedSecretV1::new_test(shared_secret_hash); let test_cases = vec![ // Accept PIN "1234". @@ -1059,9 +993,9 @@ mod test { ]; for (pin, result) in test_cases { let old_pin_hash = persistent_store.pin_hash().unwrap(); - let new_pin_enc = encrypt_pin(&shared_secret, pin); + let new_pin_enc = encrypt_pin(&shared_secret_hash, pin); assert_eq!( - check_and_store_new_pin(&mut persistent_store, &aes_dec_key, new_pin_enc), + check_and_store_new_pin(&mut persistent_store, &shared_secret, new_pin_enc), result ); if result.is_ok() { @@ -1072,31 +1006,22 @@ mod test { } } - #[test] - fn test_verify_pin_auth() { - let hmac_key = [0x88; 16]; - let pin_auth = [ - 0x88, 0x09, 0x41, 0x13, 0xF7, 0x97, 0x32, 0x0B, 0x3E, 0xD9, 0xBC, 0x76, 0x4F, 0x18, - 0x56, 0x5D, - ]; - assert!(verify_pin_auth(&hmac_key, &[], &pin_auth)); - assert!(!verify_pin_auth(&hmac_key, &[0x00], &pin_auth)); - } - #[test] fn test_encrypt_hmac_secret_output() { - let shared_secret = [0x55; 32]; + let mut rng = ThreadRng256 {}; + let shared_secret_hash = [0x88; 32]; + let shared_secret = SharedSecretV1::new_test(shared_secret_hash); let salt_enc = [0x5E; 32]; let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); assert_eq!(output.unwrap().len(), 32); let salt_enc = [0x5E; 48]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); let salt_enc = [0x5E; 64]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); assert_eq!(output.unwrap().len(), 64); let mut salt_enc = [0x00; 32]; @@ -1108,45 +1033,50 @@ mod test { let expected_output1 = hmac_256::(&cred_random, &salt1); let expected_output2 = hmac_256::(&cred_random, &salt2); - let salt_enc1 = encrypt_message(&shared_secret, &salt1); + let salt_enc1 = encrypt_message(&shared_secret_hash, &salt1); salt_enc.copy_from_slice(salt_enc1.as_slice()); - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret, &output); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec, &expected_output1); - let salt_enc2 = &encrypt_message(&shared_secret, &salt2); + let salt_enc2 = &encrypt_message(&shared_secret_hash, &salt2); salt_enc.copy_from_slice(salt_enc2.as_slice()); - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret, &output); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec, &expected_output2); let mut salt_enc = [0x00; 64]; let mut salt12 = [0x00; 64]; salt12[..32].copy_from_slice(&salt1); salt12[32..].copy_from_slice(&salt2); - let salt_enc12 = encrypt_message(&shared_secret, &salt12); + let salt_enc12 = encrypt_message(&shared_secret_hash, &salt12); salt_enc.copy_from_slice(salt_enc12.as_slice()); - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret, &output); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec[..32], &expected_output1); assert_eq!(&output_dec[32..], &expected_output2); let mut salt_enc = [0x00; 64]; let mut salt02 = [0x00; 64]; salt02[32..].copy_from_slice(&salt2); - let salt_enc02 = encrypt_message(&shared_secret, &salt02); + let salt_enc02 = encrypt_message(&shared_secret_hash, &salt02); salt_enc.copy_from_slice(salt_enc02.as_slice()); - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret, &output); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec[32..], &expected_output2); let mut salt_enc = [0x00; 64]; let mut salt10 = [0x00; 64]; salt10[..32].copy_from_slice(&salt1); - let salt_enc10 = encrypt_message(&shared_secret, &salt10); + let salt_enc10 = encrypt_message(&shared_secret_hash, &salt10); salt_enc.copy_from_slice(salt_enc10.as_slice()); - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret, &output); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec[..32], &expected_output1); } diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 4a7f868..2f90f11 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -103,9 +103,7 @@ pub fn process_config( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !client_pin.verify_pin_auth_token(&config_data, &auth_param) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + client_pin.verify_pin_auth_token(&config_data, &auth_param)?; } match sub_command { diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index e392ead..5b07294 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -290,9 +290,7 @@ pub fn process_credential_management( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !client_pin.verify_pin_auth_token(&management_data, &pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + client_pin.verify_pin_auth_token(&management_data, &pin_auth)?; // The RP ID permission is handled differently per subcommand below. client_pin.has_permission(PinPermission::CredentialManagement)?; } diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index b74f982..05cec58 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -101,9 +101,7 @@ impl LargeBlobs { LittleEndian::write_u32(&mut offset_bytes, offset as u32); message.extend(&offset_bytes); message.extend(&Sha256::hash(set.as_slice())); - if !client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param)?; } if offset + set.len() > self.expected_length { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d9606e8..6d8e2d0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -24,6 +24,7 @@ pub mod data_formats; pub mod hid; mod key_material; mod large_blobs; +mod pin_protocol; pub mod response; pub mod status_code; mod storage; @@ -648,12 +649,8 @@ where // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !self - .client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + self.client_pin + .verify_pin_auth_token(&client_data_hash, &pin_auth)?; self.client_pin .has_permission(PinPermission::MakeCredential)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; @@ -816,10 +813,11 @@ where if extensions.hmac_secret.is_some() || extensions.cred_blob { let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret { let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; - Some( - self.client_pin - .process_hmac_secret(hmac_secret_input, &cred_random)?, - ) + Some(self.client_pin.process_hmac_secret( + self.rng, + hmac_secret_input, + &cred_random, + )?) } else { None }; @@ -939,12 +937,8 @@ where // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !self - .client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + self.client_pin + .verify_pin_auth_token(&client_data_hash, &pin_auth)?; self.client_pin .has_permission(PinPermission::GetAssertion)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs new file mode 100644 index 0000000..88d608a --- /dev/null +++ b/src/ctap/pin_protocol.rs @@ -0,0 +1,443 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::ctap::client_pin::PIN_TOKEN_LENGTH; +use crate::ctap::data_formats::CoseKey; +use crate::ctap::status_code::Ctap2StatusCode; +use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; +use core::convert::TryInto; +use crypto::cbc::{cbc_decrypt, cbc_encrypt}; +use crypto::hkdf::hkdf_empty_salt_256; +use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits}; +use crypto::rng256::Rng256; +use crypto::sha256::Sha256; +use crypto::Hash256; + +/// Implements common functions between existing PIN protocols for handshakes. +pub struct PinProtocol { + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], +} + +impl PinProtocol { + /// This process is run by the authenticator at power-on. + /// + /// This function implements "initialize" from the specification. + pub fn new(rng: &mut impl Rng256) -> PinProtocol { + let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + let pin_uv_auth_token = rng.gen_uniform_u8x32(); + PinProtocol { + key_agreement_key, + pin_uv_auth_token, + } + } + + /// Generates a fresh public key. + pub fn regenerate(&mut self, rng: &mut impl Rng256) { + self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + } + + /// Generates a fresh pinUvAuthToken. + pub fn reset_pin_uv_auth_token(&mut self, rng: &mut impl Rng256) { + self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + } + + /// Returns the authenticator’s public key as a CoseKey structure. + pub fn get_public_key(&self) -> CoseKey { + CoseKey::from(self.key_agreement_key.genpk()) + } + + /// Processes the peer's encapsulated CoseKey and returns the shared secret. + pub fn decapsulate( + &self, + peer_cose_key: CoseKey, + pin_uv_auth_protocol: u64, + ) -> Result, Ctap2StatusCode> { + let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?; + let handshake = self.key_agreement_key.exchange_x(&pk); + match pin_uv_auth_protocol { + 1 => Ok(Box::new(SharedSecretV1::new(handshake))), + 2 => Ok(Box::new(SharedSecretV2::new(handshake))), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } + } + + /// Getter for pinUvAuthToken. + pub fn get_pin_uv_auth_token(&self) -> &[u8; PIN_TOKEN_LENGTH] { + &self.pin_uv_auth_token + } + + /// This is used for debugging to inject key material. + #[cfg(test)] + pub fn new_test( + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + ) -> PinProtocol { + PinProtocol { + key_agreement_key, + pin_uv_auth_token, + } + } +} + +/// Verifies the pinUvAuthToken for the given PIN protocol. +pub fn verify_pin_uv_auth_token( + token: &[u8; PIN_TOKEN_LENGTH], + message: &[u8], + signature: &[u8], + pin_uv_auth_protocol: u64, +) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + 1 => verify_v1(token, message, signature), + 2 => verify_v2(token, message, signature), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } +} + +pub trait SharedSecret { + /// Returns the encrypted plaintext. + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode>; + + /// Returns the decrypted ciphertext. + fn decrypt(&self, ciphertext: &[u8]) -> Result, Ctap2StatusCode>; + + /// Verifies that the signature is a valid MAC for the given message. + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>; +} + +fn aes256_cbc_encrypt( + rng: &mut dyn Rng256, + aes_enc_key: &crypto::aes256::EncryptionKey, + plaintext: &[u8], + has_iv: bool, +) -> Result, Ctap2StatusCode> { + if plaintext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let iv = if has_iv { + let random_bytes = rng.gen_uniform_u8x32(); + *array_ref!(random_bytes, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(plaintext.len() / 16); + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + for block in plaintext.chunks_exact(16) { + blocks.push(*array_ref!(block, 0, 16)); + } + cbc_encrypt(aes_enc_key, iv, &mut blocks); + let mut ciphertext = if has_iv { iv.to_vec() } else { vec![] }; + ciphertext.extend(blocks.iter().flatten()); + Ok(ciphertext) +} + +fn aes256_cbc_decrypt( + aes_enc_key: &crypto::aes256::EncryptionKey, + ciphertext: &[u8], + has_iv: bool, +) -> Result, Ctap2StatusCode> { + if ciphertext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut block_len = ciphertext.len() / 16; + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + let mut block_iter = ciphertext.chunks_exact(16); + let iv = if has_iv { + block_len -= 1; + let iv_block = block_iter + .next() + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + *array_ref!(iv_block, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(block_len); + for block in block_iter { + blocks.push(*array_ref!(block, 0, 16)); + } + let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + Ok(blocks.iter().flatten().cloned().collect::>()) +} + +fn verify_v1(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + if signature.len() != 16 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if verify_hmac_256_first_128bits::(key, message, array_ref![signature, 0, 16]) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } +} + +fn verify_v2(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + if signature.len() != 32 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if verify_hmac_256::(key, message, array_ref![signature, 0, 32]) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } +} + +pub struct SharedSecretV1 { + common_secret: [u8; 32], + aes_enc_key: crypto::aes256::EncryptionKey, +} + +impl SharedSecretV1 { + /// Creates a new shared secret from the handshake result. + fn new(handshake: [u8; 32]) -> SharedSecretV1 { + let common_secret = Sha256::hash(&handshake); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&common_secret); + SharedSecretV1 { + common_secret, + aes_enc_key, + } + } + + /// Creates a new shared secret for testing. + #[cfg(test)] + pub fn new_test(hash: [u8; 32]) -> SharedSecretV1 { + let aes_enc_key = crypto::aes256::EncryptionKey::new(&hash); + SharedSecretV1 { + common_secret: hash, + aes_enc_key, + } + } +} + +impl SharedSecret for SharedSecretV1 { + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, false) + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + verify_v1(&self.common_secret, message, signature) + } +} + +pub struct SharedSecretV2 { + aes_enc_key: crypto::aes256::EncryptionKey, + hmac_key: [u8; 32], +} + +impl SharedSecretV2 { + /// Creates a new shared secret from the handshake result. + fn new(handshake: [u8; 32]) -> SharedSecretV2 { + let aes_key = hkdf_empty_salt_256::(&handshake, b"CTAP2 AES key"); + SharedSecretV2 { + aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key), + hmac_key: hkdf_empty_salt_256::(&handshake, b"CTAP2 HMAC key"), + } + } +} + +impl SharedSecret for SharedSecretV2 { + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, true) + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + verify_v2(&self.hmac_key, message, signature) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crypto::rng256::ThreadRng256; + + #[test] + fn test_pin_protocol_public_key() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol = PinProtocol::new(&mut rng); + let public_key = pin_protocol.get_public_key(); + pin_protocol.regenerate(&mut rng); + let new_public_key = pin_protocol.get_public_key(); + assert_ne!(public_key, new_public_key); + } + + #[test] + fn test_pin_protocol_pin_uv_auth_token() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol = PinProtocol::new(&mut rng); + let token = *pin_protocol.get_pin_uv_auth_token(); + pin_protocol.reset_pin_uv_auth_token(&mut rng); + let new_token = pin_protocol.get_pin_uv_auth_token(); + assert_ne!(&token, new_token); + } + + #[test] + fn test_shared_secret_v1_encrypt_decrypt() { + let mut rng = ThreadRng256 {}; + let shared_secret = SharedSecretV1::new([0x55; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = shared_secret.encrypt(&mut rng, &plaintext).unwrap(); + assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); + } + + #[test] + fn test_shared_secret_v1_verify() { + let shared_secret = SharedSecretV1::new([0x55; 32]); + let message = [0xAA]; + let signature = [ + 0x8B, 0x60, 0x15, 0x7D, 0xF3, 0x44, 0x82, 0x2E, 0x54, 0x34, 0x7A, 0x01, 0xFB, 0x02, + 0x48, 0xA6, + ]; + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + assert_eq!( + shared_secret.verify(&[0xBB], &signature), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + shared_secret.verify(&message, &[0x12; 16]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_shared_secret_v2_encrypt_decrypt() { + let mut rng = ThreadRng256 {}; + let shared_secret = SharedSecretV2::new([0x55; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = shared_secret.encrypt(&mut rng, &plaintext).unwrap(); + assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); + } + + #[test] + fn test_shared_secret_v2_verify() { + let shared_secret = SharedSecretV2::new([0x55; 32]); + let message = [0xAA]; + let signature = [ + 0xC0, 0x3F, 0x2A, 0x22, 0x5C, 0xC3, 0x4E, 0x05, 0xC1, 0x0E, 0x72, 0x9C, 0x8D, 0xD5, + 0x7D, 0xE5, 0x98, 0x9C, 0x68, 0x15, 0xEC, 0xE2, 0x3A, 0x95, 0xD5, 0x90, 0xE1, 0xE9, + 0x3F, 0xF0, 0x1A, 0xAF, + ]; + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + assert_eq!( + shared_secret.verify(&[0xBB], &signature), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + shared_secret.verify(&message, &[0x12; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_decapsulate_invalid() { + let mut rng = ThreadRng256 {}; + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol.decapsulate(pin_protocol.get_public_key(), 3); + assert_eq!( + shared_secret.err(), + Some(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + fn test_decapsulate_symmetric() { + let mut rng = ThreadRng256 {}; + let pin_protocol1 = PinProtocol::new(&mut rng); + let pin_protocol2 = PinProtocol::new(&mut rng); + for protocol in 1..=2 { + let shared_secret1 = pin_protocol1 + .decapsulate(pin_protocol2.get_public_key(), protocol) + .unwrap(); + let shared_secret2 = pin_protocol2 + .decapsulate(pin_protocol1.get_public_key(), protocol) + .unwrap(); + let plaintext = vec![0xAA; 64]; + let ciphertext = shared_secret1.encrypt(&mut rng, &plaintext).unwrap(); + assert_eq!(plaintext, shared_secret2.decrypt(&ciphertext).unwrap()); + } + } + + #[test] + fn test_verify_pin_uv_auth_token_v1() { + let token = [0x91; PIN_TOKEN_LENGTH]; + let message = [0xAA]; + let signature = [ + 0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50, + 0x49, 0x68, + ]; + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &signature, 1), + Ok(()) + ); + assert_eq!( + verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &[0xBB], &signature, 1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &[0x12; 16], 1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token_v2() { + let token = [0x91; PIN_TOKEN_LENGTH]; + let message = [0xAA]; + let signature = [ + 0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50, + 0x49, 0x68, 0x94, 0x90, 0x20, 0x53, 0x0F, 0xA3, 0xD2, 0x7A, 0x9F, 0xFD, 0xFA, 0x62, + 0x36, 0x93, 0xF7, 0x84, + ]; + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &signature, 2), + Ok(()) + ); + assert_eq!( + verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &[0xBB], &signature, 2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &[0x12; 32], 2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token_invalid_protocol() { + let token = [0x91; PIN_TOKEN_LENGTH]; + let message = [0xAA]; + let signature = []; + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &signature, 3), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } +} From 3c7c5a4810965f774a055aada4f32bcf9387ad80 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Sat, 13 Mar 2021 13:16:57 +0100 Subject: [PATCH 77/86] Update the documentation to use linking by name See https://doc.rust-lang.org/stable/rustdoc/linking-to-items-by-name.html --- libraries/persistent_store/src/buffer.rs | 38 +- libraries/persistent_store/src/driver.rs | 4 + libraries/persistent_store/src/format.rs | 176 ++++---- libraries/persistent_store/src/fragment.rs | 4 +- libraries/persistent_store/src/lib.rs | 467 ++++++++++----------- libraries/persistent_store/src/model.rs | 5 +- libraries/persistent_store/src/storage.rs | 8 +- libraries/persistent_store/src/store.rs | 43 +- 8 files changed, 368 insertions(+), 377 deletions(-) diff --git a/libraries/persistent_store/src/buffer.rs b/libraries/persistent_store/src/buffer.rs index ae35089..3acd39a 100644 --- a/libraries/persistent_store/src/buffer.rs +++ b/libraries/persistent_store/src/buffer.rs @@ -12,6 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Flash storage for testing. +//! +//! [`BufferStorage`] implements the flash [`Storage`] interface but doesn't interface with an +//! actual flash storage. Instead it uses a buffer in memory to represent the storage state. + use crate::{Storage, StorageError, StorageIndex, StorageResult}; use alloc::borrow::Borrow; use alloc::boxed::Box; @@ -63,8 +68,8 @@ pub struct BufferOptions { /// /// When set, the following conditions would panic: /// - A bit is written from 0 to 1. - /// - A word is written more than `max_word_writes`. - /// - A page is erased more than `max_page_erases`. + /// - A word is written more than [`Self::max_word_writes`]. + /// - A page is erased more than [`Self::max_page_erases`]. pub strict_mode: bool, } @@ -110,15 +115,13 @@ impl BufferStorage { /// /// Before each subsequent mutable operation (write or erase), the delay is decremented if /// positive. If the delay is elapsed, the operation is saved and an error is returned. - /// Subsequent operations will panic until the interrupted operation is [corrupted] or the - /// interruption is [reset]. + /// Subsequent operations will panic until either of: + /// - The interrupted operation is [corrupted](BufferStorage::corrupt_operation). + /// - The interruption is [reset](BufferStorage::reset_interruption). /// /// # Panics /// /// Panics if an interruption is already armed. - /// - /// [corrupted]: struct.BufferStorage.html#method.corrupt_operation - /// [reset]: struct.BufferStorage.html#method.reset_interruption pub fn arm_interruption(&mut self, delay: usize) { self.interruption.arm(delay); } @@ -130,10 +133,8 @@ impl BufferStorage { /// # Panics /// /// Panics if any of the following conditions hold: - /// - An interruption was not [armed]. + /// - An interruption was not [armed](BufferStorage::arm_interruption). /// - An interruption was armed and it has triggered. - /// - /// [armed]: struct.BufferStorage.html#method.arm_interruption pub fn disarm_interruption(&mut self) -> usize { self.interruption.get().err().unwrap() } @@ -142,16 +143,14 @@ impl BufferStorage { /// /// # Panics /// - /// Panics if an interruption was not [armed]. - /// - /// [armed]: struct.BufferStorage.html#method.arm_interruption + /// Panics if an interruption was not [armed](BufferStorage::arm_interruption). pub fn reset_interruption(&mut self) { let _ = self.interruption.get(); } /// Corrupts an interrupted operation. /// - /// Applies the [corruption function] to the storage. Counters are updated accordingly: + /// Applies the corruption function to the storage. Counters are updated accordingly: /// - If a word is fully written, its counter is incremented regardless of whether other words /// of the same operation have been fully written. /// - If a page is fully erased, its counter is incremented (and its word counters are reset). @@ -159,13 +158,10 @@ impl BufferStorage { /// # Panics /// /// Panics if any of the following conditions hold: - /// - An interruption was not [armed]. + /// - An interruption was not [armed](BufferStorage::arm_interruption). /// - An interruption was armed but did not trigger. /// - The corruption function corrupts more bits than allowed. /// - The interrupted operation itself would have panicked. - /// - /// [armed]: struct.BufferStorage.html#method.arm_interruption - /// [corruption function]: type.BufferCorruptFunction.html pub fn corrupt_operation(&mut self, corrupt: BufferCorruptFunction) { let operation = self.interruption.get().unwrap(); let range = self.operation_range(&operation).unwrap(); @@ -217,7 +213,8 @@ impl BufferStorage { /// /// # Panics /// - /// Panics if the maximum number of erase cycles per page is reached. + /// Panics if the [maximum number of erase cycles per page](BufferOptions::max_page_erases) is + /// reached. fn incr_page_erases(&mut self, page: usize) { // Check that pages are not erased too many times. if self.options.strict_mode { @@ -243,7 +240,8 @@ impl BufferStorage { /// /// # Panics /// - /// Panics if the maximum number of writes per word is reached. + /// Panics if the [maximum number of writes per word](BufferOptions::max_word_writes) is + /// reached. fn incr_word_writes(&mut self, index: usize, value: &[u8], complete: &[u8]) { let word_size = self.word_size(); for i in 0..value.len() / word_size { diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index f17f576..d2baee0 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Store wrapper for testing. +//! +//! [`StoreDriver`] wraps a [`Store`] and compares its behavior with its associated [`StoreModel`]. + use crate::format::{Format, Position}; #[cfg(test)] use crate::StoreUpdate; diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index a70dcc4..a7dd4f5 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Storage representation of a store. + #[macro_use] mod bitfield; @@ -26,13 +28,14 @@ use core::convert::TryFrom; /// Internal representation of a word in flash. /// -/// Currently, the store only supports storages where a word is 32 bits. +/// Currently, the store only supports storages where a word is 32 bits, i.e. the [word +/// size](Storage::word_size) is 4 bytes. type WORD = u32; /// Abstract representation of a word in flash. /// -/// This type is kept abstract to avoid possible confusion with `Nat` if they happen to have the -/// same representation. This is because they have different semantics, `Nat` represents natural +/// This type is kept abstract to avoid possible confusion with [`Nat`] if they happen to have the +/// same representation. This is because they have different semantics, [`Nat`] represents natural /// numbers while `Word` represents sequences of bits (and thus has no arithmetic). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Word(WORD); @@ -47,7 +50,7 @@ impl Word { /// /// # Panics /// - /// Panics if `slice.len() != WORD_SIZE`. + /// Panics if `slice.len()` is not [`WORD_SIZE`] bytes. pub fn from_slice(slice: &[u8]) -> Word { Word(WORD::from_le_bytes(::try_from(slice).unwrap())) } @@ -60,47 +63,49 @@ impl Word { /// Size of a word in bytes. /// -/// Currently, the store only supports storages where a word is 4 bytes. +/// Currently, the store only supports storages where the [word size](Storage::word_size) is 4 +/// bytes. const WORD_SIZE: Nat = core::mem::size_of::() as Nat; /// Minimum number of words per page. /// -/// Currently, the store only supports storages where pages have at least 8 words. -const MIN_NUM_WORDS_PER_PAGE: Nat = 8; +/// Currently, the store only supports storages where pages have at least 8 [words](WORD_SIZE), i.e. +/// the [page size](Storage::page_size) is at least 32 bytes. +const MIN_PAGE_SIZE: Nat = 8; /// Maximum size of a page in bytes. /// -/// Currently, the store only supports storages where pages are between 8 and 1024 [words]. -/// -/// [words]: constant.WORD_SIZE.html +/// Currently, the store only supports storages where pages have at most 1024 [words](WORD_SIZE), +/// i.e. the [page size](Storage::page_size) is at most 4096 bytes. const MAX_PAGE_SIZE: Nat = 4096; /// Maximum number of erase cycles. /// -/// Currently, the store only supports storages where the maximum number of erase cycles fits on 16 -/// bits. +/// Currently, the store only supports storages where the [maximum number of erase +/// cycles](Storage::max_page_erases) fits in 16 bits, i.e. it is at most 65535. const MAX_ERASE_CYCLE: Nat = 65535; /// Minimum number of pages. /// -/// Currently, the store only supports storages with at least 3 pages. +/// Currently, the store only supports storages where the [number of pages](Storage::num_pages) is +/// at least 3. const MIN_NUM_PAGES: Nat = 3; /// Maximum page index. /// -/// Thus the maximum number of pages is one more than this number. Currently, the store only -/// supports storages where the number of pages is between 3 and 64. +/// Currently, the store only supports storages where the [number of pages](Storage::num_pages) is +/// at most 64, i.e. the maximum page index is 63. const MAX_PAGE_INDEX: Nat = 63; /// Maximum key index. /// -/// Thus the number of keys is one more than this number. Currently, the store only supports 4096 -/// keys. +/// Currently, the store only supports 4096 keys, i.e. the maximum key index is 4095. const MAX_KEY_INDEX: Nat = 4095; /// Maximum length in bytes of a user payload. /// -/// Currently, the store only supports values smaller than 1024 bytes. +/// Currently, the store only supports values at most 1023 bytes long. This may be further reduced +/// depending on the [page size](Storage::page_size), see [`Format::max_value_len`]. const MAX_VALUE_LEN: Nat = 1023; /// Maximum number of updates per transaction. @@ -109,9 +114,15 @@ const MAX_VALUE_LEN: Nat = 1023; const MAX_UPDATES: Nat = 31; /// Maximum number of words per virtual page. -const MAX_VIRT_PAGE_SIZE: Nat = div_ceil(MAX_PAGE_SIZE, WORD_SIZE) - CONTENT_WORD; +/// +/// A virtual page has [`CONTENT_WORD`] less [words](WORD_SIZE) than the storage [page +/// size](Storage::page_size). Those words are used to store the page header. Since a page has at +/// least [8](MIN_PAGE_SIZE) words, a virtual page has at least 6 words. +const MAX_VIRT_PAGE_SIZE: Nat = MAX_PAGE_SIZE / WORD_SIZE - CONTENT_WORD; /// Word with all bits set to one. +/// +/// After a page is erased, all words are equal to this value. const ERASED_WORD: Word = Word(!(0 as WORD)); /// Helpers for a given storage configuration. @@ -121,33 +132,31 @@ pub struct Format { /// /// # Invariant /// - /// - Words divide a page evenly. - /// - There are at least 8 words in a page. - /// - There are at most `MAX_PAGE_SIZE` bytes in a page. + /// - [Words](WORD_SIZE) divide a page evenly. + /// - There are at least [`MIN_PAGE_SIZE`] words in a page. + /// - There are at most [`MAX_PAGE_SIZE`] bytes in a page. page_size: Nat, /// The number of pages in the storage. /// /// # Invariant /// - /// - There are at least 3 pages. - /// - There are at most `MAX_PAGE_INDEX + 1` pages. + /// - There are at least [`MIN_NUM_PAGES`] pages. + /// - There are at most [`MAX_PAGE_INDEX`] + 1 pages. num_pages: Nat, /// The maximum number of times a page can be erased. /// /// # Invariant /// - /// - A page can be erased at most `MAX_ERASE_CYCLE` times. + /// - A page can be erased at most [`MAX_ERASE_CYCLE`] times. max_page_erases: Nat, } impl Format { /// Extracts the format from a storage. /// - /// Returns `None` if the storage is not [supported]. - /// - /// [supported]: struct.Format.html#method.is_storage_supported + /// Returns `None` if the storage is not [supported](Format::is_storage_supported). pub fn new(storage: &S) -> Option { if Format::is_storage_supported(storage) { Some(Format { @@ -163,21 +172,12 @@ impl Format { /// Returns whether a storage is supported. /// /// A storage is supported if the following conditions hold: - /// - The size of a word is [`WORD_SIZE`] bytes. - /// - The size of a word evenly divides the size of a page. - /// - A page contains at least [`MIN_NUM_WORDS_PER_PAGE`] words. - /// - A page contains at most [`MAX_PAGE_SIZE`] bytes. - /// - There are at least [`MIN_NUM_PAGES`] pages. - /// - There are at most [`MAX_PAGE_INDEX`]` + 1` pages. - /// - A word can be written at least twice between erase cycles. - /// - The maximum number of erase cycles is at most [`MAX_ERASE_CYCLE`]. - /// - /// [`WORD_SIZE`]: constant.WORD_SIZE.html - /// [`MIN_NUM_WORDS_PER_PAGE`]: constant.MIN_NUM_WORDS_PER_PAGE.html - /// [`MAX_PAGE_SIZE`]: constant.MAX_PAGE_SIZE.html - /// [`MIN_NUM_PAGES`]: constant.MIN_NUM_PAGES.html - /// [`MAX_PAGE_INDEX`]: constant.MAX_PAGE_INDEX.html - /// [`MAX_ERASE_CYCLE`]: constant.MAX_ERASE_CYCLE.html + /// - The [`Storage::word_size`] is [`WORD_SIZE`] bytes. + /// - The [`Storage::word_size`] evenly divides the [`Storage::page_size`]. + /// - The [`Storage::page_size`] is between [`MIN_PAGE_SIZE`] words and [`MAX_PAGE_SIZE`] bytes. + /// - The [`Storage::num_pages`] is between [`MIN_NUM_PAGES`] and [`MAX_PAGE_INDEX`] + 1. + /// - The [`Storage::max_word_writes`] is at least 2. + /// - The [`Storage::max_page_erases`] is at most [`MAX_ERASE_CYCLE`]. fn is_storage_supported(storage: &S) -> bool { let word_size = usize_to_nat(storage.word_size()); let page_size = usize_to_nat(storage.page_size()); @@ -186,7 +186,7 @@ impl Format { let max_page_erases = usize_to_nat(storage.max_page_erases()); word_size == WORD_SIZE && page_size % word_size == 0 - && (MIN_NUM_WORDS_PER_PAGE * word_size <= page_size && page_size <= MAX_PAGE_SIZE) + && (MIN_PAGE_SIZE * word_size <= page_size && page_size <= MAX_PAGE_SIZE) && (MIN_NUM_PAGES <= num_pages && num_pages <= MAX_PAGE_INDEX + 1) && max_word_writes >= 2 && max_page_erases <= MAX_ERASE_CYCLE @@ -199,28 +199,28 @@ impl Format { /// The size of a page in bytes. /// - /// We have `MIN_NUM_WORDS_PER_PAGE * self.word_size() <= self.page_size() <= MAX_PAGE_SIZE`. + /// This is at least [`MIN_PAGE_SIZE`] [words](WORD_SIZE) and at most [`MAX_PAGE_SIZE`] bytes. pub fn page_size(&self) -> Nat { self.page_size } - /// The number of pages in the storage, denoted by `N`. + /// The number of pages in the storage, denoted by N. /// - /// We have `MIN_NUM_PAGES <= N <= MAX_PAGE_INDEX + 1`. + /// We have [`MIN_NUM_PAGES`] ≤ N ≤ [`MAX_PAGE_INDEX`] + 1. pub fn num_pages(&self) -> Nat { self.num_pages } /// The maximum page index. /// - /// We have `2 <= self.max_page() <= MAX_PAGE_INDEX`. + /// This is at least [`MIN_NUM_PAGES`] - 1 and at most [`MAX_PAGE_INDEX`]. pub fn max_page(&self) -> Nat { self.num_pages - 1 } - /// The maximum number of times a page can be erased, denoted by `E`. + /// The maximum number of times a page can be erased, denoted by E. /// - /// We have `E <= MAX_ERASE_CYCLE`. + /// We have E ≤ [`MAX_ERASE_CYCLE`]. pub fn max_page_erases(&self) -> Nat { self.max_page_erases } @@ -235,19 +235,18 @@ impl Format { MAX_UPDATES } - /// The size of a virtual page in words, denoted by `Q`. + /// The size of a virtual page in words, denoted by Q. /// /// A virtual page is stored in a physical page after the page header. /// - /// We have `MIN_NUM_WORDS_PER_PAGE - 2 <= Q <= MAX_VIRT_PAGE_SIZE`. + /// We have [`MIN_PAGE_SIZE`] - 2 ≤ Q ≤ [`MAX_VIRT_PAGE_SIZE`]. pub fn virt_page_size(&self) -> Nat { self.page_size() / self.word_size() - CONTENT_WORD } /// The maximum length in bytes of a user payload. /// - /// We have `(MIN_NUM_WORDS_PER_PAGE - 3) * self.word_size() <= self.max_value_len() <= - /// MAX_VALUE_LEN`. + /// This is at least [`MIN_PAGE_SIZE`] - 3 [words](WORD_SIZE) and at most [`MAX_VALUE_LEN`]. pub fn max_value_len(&self) -> Nat { min( (self.virt_page_size() - 1) * self.word_size(), @@ -255,57 +254,50 @@ impl Format { ) } - /// The maximum prefix length in words, denoted by `M`. + /// The maximum prefix length in words, denoted by M. /// /// A prefix is the first words of a virtual page that belong to the last entry of the previous /// virtual page. This happens because entries may overlap up to 2 virtual pages. /// - /// We have `MIN_NUM_WORDS_PER_PAGE - 3 <= M < Q`. + /// We have [`MIN_PAGE_SIZE`] - 3 ≤ M < Q. pub fn max_prefix_len(&self) -> Nat { self.bytes_to_words(self.max_value_len()) } - /// The total virtual capacity in words, denoted by `V`. + /// The total virtual capacity in words, denoted by V. /// - /// We have `V = (N - 1) * (Q - 1) - M`. + /// We have V = (N - 1) × (Q - 1) - M. /// - /// We can show `V >= (N - 2) * (Q - 1)` with the following steps: - /// - `M <= Q - 1` from `M < Q` from [`M`] definition - /// - `-M >= -(Q - 1)` from above - /// - `V >= (N - 1) * (Q - 1) - (Q - 1)` from `V` definition - /// - /// [`M`]: struct.Format.html#method.max_prefix_len + /// We can show V ≥ (N - 2) × (Q - 1) with the following steps: + /// - M ≤ Q - 1 from M < Q from [M](Format::max_prefix_len)'s definition + /// - -M ≥ -(Q - 1) from above + /// - V ≥ (N - 1) × (Q - 1) - (Q - 1) from V's definition pub fn virt_size(&self) -> Nat { (self.num_pages() - 1) * (self.virt_page_size() - 1) - self.max_prefix_len() } - /// The total user capacity in words, denoted by `C`. + /// The total user capacity in words, denoted by C. /// - /// We have `C = V - N = (N - 1) * (Q - 2) - M - 1`. + /// We have C = V - N = (N - 1) × (Q - 2) - M - 1. /// - /// We can show `C >= (N - 2) * (Q - 2) - 2` with the following steps: - /// - `V >= (N - 2) * (Q - 1)` from [`V`] definition - /// - `C >= (N - 2) * (Q - 1) - N` from `C` definition - /// - `(N - 2) * (Q - 1) - N = (N - 2) * (Q - 2) - 2` by calculus - /// - /// [`V`]: struct.Format.html#method.virt_size + /// We can show C ≥ (N - 2) × (Q - 2) - 2 with the following steps: + /// - V ≥ (N - 2) × (Q - 1) from [V](Format::virt_size)'s definition + /// - C ≥ (N - 2) × (Q - 1) - N from C's definition + /// - (N - 2) × (Q - 1) - N = (N - 2) × (Q - 2) - 2 by calculus pub fn total_capacity(&self) -> Nat { // From the virtual capacity, we reserve N - 1 words for `Erase` entries and 1 word for a // `Clear` entry. self.virt_size() - self.num_pages() } - /// The total virtual lifetime in words, denoted by `L`. + /// The total virtual lifetime in words, denoted by L. /// - /// We have `L = (E * N + N - 1) * Q`. + /// We have L = (E × N + N - 1) × Q. pub fn total_lifetime(&self) -> Position { Position::new(self, self.max_page_erases(), self.num_pages() - 1, 0) } /// Returns the word position of the first entry of a page. - /// - /// The init info of the page must be provided to know where the first entry of the page - /// starts. pub fn page_head(&self, init: InitInfo, page: Nat) -> Position { Position::new(self, init.cycle, page, init.prefix) } @@ -557,7 +549,7 @@ impl Format { /// /// # Preconditions /// - /// - `bytes + self.word_size()` does not overflow. + /// - `bytes` + [`Self::word_size`] does not overflow. pub fn bytes_to_words(&self, bytes: Nat) -> Nat { div_ceil(bytes, self.word_size()) } @@ -571,7 +563,7 @@ const COMPACT_WORD: Nat = 1; /// The word index of the content of a page. /// -/// Since a page is at least 8 words, there is always at least 6 words of content. +/// This is also the length in words of the page header. const CONTENT_WORD: Nat = 2; /// The checksum for a single word. @@ -718,21 +710,21 @@ bitfield! { /// The position of a word in the virtual storage. /// -/// With the notations defined in `Format`, let: -/// - `w` a virtual word offset in a page which is between `0` and `Q - 1` -/// - `p` a page offset which is between `0` and `N - 1` -/// - `c` the number of erase cycles of a page which is between `0` and `E` +/// With the notations defined in [`Format`], let: +/// - w denote a word offset in a virtual page, thus between 0 and Q - 1 +/// - p denote a page offset, thus between 0 and N - 1 +/// - c denote the number of times a page was erased, thus between 0 and E /// -/// Then the position of a word is `(c*N + p)*Q + w`. This position monotonically increases and +/// The position of a word is (c × N + p) × Q + w. This position monotonically increases and /// represents the consumed lifetime of the storage. /// -/// This type is kept abstract to avoid possible confusion with `Nat` and `Word` if they happen to -/// have the same representation. Here is an overview of their semantics: +/// This type is kept abstract to avoid possible confusion with [`Nat`] and [`Word`] if they happen +/// to have the same representation. Here is an overview of their semantics: /// /// | Name | Semantics | Arithmetic operations | Bit-wise operations | /// | ---------- | --------------------------- | --------------------- | ------------------- | -/// | `Nat` | Natural numbers | Yes (no overflow) | No | -/// | `Word` | Word in flash | No | Yes | +/// | [`Nat`] | Natural numbers | Yes (no overflow) | No | +/// | [`Word`] | Word in flash | No | Yes | /// | `Position` | Position in virtual storage | Yes (no overflow) | No | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Position(Nat); @@ -763,9 +755,9 @@ impl Position { /// Create a word position given its coordinates. /// /// The coordinates of a word are: - /// - Its word index in its page. + /// - Its word index in its virtual page. /// - Its page index in the storage. - /// - The number of times that page was erased. + /// - The number of times its page was erased. pub fn new(format: &Format, cycle: Nat, page: Nat, word: Nat) -> Position { Position((cycle * format.num_pages() + page) * format.virt_page_size() + word) } @@ -928,11 +920,11 @@ pub fn is_erased(slice: &[u8]) -> bool { /// Divides then takes ceiling. /// -/// Returns `ceil(x / m)` in mathematical notations (not Rust code). +/// Returns ⌈x / m⌉, i.e. the lowest natural number r such that r ≥ x / m. /// /// # Preconditions /// -/// - `x + m` does not overflow. +/// - x + m does not overflow. const fn div_ceil(x: Nat, m: Nat) -> Nat { (x + m - 1) / m } diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index d19b511..661d5db 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Helper functions for fragmented entries. +//! Support for fragmented entries. //! //! This module permits to handle entries larger than the [maximum value //! length](Store::max_value_length) by storing ordered consecutive fragments in a sequence of keys. @@ -36,7 +36,7 @@ pub trait Keys { /// /// # Preconditions /// - /// The position must be within the length: `pos < len()`. + /// The position must be within the length: `pos` < [`Self::len`]. fn key(&self, pos: usize) -> usize; } diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index a8adc2b..e3735c3 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -12,191 +12,191 @@ // See the License for the specific language governing permissions and // limitations under the License. -// TODO(ia0): Add links once the code is complete. +// The documentation is easier to read from a browser: +// - Run: cargo doc --document-private-items --features=std +// - Open: target/doc/persistent_store/index.html + //! Store abstraction for flash storage //! //! # Specification //! -//! The store provides a partial function from keys to values on top of a storage -//! interface. The store total capacity depends on the size of the storage. Store -//! updates may be bundled in transactions. Mutable operations are atomic, including -//! when interrupted. +//! The [store](Store) provides a partial function from keys to values on top of a +//! [storage](Storage) interface. The store total [capacity](Store::capacity) depends on the size of +//! the storage. Store [updates](StoreUpdate) may be bundled in [transactions](Store::transaction). +//! Mutable operations are atomic, including when interrupted. //! -//! The store is flash-efficient in the sense that it uses the storage lifetime -//! efficiently. For each page, all words are written at least once between erase -//! cycles and all erase cycles are used. However, not all written words are user -//! content: lifetime is also consumed with metadata and compaction. +//! The store is flash-efficient in the sense that it uses the storage [lifetime](Store::lifetime) +//! efficiently. For each page, all words are written at least once between erase cycles and all +//! erase cycles are used. However, not all written words are user content: lifetime is also +//! consumed with metadata and compaction. //! -//! The store is extendable with other entries than key-values. It is essentially a -//! framework providing access to the storage lifetime. The partial function is -//! simply the most common usage and can be used to encode other usages. +//! The store is extendable with other entries than key-values. It is essentially a framework +//! providing access to the storage lifetime. The partial function is simply the most common usage +//! and can be used to encode other usages. //! //! ## Definitions //! -//! An _entry_ is a pair of a key and a value. A _key_ is a number between 0 -//! and 4095. A _value_ is a byte slice with a length between 0 and 1023 bytes (for -//! large enough pages). +//! An _entry_ is a pair of a key and a value. A _key_ is a number between 0 and +//! [4095](format::MAX_KEY_INDEX). A _value_ is a byte slice with a length between 0 and +//! [1023](format::Format::max_value_len) bytes (for large enough pages). //! //! The store provides the following _updates_: -//! - Given a key and a value, `Insert` updates the store such that the value is +//! - Given a key and a value, [`StoreUpdate::Insert`] updates the store such that the value is //! associated with the key. The values for other keys are left unchanged. -//! - Given a key, `Remove` updates the store such that no value is associated with -//! the key. The values for other keys are left unchanged. Additionally, if there -//! was a value associated with the key, the value is wiped from the storage -//! (all its bits are set to 0). +//! - Given a key, [`StoreUpdate::Remove`] updates the store such that no value is associated with +//! the key. The values for other keys are left unchanged. Additionally, if there was a value +//! associated with the key, the value is wiped from the storage (all its bits are set to 0). //! //! The store provides the following _read-only operations_: -//! - `Iter` iterates through the store returning all entries exactly once. The -//! iteration order is not specified but stable between mutable operations. -//! - `Capacity` returns how many words can be stored before the store is full. -//! - `Lifetime` returns how many words can be written before the storage lifetime -//! is consumed. +//! - [`Store::iter`] iterates through the store returning all entries exactly once. The iteration +//! order is not specified but stable between mutable operations. +//! - [`Store::capacity`] returns how many words can be stored before the store is full. +//! - [`Store::lifetime`] returns how many words can be written before the storage lifetime is +//! consumed. //! //! The store provides the following _mutable operations_: -//! - Given a set of independent updates, `Transaction` applies the sequence of -//! updates. -//! - Given a threshold, `Clear` removes all entries with a key greater or equal -//! to the threshold. -//! - Given a length in words, `Prepare` makes one step of compaction unless that -//! many words can be written without compaction. This operation has no effect -//! on the store but may still mutate its storage. In particular, the store has -//! the same capacity but a possibly reduced lifetime. +//! - Given a set of independent updates, [`Store::transaction`] applies the sequence of updates. +//! - Given a threshold, [`Store::clear`] removes all entries with a key greater or equal to the +//! threshold. +//! - Given a length in words, [`Store::prepare`] makes one step of compaction unless that many +//! words can be written without compaction. This operation has no effect on the store but may +//! still mutate its storage. In particular, the store has the same capacity but a possibly +//! reduced lifetime. //! -//! A mutable operation is _atomic_ if, when power is lost during the operation, the -//! store is either updated (as if the operation succeeded) or left unchanged (as if -//! the operation did not occur). If the store is left unchanged, lifetime may still -//! be consumed. +//! A mutable operation is _atomic_ if, when power is lost during the operation, the store is either +//! updated (as if the operation succeeded) or left unchanged (as if the operation did not occur). +//! If the store is left unchanged, lifetime may still be consumed. //! //! The store relies on the following _storage interface_: -//! - It is possible to read a byte slice. The slice won't span multiple pages. -//! - It is possible to write a word slice. The slice won't span multiple pages. -//! - It is possible to erase a page. -//! - The pages are sequentially indexed from 0. If the actual underlying storage -//! is segmented, then the storage layer should translate those indices to -//! actual page addresses. +//! - It is possible to [read](Storage::read_slice) a byte slice. The slice won't span multiple +//! pages. +//! - It is possible to [write](Storage::write_slice) a word slice. The slice won't span multiple +//! pages. +//! - It is possible to [erase](Storage::erase_page) a page. +//! - The pages are sequentially indexed from 0. If the actual underlying storage is segmented, +//! then the storage layer should translate those indices to actual page addresses. //! -//! The store has a _total capacity_ of `C = (N - 1) * (P - 4) - M - 1` words, where -//! `P` is the number of words per page, `N` is the number of pages, and `M` is the -//! maximum length in words of a value (256 for large enough pages). The capacity -//! used by each mutable operation is given below (a transient word only uses -//! capacity during the operation): -//! - `Insert` uses `1 + ceil(len / 4)` words where `len` is the length of the -//! value in bytes. If an entry was replaced, the words used by its insertion -//! are freed. -//! - `Remove` doesn't use capacity if alone in the transaction and 1 transient -//! word otherwise. If an entry was deleted, the words used by its insertion are -//! freed. -//! - `Transaction` uses 1 transient word. In addition, the updates of the -//! transaction use and free words as described above. -//! - `Clear` doesn't use capacity and frees the words used by the insertion of -//! the deleted entries. -//! - `Prepare` doesn't use capacity. +//! The store has a _total capacity_ of C = (N - 1) × (P - 4) - M - 1 words, where: +//! - P is the number of words per page +//! - [N](format::Format::num_pages) is the number of pages +//! - [M](format::Format::max_prefix_len) is the maximum length in words of a value (256 for large +//! enough pages) //! -//! The _total lifetime_ of the store is below `L = ((E + 1) * N - 1) * (P - 2)` and -//! above `L - M` words, where `E` is the maximum number of erase cycles. The -//! lifetime is used when capacity is used, including transiently, as well as when -//! compaction occurs. Compaction frequency and lifetime consumption are positively -//! correlated to the store load factor (the ratio of used capacity to total capacity). +//! The capacity used by each mutable operation is given below (a transient word only uses capacity +//! during the operation): //! -//! It is possible to approximate the cost of transient words in terms of capacity: -//! `L` transient words are equivalent to `C - x` words of capacity where `x` is the -//! average capacity (including transient) of operations. +//! | Operation/Update | Used capacity | Freed capacity | Transient capacity | +//! | ----------------------- | ---------------- | ----------------- | ------------------ | +//! | [`StoreUpdate::Insert`] | 1 + value length | overwritten entry | 0 | +//! | [`StoreUpdate::Remove`] | 0 | deleted entry | see below\* | +//! | [`Store::transaction`] | 0 + updates | 0 + updates | 1 | +//! | [`Store::clear`] | 0 | deleted entries | 0 | +//! | [`Store::prepare`] | 0 | 0 | 0 | +//! +//! \*0 if the update is alone in the transaction, otherwise 1. +//! +//! The _total lifetime_ of the store is below L = ((E + 1) × N - 1) × (P - 2) and above L - M +//! words, where E is the maximum number of erase cycles. The lifetime is used when capacity is +//! used, including transiently, as well as when compaction occurs. Compaction frequency and +//! lifetime consumption are positively correlated to the store load factor (the ratio of used +//! capacity to total capacity). +//! +//! It is possible to approximate the cost of transient words in terms of capacity: L transient +//! words are equivalent to C - x words of capacity where x is the average capacity (including +//! transient) of operations. //! //! ## Preconditions //! //! The following assumptions need to hold, or the store may behave in unexpected ways: -//! - A word can be written twice between erase cycles. -//! - A page can be erased `E` times after the first boot of the store. -//! - When power is lost while writing a slice or erasing a page, the next read -//! returns a slice where a subset (possibly none or all) of the bits that -//! should have been modified have been modified. -//! - Reading a slice is deterministic. When power is lost while writing a slice -//! or erasing a slice (erasing a page containing that slice), reading that -//! slice repeatedly returns the same result (until it is overwritten or its -//! page is erased). -//! - To decide whether a page has been erased, it is enough to test if all its -//! bits are equal to 1. -//! - When power is lost while writing a slice or erasing a page, that operation -//! does not count towards the limits. However, completing that write or erase -//! operation would count towards the limits, as if the number of writes per -//! word and number of erase cycles could be fractional. -//! - The storage is only modified by the store. Note that completely erasing the -//! storage is supported, essentially losing all content and lifetime tracking. -//! It is preferred to use `Clear` with a threshold of 0 to keep the lifetime -//! tracking. +//! - A word can be written [twice](Storage::max_word_writes) between erase cycles. +//! - A page can be erased [E](Storage::max_page_erases) times after the first boot of the store. +//! - When power is lost while writing a slice or erasing a page, the next read returns a slice +//! where a subset (possibly none or all) of the bits that should have been modified have been +//! modified. +//! - Reading a slice is deterministic. When power is lost while writing a slice or erasing a +//! slice (erasing a page containing that slice), reading that slice repeatedly returns the same +//! result (until it is overwritten or its page is erased). +//! - To decide whether a page has been erased, it is enough to test if all its bits are equal +//! to 1. +//! - When power is lost while writing a slice or erasing a page, that operation does not count +//! towards the limits. However, completing that write or erase operation would count towards +//! the limits, as if the number of writes per word and number of erase cycles could be +//! fractional. +//! - The storage is only modified by the store. Note that completely erasing the storage is +//! supported, essentially losing all content and lifetime tracking. It is preferred to use +//! [`Store::clear`] with a threshold of 0 to keep the lifetime tracking. //! -//! The store properties may still hold outside some of those assumptions, but with -//! an increasing chance of failure. +//! The store properties may still hold outside some of those assumptions, but with an increasing +//! chance of failure. //! //! # Implementation //! //! We define the following constants: -//! - `E < 65536` the number of times a page can be erased. -//! - `3 <= N < 64` the number of pages in the storage. -//! - `8 <= P <= 1024` the number of words in a page. -//! - `Q = P - 2` the number of words in a virtual page. -//! - `K = 4096` the maximum number of keys. -//! - `M = min(Q - 1, 256)` the maximum length in words of a value. -//! - `V = (N - 1) * (Q - 1) - M` the virtual capacity. -//! - `C = V - N` the user capacity. +//! - [E](format::Format::max_page_erases) ≤ [65535](format::MAX_ERASE_CYCLE) the number of times +//! a page can be erased. +//! - 3 ≤ [N](format::Format::num_pages) < 64 the number of pages in the storage. +//! - 8 ≤ P ≤ 1024 the number of words in a page. +//! - [Q](format::Format::virt_page_size) = P - 2 the number of words in a virtual page. +//! - [M](format::Format::max_prefix_len) = min(Q - 1, 256) the maximum length in words of a +//! value. +//! - [V](format::Format::virt_size) = (N - 1) × (Q - 1) - M the virtual capacity. +//! - [C](format::Format::total_capacity) = V - N the user capacity. //! -//! We build a virtual storage from the physical storage using the first 2 words of -//! each page: +//! We build a virtual storage from the physical storage using the first 2 words of each page: //! - The first word contains the number of times the page has been erased. -//! - The second word contains the starting word to which this page is being moved -//! during compaction. +//! - The second word contains the starting word to which this page is being moved during +//! compaction. //! -//! The virtual storage has a length of `(E + 1) * N * Q` words and represents the -//! lifetime of the store. (We reserve the last `Q + M` words to support adding -//! emergency lifetime.) This virtual storage has a linear address space. +//! The virtual storage has a length of (E + 1) × N × Q words and represents the lifetime of the +//! store. (We reserve the last Q + M words to support adding emergency lifetime.) This virtual +//! storage has a linear address space. //! -//! We define a set of overlapping windows of `N * Q` words at each `Q`-aligned -//! boundary. We call `i` the window spanning from `i * Q` to `(i + N) * Q`. Only -//! those windows actually exist in the underlying storage. We use compaction to -//! shift the current window from `i` to `i + 1`, preserving the content of the -//! store. +//! We define a set of overlapping windows of N × Q words at each Q-aligned boundary. We call i the +//! window spanning from i × Q to (i + N) × Q. Only those windows actually exist in the underlying +//! storage. We use compaction to shift the current window from i to i + 1, preserving the content +//! of the store. //! -//! For a given state of the virtual storage, we define `h_i` as the position of the -//! first entry of the window `i`. We call it the head of the window `i`. Because -//! entries are at most `M + 1` words, they can overlap on the next page only by `M` -//! words. So we have `i * Q <= h_i <= i * Q + M` . Since there are no entries -//! before the first page, we have `h_0 = 0`. +//! For a given state of the virtual storage, we define h\_i as the position of the first entry of +//! the window i. We call it the head of the window i. Because entries are at most M + 1 words, they +//! can overlap on the next page only by M words. So we have i × Q ≤ h_i ≤ i × Q + M . Since there +//! are no entries before the first page, we have h\_0 = 0. //! -//! We define `t_i` as one past the last entry of the window `i`. If there are no -//! entries in that window, we have `t_i = h_i`. We call `t_i` the tail of the -//! window `i`. We define the compaction invariant as `t_i - h_i <= V`. +//! We define t\_i as one past the last entry of the window i. If there are no entries in that +//! window, we have t\_i = h\_i. We call t\_i the tail of the window i. We define the compaction +//! invariant as t\_i - h\_i ≤ V. //! -//! We define `|x|` as the capacity used before position `x`. We have `|x| <= x`. We -//! define the capacity invariant as `|t_i| - |h_i| <= C`. +//! We define |x| as the capacity used before position x. We have |x| ≤ x. We define the capacity +//! invariant as |t\_i| - |h\_i| ≤ C. //! -//! Using this virtual storage, entries are appended to the tail as long as there is -//! both virtual capacity to preserve the compaction invariant and capacity to -//! preserve the capacity invariant. When virtual capacity runs out, the first page -//! of the window is compacted and the window is shifted. +//! Using this virtual storage, entries are appended to the tail as long as there is both virtual +//! capacity to preserve the compaction invariant and capacity to preserve the capacity invariant. +//! When virtual capacity runs out, the first page of the window is compacted and the window is +//! shifted. //! -//! Entries are identified by a prefix of bits. The prefix has to contain at least -//! one bit set to zero to differentiate from the tail. Entries can be one of: -//! - Padding: A word whose first bit is set to zero. The rest is arbitrary. This -//! entry is used to mark words partially written after an interrupted operation -//! as padding such that they are ignored by future operations. -//! - Header: A word whose second bit is set to zero. It contains the following fields: -//! - A bit indicating whether the entry is deleted. -//! - A bit indicating whether the value is word-aligned and has all bits set -//! to 1 in its last word. The last word of an entry is used to detect that -//! an entry has been fully written. As such it must contain at least one -//! bit equal to zero. -//! - The key of the entry. -//! - The length in bytes of the value. The value follows the header. The -//! entry is word-aligned if the value is not. -//! - The checksum of the first and last word of the entry. -//! - Erase: A word used during compaction. It contains the page to be erased and -//! a checksum. -//! - Clear: A word used during the `Clear` operation. It contains the threshold -//! and a checksum. -//! - Marker: A word used during the `Transaction` operation. It contains the -//! number of updates following the marker and a checksum. -//! - Remove: A word used during the `Transaction` operation. It contains the key -//! of the entry to be removed and a checksum. +//! Entries are identified by a prefix of bits. The prefix has to contain at least one bit set to +//! zero to differentiate from the tail. Entries can be one of: +//! - [Padding](format::ID_PADDING): A word whose first bit is set to zero. The rest is arbitrary. +//! This entry is used to mark words partially written after an interrupted operation as padding +//! such that they are ignored by future operations. +//! - [Header](format::ID_HEADER): A word whose second bit is set to zero. It contains the +//! following fields: +//! - A [bit](format::HEADER_DELETED) indicating whether the entry is deleted. +//! - A [bit](format::HEADER_FLIPPED) indicating whether the value is word-aligned and has all +//! bits set to 1 in its last word. The last word of an entry is used to detect that an +//! entry has been fully written. As such it must contain at least one bit equal to zero. +//! - The [key](format::HEADER_KEY) of the entry. +//! - The [length](format::HEADER_LENGTH) in bytes of the value. The value follows the header. +//! The entry is word-aligned if the value is not. +//! - The [checksum](format::HEADER_CHECKSUM) of the first and last word of the entry. +//! - [Erase](format::ID_ERASE): A word used during compaction. It contains the +//! [page](format::ERASE_PAGE) to be erased and a [checksum](format::WORD_CHECKSUM). +//! - [Clear](format::ID_CLEAR): A word used during the clear operation. It contains the +//! [threshold](format::CLEAR_MIN_KEY) and a [checksum](format::WORD_CHECKSUM). +//! - [Marker](format::ID_MARKER): A word used during a transaction. It contains the [number of +//! updates](format::MARKER_COUNT) following the marker and a [checksum](format::WORD_CHECKSUM). +//! - [Remove](format::ID_REMOVE): A word used inside a transaction. It contains the +//! [key](format::REMOVE_KEY) of the entry to be removed and a +//! [checksum](format::WORD_CHECKSUM). //! //! Checksums are the number of bits equal to 0. //! @@ -204,107 +204,105 @@ //! //! ## Compaction //! -//! It should always be possible to fully compact the store, after what the -//! remaining capacity should be available in the current window (restoring the -//! compaction invariant). We consider all notations on the virtual storage after -//! the full compaction. We will use the `|x|` notation although we update the state -//! of the virtual storage. This is fine because compaction doesn't change the -//! status of an existing word. +//! It should always be possible to fully compact the store, after what the remaining capacity +//! should be available in the current window (restoring the compaction invariant). We consider all +//! notations on the virtual storage after the full compaction. We will use the |x| notation +//! although we update the state of the virtual storage. This is fine because compaction doesn't +//! change the status of an existing word. //! -//! We want to show that the next `N - 1` compactions won't move the tail past the -//! last page of their window, with `I` the initial window: +//! We want to show that the next N - 1 compactions won't move the tail past the last page of their +//! window, with I the initial window: //! -//! ```text -//! forall 1 <= i <= N - 1, t_{I + i} <= (I + i + N - 1) * Q -//! ``` +//! | | | | | +//! | ----------------:| ----------:|:-:|:------------------- | +//! | ∀(1 ≤ i ≤ N - 1) | t\_{I + i} | ≤ | (I + i + N - 1) × Q | //! -//! We assume `i` between `1` and `N - 1`. +//! We assume i between 1 and N - 1. //! -//! One step of compaction advances the tail by how many words were used in the -//! first page of the window with the last entry possibly overlapping on the next -//! page. +//! One step of compaction advances the tail by how many words were used in the first page of the +//! window with the last entry possibly overlapping on the next page. //! -//! ```text -//! forall j, t_{j + 1} = t_j + |h_{j + 1}| - |h_j| + 1 -//! ``` +//! | | | | | +//! | --:| ----------:|:-:|:------------------------------------ | +//! | ∀j | t\_{j + 1} | = | t\_j + \|h\_{j + 1}\| - \|h\_j\| + 1 | //! //! By induction, we have: //! -//! ```text -//! t_{I + i} <= t_I + |h_{I + i}| - |h_I| + i -//! ``` +//! | | | | +//! | ----------:|:-:|:------------------------------------ | +//! | t\_{I + i} | ≤ | t\_I + \|h\_{I + i}\| - \|h\_I\| + i | //! //! We have the following properties: //! -//! ```text -//! t_I <= h_I + V -//! |h_{I + i}| - |h_I| <= h_{I + i} - h_I -//! h_{I + i} <= (I + i) * Q + M -//! ``` +//! | | | | +//! | -------------------------:|:-:|:----------------- | +//! | t\_I | ≤ | h\_I + V | +//! | \|h\_{I + i}\| - \|h\_I\| | ≤ | h\_{I + i} - h\_I | +//! | h\_{I + i} | ≤ | (I + i) × Q + M | //! //! Replacing into our previous equality, we can conclude: //! -//! ```text -//! t_{I + i} = t_I + |h_{I + i}| - |h_I| + i -//! <= h_I + V + (I + i) * Q + M - h_I + i -//! = (N - 1) * (Q - 1) - M + (I + i) * Q + M + i -//! = (N - 1) * (Q - 1) + (I + i) * Q + i -//! = (I + i + N - 1) * Q + i - (N - 1) -//! <= (I + i + N - 1) * Q -//! ``` +//! | | | | +//! | ----------:|:-:| ------------------------------------------- | +//! | t\_{I + i} | = | t_I + \|h_{I + i}\| - \|h_I\| + i | +//! | | ≤ | h\_I + V + (I + i) * Q + M - h\_I + i | +//! | | = | (N - 1) × (Q - 1) - M + (I + i) × Q + M + i | +//! | | = | (N - 1) × (Q - 1) + (I + i) × Q + i | +//! | | = | (I + i + N - 1) × Q + i - (N - 1) | +//! | | ≤ | (I + i + N - 1) × Q | //! -//! We also want to show that after `N - 1` compactions, the remaining capacity is -//! available without compaction. +//! We also want to show that after N - 1 compactions, the remaining capacity is available without +//! compaction. //! -//! ```text -//! V - (t_{I + N - 1} - h_{I + N - 1}) >= // The available words in the window. -//! C - (|t_{I + N - 1}| - |h_{I + N - 1}|) // The remaining capacity. -//! + 1 // Reserved for Clear. -//! ``` +//! | | | | +//! | -:| --------------------------------------------- | --------------------------------- | +//! | | V - (t\_{I + N - 1} - h\_{I + N - 1}) | The available words in the window | +//! | ≥ | C - (\|t\_{I + N - 1}\| - \|h\_{I + N - 1}\|) | The remaining capacity | +//! | + | 1 | Reserved for clear | //! -//! We can replace the definition of `C` and simplify: +//! We can replace the definition of C and simplify: //! -//! ```text -//! V - (t_{I + N - 1} - h_{I + N - 1}) >= V - N - (|t_{I + N - 1}| - |h_{I + N - 1}|) + 1 -//! iff t_{I + N - 1} - h_{I + N - 1} <= |t_{I + N - 1}| - |h_{I + N - 1}| + N - 1 -//! ``` +//! | | | | | +//! | ---:| -------------------------------------:|:-:|:----------------------------------------------------- | +//! | | V - (t\_{I + N - 1} - h\_{I + N - 1}) | ≥ | V - N - (\|t\_{I + N - 1}\| - \|h\_{I + N - 1}\|) + 1 | +//! | iff | t\_{I + N - 1} - h\_{I + N - 1} | ≤ | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| + N - 1 | //! //! We have the following properties: //! -//! ```text -//! t_{I + N - 1} = t_I + |h_{I + N - 1}| - |h_I| + N - 1 -//! |t_{I + N - 1}| - |h_{I + N - 1}| = |t_I| - |h_I| // Compaction preserves capacity. -//! |h_{I + N - 1}| - |t_I| <= h_{I + N - 1} - t_I -//! ``` +//! +//! | | | | | +//! | ---------------------------------------:|:-:|:-------------------------------------------- |:------ | +//! | t\_{I + N - 1} | = | t\_I + \|h\_{I + N - 1}\| - \|h\_I\| + N - 1 | | +//! | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| | = | \|t\_I\| - \|h\_I\| | Compaction preserves capacity | +//! | \|h\_{I + N - 1}\| - \|t\_I\| | ≤ | h\_{I + N - 1} - t\_I | | //! //! From which we conclude: //! -//! ```text -//! t_{I + N - 1} - h_{I + N - 1} <= |t_{I + N - 1}| - |h_{I + N - 1}| + N - 1 -//! iff t_I + |h_{I + N - 1}| - |h_I| + N - 1 - h_{I + N - 1} <= |t_I| - |h_I| + N - 1 -//! iff t_I + |h_{I + N - 1}| - h_{I + N - 1} <= |t_I| -//! iff |h_{I + N - 1}| - |t_I| <= h_{I + N - 1} - t_I -//! ``` +//! | | | | | +//! | ---:| -------------------------------:|:-:|:----------------------------------------------- | +//! | | t\_{I + N - 1} - h\_{I + N - 1} | ≤ | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| + N - 1 | +//! | iff | t\_I + \|h\_{I + N - 1}\| - \|h\_I\| + N - 1 - h\_{I + N - 1} | ≤ | \|t\_I\| - \|h\_I\| + N - 1 | +//! | iff | t\_I + \|h\_{I + N - 1}\| - h\_{I + N - 1} | ≤ | \|t\_I\| | +//! | iff | \|h\_{I + N - 1}\| - \|t\_I\| | ≤ | h\_{I + N - 1} - t\_I | //! //! //! ## Checksum //! -//! The main property we want is that all partially written/erased words are either -//! the initial word, the final word, or invalid. +//! The main property we want is that all partially written/erased words are either the initial +//! word, the final word, or invalid. //! -//! We say that a bit sequence `TARGET` is reachable from a bit sequence `SOURCE` if -//! both have the same length and `SOURCE & TARGET == TARGET` where `&` is the -//! bitwise AND operation on bit sequences of that length. In other words, when -//! `SOURCE` has a bit equal to 0 then `TARGET` also has that bit equal to 0. +//! We say that a bit sequence `TARGET` is reachable from a bit sequence `SOURCE` if both have the +//! same length and `SOURCE & TARGET == TARGET` where `&` is the bitwise AND operation on bit +//! sequences of that length. In other words, when `SOURCE` has a bit equal to 0 then `TARGET` also +//! has that bit equal to 0. //! -//! The only written entries start with `101` or `110` and are written from an -//! erased word. Marking an entry as padding or deleted is a single bit operation, -//! so the property trivially holds. For those cases, the proof relies on the fact -//! that there is exactly one bit equal to 0 in the 3 first bits. Either the 3 first -//! bits are still `111` in which case we expect the remaining bits to be equal -//! to 1. Otherwise we can use the checksum of the given type of entry because those -//! 2 types of entries are not reachable from each other. Here is a visualization of -//! the partitioning based on the first 3 bits: +//! The only written entries start with `101` or `110` and are written from an erased word. Marking +//! an entry as padding or deleted is a single bit operation, so the property trivially holds. For +//! those cases, the proof relies on the fact that there is exactly one bit equal to 0 in the 3 +//! first bits. Either the 3 first bits are still `111` in which case we expect the remaining bits +//! to be equal to 1. Otherwise we can use the checksum of the given type of entry because those 2 +//! types of entries are not reachable from each other. Here is a visualization of the partitioning +//! based on the first 3 bits: //! //! | First 3 bits | Description | How to check | //! | ------------:| ------------------ | ---------------------------- | @@ -314,34 +312,27 @@ //! | `100` | Deleted user entry | No check, atomically written | //! | `0??` | Padding entry | No check, atomically written | //! -//! To show that valid entries of a given type are not reachable from each other, we -//! show 3 lemmas: +//! To show that valid entries of a given type are not reachable from each other, we show 3 lemmas: //! -//! 1. A bit sequence is not reachable from another if its number of bits equal to -//! 0 is smaller. +//! 1. A bit sequence is not reachable from another if its number of bits equal to 0 is smaller. +//! 2. A bit sequence is not reachable from another if they have the same number of bits equals to +//! 0 and are different. +//! 3. A bit sequence is not reachable from another if it is bigger when they are interpreted as +//! numbers in binary representation. //! -//! 2. A bit sequence is not reachable from another if they have the same number of -//! bits equals to 0 and are different. -//! -//! 3. A bit sequence is not reachable from another if it is bigger when they are -//! interpreted as numbers in binary representation. -//! -//! From those lemmas we consider the 2 cases. If both entries have the same number -//! of bits equal to 0, they are either equal or not reachable from each other -//! because of the second lemma. If they don't have the same number of bits equal to -//! 0, then the one with less bits equal to 0 is not reachable from the other -//! because of the first lemma and the one with more bits equal to 0 is not -//! reachable from the other because of the third lemma and the definition of the -//! checksum. +//! From those lemmas we consider the 2 cases. If both entries have the same number of bits equal to +//! 0, they are either equal or not reachable from each other because of the second lemma. If they +//! don't have the same number of bits equal to 0, then the one with less bits equal to 0 is not +//! reachable from the other because of the first lemma and the one with more bits equal to 0 is not +//! reachable from the other because of the third lemma and the definition of the checksum. //! //! # Fuzzing //! -//! For any sequence of operations and interruptions starting from an erased -//! storage, the store is checked against its model and some internal invariant at -//! each step. +//! For any sequence of operations and interruptions starting from an erased storage, the store is +//! checked against its model and some internal invariant at each step. //! -//! For any sequence of operations and interruptions starting from an arbitrary -//! storage, the store is checked not to crash. +//! For any sequence of operations and interruptions starting from an arbitrary storage, the store +//! is checked not to crash. #![cfg_attr(not(feature = "std"), no_std)] #![feature(try_trait)] diff --git a/libraries/persistent_store/src/model.rs b/libraries/persistent_store/src/model.rs index eebc329..e0bf9e3 100644 --- a/libraries/persistent_store/src/model.rs +++ b/libraries/persistent_store/src/model.rs @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Store specification. + use crate::format::Format; use crate::{usize_to_nat, StoreError, StoreRatio, StoreResult, StoreUpdate}; use std::collections::HashMap; /// Models the mutable operations of a store. /// -/// The model doesn't model the storage and read-only operations. This is done by the driver. +/// The model doesn't model the storage and read-only operations. This is done by the +/// [driver](crate::StoreDriver). #[derive(Clone, Debug)] pub struct StoreModel { /// Represents the content of the store. diff --git a/libraries/persistent_store/src/storage.rs b/libraries/persistent_store/src/storage.rs index becd900..fb5b0cb 100644 --- a/libraries/persistent_store/src/storage.rs +++ b/libraries/persistent_store/src/storage.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Flash storage abstraction. + /// Represents a byte position in a storage. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct StorageIndex { @@ -65,12 +67,14 @@ pub trait Storage { /// The following pre-conditions must hold: /// - The `index` must designate `value.len()` bytes in the storage. /// - Both `index` and `value.len()` must be word-aligned. - /// - The written words should not have been written too many times since last page erasure. + /// - The written words should not have been written [too many](Self::max_word_writes) times + /// since the last page erasure. fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()>; /// Erases a page of the storage. /// - /// The `page` must be in the storage. + /// The `page` must be in the storage, i.e. less than [`Storage::num_pages`]. And the page + /// should not have been erased [too many](Self::max_page_erases) times. fn erase_page(&mut self, page: usize) -> StorageResult<()>; } diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index f19f463..c143c89 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Store implementation. + use crate::format::{ is_erased, CompactInfo, Format, Header, InitInfo, InternalEntry, Padding, ParsedWord, Position, Word, WordState, @@ -55,17 +57,14 @@ pub enum StoreError { /// /// The consequences depend on the storage failure. In particular, the operation may or may not /// have succeeded, and the storage may have become invalid. Before doing any other operation, - /// the store should be [recovered]. The operation may then be retried if idempotent. - /// - /// [recovered]: struct.Store.html#method.recover + /// the store should be [recovered](Store::recover). The operation may then be retried if + /// idempotent. StorageError, /// Storage is invalid. /// - /// The storage should be erased and the store [recovered]. The store would be empty and have - /// lost track of lifetime. - /// - /// [recovered]: struct.Store.html#method.recover + /// The storage should be erased and the store [recovered](Store::recover). The store would be + /// empty and have lost track of lifetime. InvalidStorage, } @@ -92,14 +91,12 @@ pub type StoreResult = Result; /// Progression ratio for store metrics. /// -/// This is used for the [capacity] and [lifetime] metrics. Those metrics are measured in words. +/// This is used for the [`Store::capacity`] and [`Store::lifetime`] metrics. Those metrics are +/// measured in words. /// /// # Invariant /// -/// - The used value does not exceed the total: `used <= total`. -/// -/// [capacity]: struct.Store.html#method.capacity -/// [lifetime]: struct.Store.html#method.lifetime +/// - The used value does not exceed the total: `used` ≤ `total`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct StoreRatio { /// How much of the metric is used. @@ -148,11 +145,11 @@ impl StoreHandle { self.key as usize } - /// Returns the length of value of the entry. + /// Returns the value length of the entry. /// /// # Errors /// - /// Returns `InvalidArgument` if the entry has been deleted or compacted. + /// Returns [`StoreError::InvalidArgument`] if the entry has been deleted or compacted. pub fn get_length(&self, store: &Store) -> StoreResult { store.get_length(self) } @@ -161,7 +158,7 @@ impl StoreHandle { /// /// # Errors /// - /// Returns `InvalidArgument` if the entry has been deleted or compacted. + /// Returns [`StoreError::InvalidArgument`] if the entry has been deleted or compacted. pub fn get_value(&self, store: &Store) -> StoreResult> { store.get_value(self) } @@ -211,7 +208,7 @@ pub struct Store { /// The list of the position of the user entries. /// - /// The position is encoded as the word offset from the [head](Store#structfield.head). + /// The position is encoded as the word offset from the [head](Store::head). entries: Option>, } @@ -224,7 +221,8 @@ impl Store { /// /// # Errors /// - /// Returns `InvalidArgument` if the storage is not supported. + /// Returns [`StoreError::InvalidArgument`] if the storage is not + /// [supported](Format::is_storage_supported). pub fn new(storage: S) -> Result, (StoreError, S)> { let format = match Format::new(&storage) { None => return Err((StoreError::InvalidArgument, storage)), @@ -258,7 +256,7 @@ impl Store { ))) } - /// Returns the current capacity in words. + /// Returns the current and total capacity in words. /// /// The capacity represents the size of what is stored. pub fn capacity(&self) -> StoreResult { @@ -271,7 +269,7 @@ impl Store { Ok(StoreRatio { used, total }) } - /// Returns the current lifetime in words. + /// Returns the current and total lifetime in words. /// /// The lifetime represents the age of the storage. The limit is an over-approximation by at /// most the maximum length of a value (the actual limit depends on the length of the prefix of @@ -286,10 +284,11 @@ impl Store { /// /// # Errors /// - /// Returns `InvalidArgument` in the following circumstances: - /// - There are too many updates. + /// Returns [`StoreError::InvalidArgument`] in the following circumstances: + /// - There are [too many](Format::max_updates) updates. /// - The updates overlap, i.e. their keys are not disjoint. - /// - The updates are invalid, e.g. key out of bound or value too long. + /// - The updates are invalid, e.g. key [out of bound](Format::max_key) or value [too + /// long](Format::max_value_len). pub fn transaction>( &mut self, updates: &[StoreUpdate], From 6cb6538db6782d6ea2dce5fe685395cfb28b0316 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 15 Mar 2021 12:10:13 +0100 Subject: [PATCH 78/86] Fix typography --- libraries/persistent_store/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index e3735c3..4be15e3 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -27,7 +27,7 @@ //! //! The store is flash-efficient in the sense that it uses the storage [lifetime](Store::lifetime) //! efficiently. For each page, all words are written at least once between erase cycles and all -//! erase cycles are used. However, not all written words are user content: lifetime is also +//! erase cycles are used. However, not all written words are user content: Lifetime is also //! consumed with metadata and compaction. //! //! The store is extendable with other entries than key-values. It is essentially a framework From e5313057f9874bd8a0715c4f006cf5b96bbb407c Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Mon, 15 Mar 2021 13:36:28 +0100 Subject: [PATCH 79/86] PIN protocol V2 in ClientPin (#293) * PIN protocol V2 in ClientPin * the test ClientPin has a random second private key --- src/ctap/client_pin.rs | 1055 +++++++++++++++++------------ src/ctap/command.rs | 23 +- src/ctap/config_command.rs | 25 +- src/ctap/credential_management.rs | 23 +- src/ctap/data_formats.rs | 79 ++- src/ctap/large_blobs.rs | 19 +- src/ctap/mod.rs | 8 +- src/ctap/pin_protocol.rs | 108 +-- 8 files changed, 815 insertions(+), 525 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 245f020..12e049c 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -13,11 +13,14 @@ // limitations under the License. use super::command::AuthenticatorClientPinParameters; -use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; +use super::data_formats::{ + ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PinUvAuthProtocol, +}; use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use alloc::boxed::Box; use alloc::str; use alloc::string::String; use alloc::vec::Vec; @@ -48,29 +51,6 @@ pub const PIN_TOKEN_LENGTH: usize = 32; /// is fixed since CTAP2.1. const PIN_PADDED_LENGTH: usize = 64; -/// Computes and encrypts the HMAC-secret outputs. -/// -/// To compute them, we first have to decrypt the HMAC secret salt(s) that were -/// encrypted with the shared secret. The credRandom is used as a secret in HMAC -/// for those salts. -fn encrypt_hmac_secret_output( - rng: &mut impl Rng256, - shared_secret: &dyn SharedSecret, - salt_enc: &[u8], - cred_random: &[u8; 32], -) -> Result, Ctap2StatusCode> { - let decrypted_salts = shared_secret.decrypt(salt_enc)?; - if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); - if decrypted_salts.len() == 64 { - let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); - output.append(&mut output2); - } - shared_secret.encrypt(rng, &output) -} - /// Decrypts the new_pin_enc and outputs the found PIN. fn decrypt_pin( shared_secret: &dyn SharedSecret, @@ -124,6 +104,7 @@ pub enum PinPermission { pub struct ClientPin { pin_protocol_v1: PinProtocol, + pin_protocol_v2: PinProtocol, consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, @@ -133,12 +114,42 @@ impl ClientPin { pub fn new(rng: &mut impl Rng256) -> ClientPin { ClientPin { pin_protocol_v1: PinProtocol::new(rng), + pin_protocol_v2: PinProtocol::new(rng), consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, } } + /// Gets a reference to the PIN protocol of the given version. + fn get_pin_protocol(&self, pin_uv_auth_protocol: PinUvAuthProtocol) -> &PinProtocol { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => &self.pin_protocol_v1, + PinUvAuthProtocol::V2 => &self.pin_protocol_v2, + } + } + + /// Gets a mutable reference to the PIN protocol of the given version. + fn get_mut_pin_protocol( + &mut self, + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> &mut PinProtocol { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => &mut self.pin_protocol_v1, + PinUvAuthProtocol::V2 => &mut self.pin_protocol_v2, + } + } + + /// Computes the shared secret for the given version. + fn get_shared_secret( + &self, + pin_uv_auth_protocol: PinUvAuthProtocol, + key_agreement: CoseKey, + ) -> Result, Ctap2StatusCode> { + self.get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol) + } + /// Checks the given encrypted PIN hash against the stored PIN hash. /// /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. @@ -148,6 +159,7 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, + pin_uv_auth_protocol: PinUvAuthProtocol, shared_secret: &dyn SharedSecret, pin_hash_enc: Vec, ) -> Result<(), Ctap2StatusCode> { @@ -157,13 +169,13 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } persistent_store.decr_pin_retries()?; - if pin_hash_enc.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - let pin_hash_dec = shared_secret.decrypt(&pin_hash_enc)?; + let pin_hash_dec = shared_secret + .decrypt(&pin_hash_enc) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?; if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { - self.pin_protocol_v1.regenerate(rng); + self.get_mut_pin_protocol(pin_uv_auth_protocol) + .regenerate(rng); if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } @@ -193,9 +205,16 @@ impl ClientPin { }) } - fn process_get_key_agreement(&self) -> Result { + fn process_get_key_agreement( + &self, + client_pin_params: AuthenticatorClientPinParameters, + ) -> Result { + let key_agreement = Some( + self.get_pin_protocol(client_pin_params.pin_uv_auth_protocol) + .get_public_key(), + ); Ok(AuthenticatorClientPinResponse { - key_agreement: Some(self.pin_protocol_v1.get_public_key()), + key_agreement, pin_token: None, retries: None, }) @@ -204,15 +223,24 @@ impl ClientPin { fn process_set_pin( &mut self, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, + client_pin_params: AuthenticatorClientPinParameters, ) -> Result<(), Ctap2StatusCode> { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_uv_auth_param, + new_pin_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; + let new_pin_enc = ok_or_missing(new_pin_enc)?; + if persistent_store.pin_hash()?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; - shared_secret.verify(&new_pin_enc, &pin_auth)?; + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + shared_secret.verify(&new_pin_enc, &pin_uv_auth_param)?; check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; persistent_store.reset_pin_retries()?; @@ -223,22 +251,39 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - pin_hash_enc: Vec, + client_pin_params: AuthenticatorClientPinParameters, ) -> Result<(), Ctap2StatusCode> { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_uv_auth_param, + new_pin_enc, + pin_hash_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; + let new_pin_enc = ok_or_missing(new_pin_enc)?; + let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); - shared_secret.verify(&auth_param_data, &pin_auth)?; - self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; + shared_secret.verify(&auth_param_data, &pin_uv_auth_param)?; + self.verify_pin_hash_enc( + rng, + persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc, + )?; check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); Ok(()) } @@ -246,19 +291,37 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, + client_pin_params: AuthenticatorClientPinParameters, ) -> Result { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_hash_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; - self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + self.verify_pin_hash_enc( + rng, + persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc, + )?; if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt(rng, self.pin_protocol_v1.get_pin_uv_auth_token())?; + let pin_token = shared_secret.encrypt( + rng, + self.get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token(), + )?; self.permissions = 0x03; self.permissions_rp_id = None; @@ -273,16 +336,14 @@ impl ClientPin { &self, // If you want to support local user verification, implement this function. // Lacking a fingerprint reader, this subcommand is currently unsupported. - _key_agreement: CoseKey, - _permissions: u8, - _permissions_rp_id: Option, + _client_pin_params: AuthenticatorClientPinParameters, ) -> Result { - // User verifications is only supported through PIN currently. + // User verification is only supported through PIN currently. Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } fn process_get_uv_retries(&self) -> Result { - // User verifications is only supported through PIN currently. + // User verification is only supported through PIN currently. Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } @@ -290,11 +351,13 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, - permissions: u8, - permissions_rp_id: Option, + mut client_pin_params: AuthenticatorClientPinParameters, ) -> Result { + let permissions = ok_or_missing(client_pin_params.permissions)?; + // Mutating client_pin_params is just an optimization to move it into + // process_get_pin_token, without cloning permissions_rp_id here. + let permissions_rp_id = client_pin_params.permissions_rp_id.take(); + if permissions == 0 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } @@ -303,8 +366,7 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let response = - self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; + let response = self.process_get_pin_token(rng, persistent_store, client_pin_params)?; self.permissions = permissions; self.permissions_rp_id = permissions_rp_id; @@ -312,102 +374,80 @@ impl ClientPin { Ok(response) } + /// Processes the authenticatorClientPin command. pub fn process_command( &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { - let AuthenticatorClientPinParameters { - pin_uv_auth_protocol, - sub_command, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - permissions, - permissions_rp_id, - } = client_pin_params; - - if pin_uv_auth_protocol != 1 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - - let response = match sub_command { + let response = match client_pin_params.sub_command { ClientPinSubCommand::GetPinRetries => { Some(self.process_get_pin_retries(persistent_store)?) } - ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?), + ClientPinSubCommand::GetKeyAgreement => { + Some(self.process_get_key_agreement(client_pin_params)?) + } ClientPinSubCommand::SetPin => { - self.process_set_pin( - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; + self.process_set_pin(persistent_store, client_pin_params)?; None } ClientPinSubCommand::ChangePin => { - self.process_change_pin( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; + self.process_change_pin(rng, persistent_store, client_pin_params)?; None } - ClientPinSubCommand::GetPinToken => Some(self.process_get_pin_token( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?), + ClientPinSubCommand::GetPinToken => { + Some(self.process_get_pin_token(rng, persistent_store, client_pin_params)?) + } ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( - self.process_get_pin_uv_auth_token_using_uv_with_permissions( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, - )?, + self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, ), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( rng, persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, + client_pin_params, )?, ), }; Ok(ResponseData::AuthenticatorClientPin(response)) } + /// Verifies the HMAC for the PIN protocol V1 pinUvAuthToken. pub fn verify_pin_auth_token( &self, hmac_contents: &[u8], - pin_auth: &[u8], + pin_uv_auth_param: &[u8], ) -> Result<(), Ctap2StatusCode> { - // TODO(kaczmarczyck) pass the protocol number verify_pin_uv_auth_token( - self.pin_protocol_v1.get_pin_uv_auth_token(), + self.get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(), hmac_contents, - pin_auth, - 1, + pin_uv_auth_param, + PinUvAuthProtocol::V1, ) } + /// Resets all held state. pub fn reset(&mut self, rng: &mut impl Rng256) { self.pin_protocol_v1.regenerate(rng); self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.regenerate(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; self.permissions = 0; self.permissions_rp_id = None; } + /// Verifies, computes and encrypts the HMAC-secret outputs. + /// + /// The salt_enc is + /// - verified with the shared secret and salt_auth, + /// - decrypted with the shared secret, + /// - HMAC'ed with cred_random. + /// The length of the output matches salt_enc and has to be 1 or 2 blocks of + /// 32 byte. pub fn process_hmac_secret( &self, rng: &mut impl Rng256, @@ -418,10 +458,23 @@ impl ClientPin { key_agreement, salt_enc, salt_auth, + pin_uv_auth_protocol, } = hmac_secret_input; - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + let shared_secret = self + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol)?; shared_secret.verify(&salt_enc, &salt_auth)?; - encrypt_hmac_secret_output(rng, shared_secret.as_ref(), &salt_enc[..], cred_random) + + let decrypted_salts = shared_secret.decrypt(&salt_enc)?; + if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); + if decrypted_salts.len() == 64 { + let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); + output.append(&mut output2); + } + shared_secret.encrypt(rng, &output) } /// Check if the required command's token permission is granted. @@ -481,9 +534,17 @@ impl ClientPin { pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> ClientPin { + use crypto::rng256::ThreadRng256; + let mut rng = ThreadRng256 {}; + let (key_agreement_key_v1, key_agreement_key_v2) = match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(&mut rng)), + PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(&mut rng), key_agreement_key), + }; ClientPin { - pin_protocol_v1: PinProtocol::new_test(key_agreement_key, pin_uv_auth_token), + pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), + pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, @@ -493,7 +554,6 @@ impl ClientPin { #[cfg(test)] mod test { - use super::super::pin_protocol::SharedSecretV1; use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; @@ -507,62 +567,100 @@ mod test { persistent_store.set_pin(&pin_hash, 4).unwrap(); } - /// Encrypts the message with a zero IV and key derived from shared_secret. - fn encrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { - let mut rng = ThreadRng256 {}; - let shared_secret = SharedSecretV1::new_test(*shared_secret); - shared_secret.encrypt(&mut rng, message).unwrap() - } - - /// Decrypts the message with a zero IV and key derived from shared_secret. - fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { - let shared_secret = SharedSecretV1::new_test(*shared_secret); - shared_secret.decrypt(message).unwrap() - } - /// Fails on PINs bigger than 64 bytes. - fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { + fn encrypt_pin(shared_secret: &dyn SharedSecret, pin: Vec) -> Vec { assert!(pin.len() <= 64); + let mut rng = ThreadRng256 {}; let mut padded_pin = [0u8; 64]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); - encrypt_message(shared_secret, &padded_pin) + shared_secret.encrypt(&mut rng, &padded_pin).unwrap() } - /// Encrypts the dummy PIN "1234". - fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { - encrypt_pin(shared_secret, b"1234".to_vec()) + /// Generates a ClientPin instance and a shared secret for testing. + /// + /// The shared secret for the desired PIN protocol is generated in a + /// handshake with itself. The other protocol has a random private key, so + /// tests using the wrong combination of PIN protocol and shared secret + /// should fail. + fn create_client_pin_and_shared_secret( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> (ClientPin, Box) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let key_agreement = CoseKey::from(pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol) + .unwrap(); + (client_pin, shared_secret) } - /// Encrypts the PIN hash corresponding to the dummy PIN "1234". - fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { - let mut pin = [0u8; 64]; - pin[..4].copy_from_slice(b"1234"); - let pin_hash = Sha256::hash(&pin); - encrypt_message(shared_secret, &pin_hash[..16]) + /// Generates standard input parameters to the ClientPin command. + /// + /// All fields are populated for simplicity, even though most are unused. + fn create_client_pin_and_parameters( + pin_uv_auth_protocol: PinUvAuthProtocol, + sub_command: ClientPinSubCommand, + ) -> (ClientPin, AuthenticatorClientPinParameters) { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + + let pin = b"1234"; + let mut padded_pin = [0u8; 64]; + padded_pin[..pin.len()].copy_from_slice(&pin[..]); + let pin_hash = Sha256::hash(&padded_pin); + let new_pin_enc = shared_secret + .as_ref() + .encrypt(&mut rng, &padded_pin) + .unwrap(); + let pin_uv_auth_param = shared_secret.as_ref().authenticate(&new_pin_enc); + let pin_hash_enc = shared_secret + .as_ref() + .encrypt(&mut rng, &pin_hash[..16]) + .unwrap(); + let params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command, + key_agreement: Some( + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + ), + pin_uv_auth_param: Some(pin_uv_auth_param), + new_pin_enc: Some(new_pin_enc), + pin_hash_enc: Some(pin_hash_enc), + permissions: Some(0x03), + permissions_rp_id: Some("example.com".to_string()), + }; + (client_pin, params) } - #[test] - fn test_verify_pin_hash_enc() { + fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + let pin_protocol = client_pin.get_pin_protocol(pin_uv_auth_protocol); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); // The PIN is "1234". let pin_hash = [ 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0xC4, 0x12, ]; persistent_store.set_pin(&pin_hash, 4).unwrap(); - let shared_secret = SharedSecretV1::new_test([0x88; 32]); - let mut client_pin = ClientPin::new(&mut rng); - let pin_hash_enc = vec![ - 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, - 0x99, 0x66, - ]; + let pin_hash_enc = shared_secret.as_ref().encrypt(&mut rng, &pin_hash).unwrap(); assert_eq!( client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Ok(()) @@ -573,22 +671,21 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); - let pin_hash_enc = vec![ - 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, - 0x99, 0x66, - ]; + let pin_hash_enc = shared_secret.as_ref().encrypt(&mut rng, &pin_hash).unwrap(); client_pin.consecutive_pin_mismatches = 3; assert_eq!( client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) @@ -600,7 +697,8 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -611,7 +709,8 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -619,95 +718,119 @@ mod test { } #[test] - fn test_process_get_pin_retries() { + fn test_verify_pin_hash_enc_v1() { + test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V1); + } + + #[test] + fn test_verify_pin_hash_enc_v2() { + test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_retries(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinRetries, + ); let mut rng = ThreadRng256 {}; - let persistent_store = PersistentStore::new(&mut rng); - let client_pin = ClientPin::new(&mut rng); - let expected_response = Ok(AuthenticatorClientPinResponse { + let mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - client_pin.process_get_pin_retries(&persistent_store), - expected_response + client_pin.process_command(&mut rng, &mut persistent_store, params), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } #[test] - fn test_process_get_key_agreement() { + fn test_process_get_pin_retries_v1() { + test_helper_process_get_pin_retries(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_retries_v2() { + test_helper_process_get_pin_retries(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_key_agreement(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetKeyAgreement, + ); let mut rng = ThreadRng256 {}; - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let expected_response = Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), + let mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: params.key_agreement.clone(), pin_token: None, retries: None, }); - assert_eq!(client_pin.process_get_key_agreement(), expected_response); - } - - #[test] - fn test_process_set_pin() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let new_pin_enc = encrypt_standard_pin(&shared_secret); - let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); assert_eq!( - client_pin.process_set_pin(&mut persistent_store, key_agreement, pin_auth, new_pin_enc), - Ok(()) + client_pin.process_command(&mut rng, &mut persistent_store, params), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } #[test] - fn test_process_change_pin() { + fn test_process_get_key_agreement_v1() { + test_helper_process_get_key_agreement(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_key_agreement_v2() { + test_helper_process_get_key_agreement(PinUvAuthProtocol::V2); + } + + fn test_helper_process_set_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = + create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::SetPin); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params), + Ok(ResponseData::AuthenticatorClientPin(None)) + ); + } + + #[test] + fn test_process_set_pin_v1() { + test_helper_process_set_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_set_pin_v2() { + test_helper_process_set_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_process_change_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, mut params) = + create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::ChangePin); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let new_pin_enc = encrypt_standard_pin(&shared_secret); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - let mut auth_param_data = new_pin_enc.clone(); - auth_param_data.extend(&pin_hash_enc); - let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); + let mut auth_param_data = params.new_pin_enc.clone().unwrap(); + auth_param_data.extend(params.pin_hash_enc.as_ref().unwrap()); + let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); + params.pin_uv_auth_param = Some(pin_uv_auth_param); assert_eq!( - client_pin.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - pin_hash_enc.clone() - ), - Ok(()) + client_pin.process_command(&mut rng, &mut persistent_store, params.clone()), + Ok(ResponseData::AuthenticatorClientPin(None)) ); - let bad_pin_hash_enc = vec![0xEE; 16]; + let mut bad_params = params.clone(); + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - bad_pin_hash_enc - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); @@ -715,102 +838,91 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - client_pin.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - ), + client_pin.process_command(&mut rng, &mut persistent_store, params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) ); } #[test] - fn test_process_get_pin_token() { + fn test_process_change_pin_v1() { + test_helper_process_change_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_change_pin_v2() { + test_helper_process_change_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_token(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinToken, + ); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin - .process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc - ) + .process_command(&mut rng, &mut persistent_store, params.clone()) .is_ok()); - let pin_hash_enc = vec![0xEE; 16]; + let mut bad_params = params; + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] - fn test_process_get_pin_token_force_pin_change() { + fn test_process_get_pin_token_v1() { + test_helper_process_get_pin_token(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_token_v2() { + test_helper_process_get_pin_token(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_token_force_pin_change(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinToken, + ); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - assert_eq!(persistent_store.force_pin_change(), Ok(())); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc - ), + client_pin.process_command(&mut rng, &mut persistent_store, params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), ); } #[test] - fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { + fn test_process_get_pin_token_force_pin_change_v1() { + test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_token_force_pin_change_v2() { + test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin - .process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x03, - Some(String::from("example.com")), - ) + .process_command(&mut rng, &mut persistent_store, params.clone()) .is_ok()); assert_eq!(client_pin.permissions, 0x03); assert_eq!( @@ -818,159 +930,121 @@ mod test { Some(String::from("example.com")) ); + let mut bad_params = params.clone(); + bad_params.permissions = Some(0x00); assert_eq!( - client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x00, - Some(String::from("example.com")), - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); + let mut bad_params = params.clone(); + bad_params.permissions_rp_id = None; assert_eq!( - client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc, - 0x03, - None, - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); - let pin_hash_enc = vec![0xEE; 16]; + let mut bad_params = params; + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc, - 0x03, - Some(String::from("example.com")), - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] - fn test_process_get_pin_token_force_pin_change_force_pin_change() { + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v1() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v2() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( - client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc, - 0x03, - Some(String::from("example.com")), - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + client_pin.process_command(&mut rng, &mut persistent_store, params), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] - fn test_process() { + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v1() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + PinUvAuthProtocol::V1, + ); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v2() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + PinUvAuthProtocol::V2, + ); + } + + fn test_helper_decrypt_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut client_pin = ClientPin::new(&mut rng); - let client_pin_params = AuthenticatorClientPinParameters { - pin_uv_auth_protocol: 1, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - pin_auth: None, - new_pin_enc: None, - pin_hash_enc: None, - permissions: None, - permissions_rp_id: None, - }; - assert!(client_pin - .process_command(&mut rng, &mut persistent_store, client_pin_params) - .is_ok()); + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); - let client_pin_params = AuthenticatorClientPinParameters { - pin_uv_auth_protocol: 2, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - pin_auth: None, - new_pin_enc: None, - pin_hash_enc: None, - permissions: None, - permissions_rp_id: None, - }; - let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"1234".to_vec()); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, client_pin_params), - Err(error_code) - ); - } - - #[test] - fn test_decrypt_pin() { - let shared_secret = SharedSecretV1::new_test([0x88; 32]); - - // "1234" - let new_pin_enc = vec![ - 0xC0, 0xCF, 0xAE, 0x4C, 0x79, 0x56, 0x87, 0x99, 0xE5, 0x83, 0x4F, 0xE6, 0x4D, 0xFE, - 0x53, 0x32, 0x36, 0x0D, 0xF9, 0x1E, 0x47, 0x66, 0x10, 0x5C, 0x63, 0x30, 0x1D, 0xCC, - 0x00, 0x09, 0x91, 0xA4, 0x20, 0x6B, 0x78, 0x10, 0xFE, 0xC6, 0x2E, 0x7E, 0x75, 0x14, - 0xEE, 0x01, 0x99, 0x6C, 0xD7, 0xE5, 0x2B, 0xA5, 0x7A, 0x5A, 0xE1, 0xEC, 0x69, 0x31, - 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, - ]; - assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Ok(b"1234".to_vec()), ); - // "123" - let new_pin_enc = vec![ - 0xF3, 0x54, 0x29, 0x17, 0xD4, 0xF8, 0xCD, 0x23, 0x1D, 0x59, 0xED, 0xE5, 0x33, 0x42, - 0x13, 0x39, 0x22, 0xBB, 0x91, 0x28, 0x87, 0x6A, 0xF9, 0xB1, 0x80, 0x9C, 0x9D, 0x76, - 0xFF, 0xDD, 0xB8, 0xD6, 0x8D, 0x66, 0x99, 0xA2, 0x42, 0x67, 0xB0, 0x5C, 0x82, 0x3F, - 0x08, 0x55, 0x8C, 0x04, 0xC5, 0x91, 0xF0, 0xF9, 0x58, 0x44, 0x00, 0x1B, 0x99, 0xA6, - 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, - ]; + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"123".to_vec()); assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Ok(b"123".to_vec()), ); // Encrypted PIN is too short. let new_pin_enc = vec![0x44; 63]; assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); // Encrypted PIN is too long. let new_pin_enc = vec![0x44; 65]; assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); } #[test] - fn test_check_and_store_new_pin() { + fn test_decrypt_pin_v1() { + test_helper_decrypt_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_decrypt_pin_v2() { + test_helper_decrypt_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_check_and_store_new_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let shared_secret_hash = [0x88; 32]; - let shared_secret = SharedSecretV1::new_test(shared_secret_hash); + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); let test_cases = vec![ // Accept PIN "1234". @@ -993,9 +1067,10 @@ mod test { ]; for (pin, result) in test_cases { let old_pin_hash = persistent_store.pin_hash().unwrap(); - let new_pin_enc = encrypt_pin(&shared_secret_hash, pin); + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin); + assert_eq!( - check_and_store_new_pin(&mut persistent_store, &shared_secret, new_pin_enc), + check_and_store_new_pin(&mut persistent_store, shared_secret.as_ref(), new_pin_enc), result ); if result.is_ok() { @@ -1007,77 +1082,151 @@ mod test { } #[test] - fn test_encrypt_hmac_secret_output() { + fn test_check_and_store_new_pin_v1() { + test_helper_check_and_store_new_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_check_and_store_new_pin_v2() { + test_helper_check_and_store_new_pin(PinUvAuthProtocol::V2); + } + + /// Generates valid inputs for process_hmac_secret and returns the output. + fn get_process_hmac_secret_decrypted_output( + pin_uv_auth_protocol: PinUvAuthProtocol, + cred_random: &[u8; 32], + salt: Vec, + ) -> Result, Ctap2StatusCode> { let mut rng = ThreadRng256 {}; - let shared_secret_hash = [0x88; 32]; - let shared_secret = SharedSecretV1::new_test(shared_secret_hash); - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 32); + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); - let salt_enc = [0x5E; 48]; - let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); - assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); + let salt_enc = shared_secret.as_ref().encrypt(&mut rng, &salt).unwrap(); + let salt_auth = shared_secret.authenticate(&salt_enc); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let output = client_pin.process_hmac_secret(&mut rng, hmac_secret_input, cred_random); + output.map(|v| shared_secret.as_ref().decrypt(&v).unwrap()) + } - let salt_enc = [0x5E; 64]; - let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 64); - - let mut salt_enc = [0x00; 32]; + fn test_helper_process_hmac_secret_bad_salt_auth(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + let cred_random = [0xC9; 32]; + + let salt_enc = vec![0x01; 32]; + let mut salt_auth = shared_secret.authenticate(&salt_enc); + salt_auth[0] = 0x00; + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let output = client_pin.process_hmac_secret(&mut rng, hmac_secret_input, &cred_random); + assert_eq!(output, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)); + } + + #[test] + fn test_process_hmac_secret_bad_salt_auth_v1() { + test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_bad_salt_auth_v2() { + test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_one_salt(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let salt = vec![0x01; 32]; + let expected_output = hmac_256::(&cred_random, &salt); + + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt) + .unwrap(); + assert_eq!(&output, &expected_output); + } + + #[test] + fn test_process_hmac_secret_one_salt_v1() { + test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_one_salt_v2() { + test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_two_salts(pin_uv_auth_protocol: PinUvAuthProtocol) { let cred_random = [0xC9; 32]; - // Test values to check for reproducibility. let salt1 = [0x01; 32]; let salt2 = [0x02; 32]; let expected_output1 = hmac_256::(&cred_random, &salt1); let expected_output2 = hmac_256::(&cred_random, &salt2); - let salt_enc1 = encrypt_message(&shared_secret_hash, &salt1); - salt_enc.copy_from_slice(salt_enc1.as_slice()); - let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec, &expected_output1); - - let salt_enc2 = &encrypt_message(&shared_secret_hash, &salt2); - salt_enc.copy_from_slice(salt_enc2.as_slice()); - let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec, &expected_output2); - - let mut salt_enc = [0x00; 64]; - let mut salt12 = [0x00; 64]; + let mut salt12 = vec![0x00; 64]; salt12[..32].copy_from_slice(&salt1); salt12[32..].copy_from_slice(&salt2); - let salt_enc12 = encrypt_message(&shared_secret_hash, &salt12); - salt_enc.copy_from_slice(salt_enc12.as_slice()); let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec[..32], &expected_output1); - assert_eq!(&output_dec[32..], &expected_output2); + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt12) + .unwrap(); + assert_eq!(&output[..32], &expected_output1); + assert_eq!(&output[32..], &expected_output2); - let mut salt_enc = [0x00; 64]; - let mut salt02 = [0x00; 64]; + let mut salt02 = vec![0x00; 64]; salt02[32..].copy_from_slice(&salt2); - let salt_enc02 = encrypt_message(&shared_secret_hash, &salt02); - salt_enc.copy_from_slice(salt_enc02.as_slice()); let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec[32..], &expected_output2); + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt02) + .unwrap(); + assert_eq!(&output[32..], &expected_output2); - let mut salt_enc = [0x00; 64]; - let mut salt10 = [0x00; 64]; + let mut salt10 = vec![0x00; 64]; salt10[..32].copy_from_slice(&salt1); - let salt_enc10 = encrypt_message(&shared_secret_hash, &salt10); - salt_enc.copy_from_slice(salt_enc10.as_slice()); let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec[..32], &expected_output1); + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt10) + .unwrap(); + assert_eq!(&output[..32], &expected_output1); + } + + #[test] + fn test_process_hmac_secret_two_salts_v1() { + test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_two_salts_v2() { + test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_wrong_length(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let output = get_process_hmac_secret_decrypted_output( + pin_uv_auth_protocol, + &cred_random, + vec![0x5E; 48], + ); + assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); + } + + #[test] + fn test_process_hmac_secret_wrong_length_v1() { + test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_wrong_length_v2() { + test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V2); } #[test] @@ -1160,4 +1309,34 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } + + #[test] + fn test_reset() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + let public_key_v1 = client_pin.pin_protocol_v1.get_public_key(); + let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); + let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); + let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); + client_pin.permissions = 0xFF; + client_pin.permissions_rp_id = Some(String::from("example.com")); + client_pin.reset(&mut rng); + assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); + assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); + assert_ne!( + &token_v1, + client_pin.pin_protocol_v1.get_pin_uv_auth_token() + ); + assert_ne!( + &token_v2, + client_pin.pin_protocol_v2.get_pin_uv_auth_token() + ); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + } } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 8c5aaec..748c33e 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -18,8 +18,8 @@ use super::data_formats::{ extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, SetMinPinLengthParams, + PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -302,12 +302,12 @@ impl TryFrom for AuthenticatorGetAssertionParameters { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { - pub pin_uv_auth_protocol: u64, + pub pin_uv_auth_protocol: PinUvAuthProtocol, pub sub_command: ClientPinSubCommand, pub key_agreement: Option, - pub pin_auth: Option>, + pub pin_uv_auth_param: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, pub permissions: Option, @@ -323,7 +323,7 @@ impl TryFrom for AuthenticatorClientPinParameters { 0x01 => pin_uv_auth_protocol, 0x02 => sub_command, 0x03 => key_agreement, - 0x04 => pin_auth, + 0x04 => pin_uv_auth_param, 0x05 => new_pin_enc, 0x06 => pin_hash_enc, 0x09 => permissions, @@ -331,10 +331,11 @@ impl TryFrom for AuthenticatorClientPinParameters { } = extract_map(cbor_value)?; } - let pin_uv_auth_protocol = extract_unsigned(ok_or_missing(pin_uv_auth_protocol)?)?; + let pin_uv_auth_protocol = + PinUvAuthProtocol::try_from(ok_or_missing(pin_uv_auth_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?; - let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.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()?; // We expect a bit field of 8 bits, and drop everything else. @@ -349,7 +350,7 @@ impl TryFrom for AuthenticatorClientPinParameters { pin_uv_auth_protocol, sub_command, key_agreement, - pin_auth, + pin_uv_auth_param, new_pin_enc, pin_hash_enc, permissions, @@ -706,10 +707,10 @@ mod test { AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); let expected_client_pin_parameters = AuthenticatorClientPinParameters { - pin_uv_auth_protocol: 1, + pin_uv_auth_protocol: PinUvAuthProtocol::V1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), - pin_auth: Some(vec![0xBB]), + pin_uv_auth_param: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), permissions: Some(0x03), diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 2f90f11..3cbefd5 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -126,6 +126,7 @@ pub fn process_config( mod test { use super::*; use crate::ctap::customization::ENFORCE_ALWAYS_UV; + use crate::ctap::data_formats::PinUvAuthProtocol; use crypto::rng256::ThreadRng256; #[test] @@ -134,7 +135,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::EnableEnterpriseAttestation, @@ -161,7 +163,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, @@ -197,7 +200,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -257,7 +261,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); // First, increase minimum PIN length from 4 to 6 without PIN auth. let min_pin_length = 6; @@ -301,7 +306,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); // First, set RP IDs without PIN auth. let min_pin_length = 6; @@ -377,7 +383,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); // Increase min PIN, force PIN change. @@ -400,7 +407,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -431,7 +439,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::VendorPrototype, diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 5b07294..bbfb320 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -351,7 +351,7 @@ pub fn process_credential_management( #[cfg(test)] mod test { - use super::super::data_formats::PublicKeyCredentialType; + use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType}; use super::super::CtapState; use super::*; use crypto::rng256::{Rng256, ThreadRng256}; @@ -382,7 +382,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); @@ -453,7 +454,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source1 = create_credential_source(&mut rng); let mut credential_source2 = create_credential_source(&mut rng); credential_source2.rp_id = "another.example.com".to_string(); @@ -550,7 +552,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); @@ -632,7 +635,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source1 = create_credential_source(&mut rng); let mut credential_source2 = create_credential_source(&mut rng); credential_source2.user_handle = vec![0x02]; @@ -737,7 +741,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut credential_source = create_credential_source(&mut rng); credential_source.credential_id = vec![0x1D; 32]; @@ -808,7 +813,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut credential_source = create_credential_source(&mut rng); credential_source.credential_id = vec![0x1D; 32]; @@ -880,7 +886,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 97ee858..138dd3a 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -356,6 +356,7 @@ pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, pub salt_auth: Vec, + pub pin_uv_auth_protocol: PinUvAuthProtocol, } impl TryFrom for GetAssertionHmacSecretInput { @@ -367,16 +368,20 @@ impl TryFrom for GetAssertionHmacSecretInput { 1 => key_agreement, 2 => salt_enc, 3 => salt_auth, + 4 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } let key_agreement = CoseKey::try_from(ok_or_missing(key_agreement)?)?; let salt_enc = extract_byte_string(ok_or_missing(salt_enc)?)?; let salt_auth = extract_byte_string(ok_or_missing(salt_auth)?)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.map_or(Ok(PinUvAuthProtocol::V1), PinUvAuthProtocol::try_from)?; Ok(Self { key_agreement, salt_enc, salt_auth, + pin_uv_auth_protocol, }) } } @@ -638,7 +643,7 @@ impl TryFrom for PublicKeyCredentialSource { let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; - let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; + let creation_order = creation_order.map_or(Ok(0), extract_unsigned)?; let user_name = user_name.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?; let cred_blob = cred_blob.map(extract_byte_string).transpose()?; @@ -809,6 +814,24 @@ impl TryFrom for ecdh::PubKey { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PinUvAuthProtocol { + V1, + V2, +} + +impl TryFrom for PinUvAuthProtocol { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + match extract_unsigned(cbor_value)? { + 1 => Ok(PinUvAuthProtocol::V1), + 2 => Ok(PinUvAuthProtocol::V2), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } + } +} + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { @@ -1569,7 +1592,7 @@ mod test { } #[test] - fn test_from_get_assertion_extensions() { + fn test_from_get_assertion_extensions_default_protocol() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1588,6 +1611,7 @@ mod test { key_agreement: cose_key, salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), @@ -1597,6 +1621,38 @@ mod test { assert_eq!(extensions, Ok(expected_extensions)); } + #[test] + fn test_from_get_assertion_extensions_with_protocol() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cbor_extensions = cbor_map! { + "hmac-secret" => cbor_map! { + 1 => cbor::Value::from(cose_key.clone()), + 2 => vec![0x02; 32], + 3 => vec![0x03; 16], + 4 => 2, + }, + "credBlob" => true, + "largeBlobKey" => true, + }; + let extensions = GetAssertionExtensions::try_from(cbor_extensions); + let expected_input = GetAssertionHmacSecretInput { + key_agreement: cose_key, + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V2, + }; + let expected_extensions = GetAssertionExtensions { + hmac_secret: Some(expected_input), + cred_blob: true, + large_blob_key: Some(true), + }; + assert_eq!(extensions, Ok(expected_extensions)); + // TODO more tests, check default + } + #[test] fn test_from_make_credential_options() { let cbor_make_options = cbor_map! { @@ -1759,6 +1815,25 @@ mod test { assert_eq!(cose_key.algorithm, ES256_ALGORITHM); } + #[test] + fn test_from_pin_uv_auth_protocol() { + let cbor_protocol: cbor::Value = cbor_int!(0x01); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Ok(PinUvAuthProtocol::V1) + ); + let cbor_protocol: cbor::Value = cbor_int!(0x02); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Ok(PinUvAuthProtocol::V2) + ); + let cbor_protocol: cbor::Value = cbor_int!(0x03); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_from_into_client_pin_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index 05cec58..d366ce4 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -135,6 +135,7 @@ impl LargeBlobs { #[cfg(test)] mod test { + use super::super::data_formats::PinUvAuthProtocol; use super::*; use crypto::rng256::ThreadRng256; @@ -144,7 +145,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ @@ -175,7 +177,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -237,7 +240,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -283,7 +287,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -329,7 +334,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -358,7 +364,8 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6d8e2d0..8432f4e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1223,7 +1223,8 @@ mod test { use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, - MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + MakeCredentialOptions, PinUvAuthProtocol, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; use super::*; use cbor::{cbor_array, cbor_array_vec, cbor_map}; @@ -1983,6 +1984,7 @@ mod test { key_agreement: CoseKey::from(pk), salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), @@ -2040,6 +2042,7 @@ mod test { key_agreement: CoseKey::from(pk), salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), @@ -2317,7 +2320,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x88; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs index 88d608a..912f7aa 100644 --- a/src/ctap/pin_protocol.rs +++ b/src/ctap/pin_protocol.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::ctap::client_pin::PIN_TOKEN_LENGTH; -use crate::ctap::data_formats::CoseKey; +use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol}; use crate::ctap::status_code::Ctap2StatusCode; use alloc::boxed::Box; use alloc::vec; @@ -21,6 +21,8 @@ use alloc::vec::Vec; use core::convert::TryInto; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hkdf::hkdf_empty_salt_256; +#[cfg(test)] +use crypto::hmac::hmac_256; use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; @@ -64,14 +66,13 @@ impl PinProtocol { pub fn decapsulate( &self, peer_cose_key: CoseKey, - pin_uv_auth_protocol: u64, + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result, Ctap2StatusCode> { let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?; let handshake = self.key_agreement_key.exchange_x(&pk); match pin_uv_auth_protocol { - 1 => Ok(Box::new(SharedSecretV1::new(handshake))), - 2 => Ok(Box::new(SharedSecretV2::new(handshake))), - _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))), + PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))), } } @@ -98,12 +99,11 @@ pub fn verify_pin_uv_auth_token( token: &[u8; PIN_TOKEN_LENGTH], message: &[u8], signature: &[u8], - pin_uv_auth_protocol: u64, + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { match pin_uv_auth_protocol { - 1 => verify_v1(token, message, signature), - 2 => verify_v2(token, message, signature), - _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + PinUvAuthProtocol::V1 => verify_v1(token, message, signature), + PinUvAuthProtocol::V2 => verify_v2(token, message, signature), } } @@ -116,6 +116,10 @@ pub trait SharedSecret { /// Verifies that the signature is a valid MAC for the given message. fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>; + + /// Creates a signature that matches verify. + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec; } fn aes256_cbc_encrypt( @@ -210,16 +214,6 @@ impl SharedSecretV1 { aes_enc_key, } } - - /// Creates a new shared secret for testing. - #[cfg(test)] - pub fn new_test(hash: [u8; 32]) -> SharedSecretV1 { - let aes_enc_key = crypto::aes256::EncryptionKey::new(&hash); - SharedSecretV1 { - common_secret: hash, - aes_enc_key, - } - } } impl SharedSecret for SharedSecretV1 { @@ -234,6 +228,11 @@ impl SharedSecret for SharedSecretV1 { fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { verify_v1(&self.common_secret, message, signature) } + + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec { + hmac_256::(&self.common_secret, message)[..16].to_vec() + } } pub struct SharedSecretV2 { @@ -264,6 +263,11 @@ impl SharedSecret for SharedSecretV2 { fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { verify_v2(&self.hmac_key, message, signature) } + + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec { + hmac_256::(&self.hmac_key, message).to_vec() + } } #[cfg(test)] @@ -300,6 +304,14 @@ mod test { assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); } + #[test] + fn test_shared_secret_v1_authenticate_verify() { + let shared_secret = SharedSecretV1::new([0x55; 32]); + let message = [0xAA; 32]; + let signature = shared_secret.authenticate(&message); + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + } + #[test] fn test_shared_secret_v1_verify() { let shared_secret = SharedSecretV1::new([0x55; 32]); @@ -328,6 +340,14 @@ mod test { assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); } + #[test] + fn test_shared_secret_v2_authenticate_verify() { + let shared_secret = SharedSecretV2::new([0x55; 32]); + let message = [0xAA; 32]; + let signature = shared_secret.authenticate(&message); + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + } + #[test] fn test_shared_secret_v2_verify() { let shared_secret = SharedSecretV2::new([0x55; 32]); @@ -348,23 +368,12 @@ mod test { ); } - #[test] - fn test_decapsulate_invalid() { - let mut rng = ThreadRng256 {}; - let pin_protocol = PinProtocol::new(&mut rng); - let shared_secret = pin_protocol.decapsulate(pin_protocol.get_public_key(), 3); - assert_eq!( - shared_secret.err(), - Some(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - } - #[test] fn test_decapsulate_symmetric() { let mut rng = ThreadRng256 {}; let pin_protocol1 = PinProtocol::new(&mut rng); let pin_protocol2 = PinProtocol::new(&mut rng); - for protocol in 1..=2 { + for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] { let shared_secret1 = pin_protocol1 .decapsulate(pin_protocol2.get_public_key(), protocol) .unwrap(); @@ -386,19 +395,24 @@ mod test { 0x49, 0x68, ]; assert_eq!( - verify_pin_uv_auth_token(&token, &message, &signature, 1), + verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V1), Ok(()) ); assert_eq!( - verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 1), + verify_pin_uv_auth_token( + &[0x12; PIN_TOKEN_LENGTH], + &message, + &signature, + PinUvAuthProtocol::V1 + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &[0xBB], &signature, 1), + verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V1), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &message, &[0x12; 16], 1), + verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -413,31 +427,25 @@ mod test { 0x36, 0x93, 0xF7, 0x84, ]; assert_eq!( - verify_pin_uv_auth_token(&token, &message, &signature, 2), + verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V2), Ok(()) ); assert_eq!( - verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 2), + verify_pin_uv_auth_token( + &[0x12; PIN_TOKEN_LENGTH], + &message, + &signature, + PinUvAuthProtocol::V2 + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &[0xBB], &signature, 2), + verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V2), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &message, &[0x12; 32], 2), + verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } - - #[test] - fn test_verify_pin_uv_auth_token_invalid_protocol() { - let token = [0x91; PIN_TOKEN_LENGTH]; - let message = [0xAA]; - let signature = []; - assert_eq!( - verify_pin_uv_auth_token(&token, &message, &signature, 3), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - } } From aec1e0a4092007eaabfe2c7f0eb2c51625f8f5d5 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 18 Mar 2021 17:29:32 +0100 Subject: [PATCH 80/86] adds PIN protocol V2 to all commands (#295) --- src/ctap/client_pin.rs | 127 +++++++++++- src/ctap/command.rs | 50 +++-- src/ctap/config_command.rs | 71 ++++--- src/ctap/credential_management.rs | 137 ++++++------- src/ctap/data_formats.rs | 4 +- src/ctap/large_blobs.rs | 55 +++-- src/ctap/mod.rs | 324 ++++++++++++++++++++---------- src/ctap/pin_protocol.rs | 13 ++ 8 files changed, 520 insertions(+), 261 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 12e049c..a580b1b 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -90,14 +90,12 @@ fn check_and_store_new_pin( } #[cfg_attr(test, derive(IntoEnumIterator))] -// TODO remove when all variants are used -#[allow(dead_code)] pub enum PinPermission { // All variants should use integers with a single bit set. MakeCredential = 0x01, GetAssertion = 0x02, CredentialManagement = 0x04, - BioEnrollment = 0x08, + _BioEnrollment = 0x08, LargeBlobWrite = 0x10, AuthenticatorConfiguration = 0x20, } @@ -414,18 +412,19 @@ impl ClientPin { Ok(ResponseData::AuthenticatorClientPin(response)) } - /// Verifies the HMAC for the PIN protocol V1 pinUvAuthToken. - pub fn verify_pin_auth_token( + /// Verifies the HMAC for the pinUvAuthToken of the given version. + pub fn verify_pin_uv_auth_token( &self, hmac_contents: &[u8], pin_uv_auth_param: &[u8], + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { verify_pin_uv_auth_token( - self.get_pin_protocol(PinUvAuthProtocol::V1) + self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), hmac_contents, pin_uv_auth_param, - PinUvAuthProtocol::V1, + pin_uv_auth_protocol, ) } @@ -554,6 +553,7 @@ impl ClientPin { #[cfg(test)] mod test { + use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; @@ -639,6 +639,48 @@ mod test { (client_pin, params) } + #[test] + fn test_mix_pin_protocols() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let pin_protocol_v1 = client_pin.get_pin_protocol(PinUvAuthProtocol::V1); + let pin_protocol_v2 = client_pin.get_pin_protocol(PinUvAuthProtocol::V2); + let message = vec![0xAA; 16]; + + let shared_secret_v1 = pin_protocol_v1 + .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V1) + .unwrap(); + let shared_secret_v2 = pin_protocol_v2 + .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V2) + .unwrap(); + let ciphertext = shared_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = shared_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + + let fake_secret_v1 = pin_protocol_v1 + .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V1) + .unwrap(); + let ciphertext = shared_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = fake_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = fake_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + + let fake_secret_v2 = pin_protocol_v2 + .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V2) + .unwrap(); + let ciphertext = shared_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = fake_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = fake_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + } + fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -1310,6 +1352,77 @@ mod test { ); } + #[test] + fn test_verify_pin_uv_auth_token() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + let pin_uv_auth_token_v2 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V2) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v2 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V2); + let pin_uv_auth_param_v1_from_v2_token = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V1); + let pin_uv_auth_param_v2_from_v1_token = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V2); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Ok(()) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2, + PinUvAuthProtocol::V2 + ), + Ok(()) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V2 + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1_from_v2_token, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2_from_v1_token, + PinUvAuthProtocol::V2 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_reset() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 748c33e..c0ecb2d 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -155,7 +155,7 @@ pub struct AuthenticatorMakeCredentialParameters { // Same for options, use defaults when not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, pub enterprise_attestation: Option, } @@ -213,7 +213,9 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { .unwrap_or_default(); 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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { @@ -241,7 +243,7 @@ pub struct AuthenticatorGetAssertionParameters { // Same for options, use defaults when not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorGetAssertionParameters { @@ -288,7 +290,9 @@ impl TryFrom for AuthenticatorGetAssertionParameters { .unwrap_or_default(); 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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; Ok(AuthenticatorGetAssertionParameters { rp_id, @@ -366,7 +370,7 @@ pub struct AuthenticatorLargeBlobsParameters { pub offset: usize, pub length: Option, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorLargeBlobsParameters { @@ -394,7 +398,9 @@ impl TryFrom for AuthenticatorLargeBlobsParameters { .transpose()? .map(|u| u as usize); 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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; if get.is_none() && set.is_none() { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -439,7 +445,7 @@ pub struct AuthenticatorConfigParameters { pub sub_command: ConfigSubCommand, pub sub_command_params: Option, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorConfigParameters { @@ -463,7 +469,9 @@ impl TryFrom for AuthenticatorConfigParameters { _ => None, }; 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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; Ok(AuthenticatorConfigParameters { sub_command, @@ -507,8 +515,8 @@ impl TryFrom for AuthenticatorAttestationMaterial { pub struct AuthenticatorCredentialManagementParameters { pub sub_command: CredentialManagementSubCommand, pub sub_command_params: Option, - pub pin_uv_auth_protocol: Option, - pub pin_auth: Option>, + pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_param: Option>, } impl TryFrom for AuthenticatorCredentialManagementParameters { @@ -520,7 +528,7 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { 0x01 => sub_command, 0x02 => sub_command_params, 0x03 => pin_uv_auth_protocol, - 0x04 => pin_auth, + 0x04 => pin_uv_auth_param, } = extract_map(cbor_value)?; } @@ -528,14 +536,16 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { let sub_command_params = sub_command_params .map(CredentialManagementSubCommandParameters::try_from) .transpose()?; - let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; - let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; Ok(AuthenticatorCredentialManagementParameters { sub_command, sub_command_params, pin_uv_auth_protocol, - pin_auth, + pin_uv_auth_param, }) } } @@ -630,7 +640,7 @@ mod test { extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), enterprise_attestation: Some(2), }; @@ -677,7 +687,7 @@ mod test { extensions: GetAssertionExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( @@ -766,8 +776,8 @@ mod test { let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(params), - pin_uv_auth_protocol: Some(1), - pin_auth: Some(vec![0x9A; 16]), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: Some(vec![0x9A; 16]), }; assert_eq!( @@ -821,7 +831,7 @@ mod test { offset: 0, length: Some(MIN_LARGE_BLOB_LEN), pin_uv_auth_param: Some(vec![0xA9]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( returned_large_blobs_parameters, @@ -843,7 +853,7 @@ mod test { offset: 1, length: None, pin_uv_auth_param: Some(vec![0xA9]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( returned_large_blobs_parameters, diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 3cbefd5..bdedc6b 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::client_pin::ClientPin; +use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorConfigParameters; +use super::customization::ENTERPRISE_ATTESTATION_MODE; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::{check_pin_uv_auth_protocol, ENTERPRISE_ATTESTATION_MODE}; use alloc::vec; /// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig. @@ -91,10 +91,10 @@ pub fn process_config( _ => true, } && persistent_store.has_always_uv()?; if persistent_store.pin_hash()?.is_some() || enforce_uv { - // TODO(kaczmarczyck) The error code is specified inconsistently with other commands. - check_pin_uv_auth_protocol(pin_uv_auth_protocol) - .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; - let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_param = + pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; // Constants are taken from the specification, section 6.11, step 4.2. let mut config_data = vec![0xFF; 32]; config_data.extend(&[0x0D, sub_command as u8]); @@ -103,7 +103,12 @@ pub fn process_config( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - client_pin.verify_pin_auth_token(&config_data, &auth_param)?; + client_pin.verify_pin_uv_auth_token( + &config_data, + &pin_uv_auth_param, + pin_uv_auth_protocol, + )?; + client_pin.has_permission(PinPermission::AuthenticatorConfiguration)?; } match sub_command { @@ -127,6 +132,7 @@ mod test { use super::*; use crate::ctap::customization::ENFORCE_ALWAYS_UV; use crate::ctap::data_formats::PinUvAuthProtocol; + use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token; use crypto::rng256::ThreadRng256; #[test] @@ -194,25 +200,24 @@ mod test { } } - #[test] - fn test_process_toggle_always_uv_with_pin() { + fn test_helper_process_toggle_always_uv_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); - let pin_uv_auth_param = Some(vec![ - 0x99, 0xBA, 0x0A, 0x57, 0x9D, 0x95, 0x5A, 0x44, 0xE3, 0x77, 0xCF, 0x95, 0x51, 0x3F, - 0xFD, 0xBE, - ]); + let mut config_data = vec![0xFF; 32]; + config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]); + let pin_uv_auth_param = + authenticate_pin_uv_auth_token(&pin_uv_auth_token, &config_data, pin_uv_auth_protocol); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, sub_command_params: None, - pin_uv_auth_param: pin_uv_auth_param.clone(), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_param: Some(pin_uv_auth_param.clone()), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENFORCE_ALWAYS_UV { @@ -228,14 +233,24 @@ mod test { let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, sub_command_params: None, - pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(!persistent_store.has_always_uv().unwrap()); } + #[test] + fn test_process_toggle_always_uv_with_pin_v1() { + test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_toggle_always_uv_with_pin_v2() { + test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V2); + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, @@ -251,7 +266,7 @@ mod test { set_min_pin_length_params, )), pin_uv_auth_param: None, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), } } @@ -276,22 +291,22 @@ mod test { persistent_store.set_pin(&[0x88; 16], 8).unwrap(); let min_pin_length = 8; let mut config_params = create_min_pin_config_params(min_pin_length, None); - let pin_auth = vec![ + let pin_uv_auth_param = vec![ 0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29, 0xB2, 0xDE, ]; - config_params.pin_uv_auth_param = Some(pin_auth); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); // Third, decreasing the minimum PIN length from 8 to 7 fails. let mut config_params = create_min_pin_config_params(7, None); - let pin_auth = vec![ + let pin_uv_auth_param = vec![ 0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8, 0xA7, 0x71, ]; - config_params.pin_uv_auth_param = Some(pin_auth); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, @@ -329,11 +344,11 @@ mod test { persistent_store.set_pin(&[0x88; 16], 8).unwrap(); let mut config_params = create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone())); - let pin_auth = vec![ + let pin_uv_auth_param = vec![ 0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D, 0xD6, 0xDA, ]; - config_params.pin_uv_auth_param = Some(pin_auth.clone()); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone()); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); @@ -346,7 +361,7 @@ mod test { // One PIN auth shouldn't work for different lengths. let mut config_params = create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone())); - config_params.pin_uv_auth_param = Some(pin_auth.clone()); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone()); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, @@ -364,7 +379,7 @@ mod test { min_pin_length, Some(vec!["counter.example.com".to_string()]), ); - config_params.pin_uv_auth_param = Some(pin_auth); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, @@ -426,7 +441,7 @@ mod test { set_min_pin_length_params, )), pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index bbfb320..b81648a 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -22,7 +22,7 @@ use super::data_formats::{ use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::{check_pin_uv_auth_protocol, StatefulCommand, StatefulPermission}; +use super::{StatefulCommand, StatefulPermission}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -259,7 +259,7 @@ pub fn process_credential_management( sub_command, sub_command_params, pin_uv_auth_protocol, - pin_auth, + pin_uv_auth_param, } = cred_management_params; match (sub_command, stateful_command_permission.get_command()) { @@ -282,15 +282,21 @@ pub fn process_credential_management( | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { - check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; - let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_param = + pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; let mut management_data = vec![sub_command as u8]; if let Some(sub_command_params) = sub_command_params.clone() { if !cbor::write(sub_command_params.into(), &mut management_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - client_pin.verify_pin_auth_token(&management_data, &pin_auth)?; + client_pin.verify_pin_uv_auth_token( + &management_data, + &pin_uv_auth_param, + pin_uv_auth_protocol, + )?; // The RP ID permission is handled differently per subcommand below. client_pin.has_permission(PinPermission::CredentialManagement)?; } @@ -352,6 +358,7 @@ pub fn process_credential_management( #[cfg(test)] mod test { use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType}; + use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::super::CtapState; use super::*; use crypto::rng256::{Rng256, ThreadRng256}; @@ -377,13 +384,12 @@ mod test { } } - #[test] - fn test_process_get_creds_metadata() { + fn test_helper_process_get_creds_metadata(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); @@ -391,16 +397,18 @@ mod test { ctap_state.client_pin = client_pin; ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ - 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, - 0xFB, 0x00, - ]); + let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &management_data, + pin_uv_auth_protocol, + ); let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth: pin_auth.clone(), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + pin_uv_auth_param: Some(pin_uv_auth_param.clone()), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -427,8 +435,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + pin_uv_auth_param: Some(pin_uv_auth_param), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -449,6 +457,16 @@ mod test { }; } + #[test] + fn test_process_get_creds_metadata_v1() { + test_helper_process_get_creds_metadata(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_creds_metadata_v2() { + test_helper_process_get_creds_metadata(PinUvAuthProtocol::V2); + } + #[test] fn test_process_enumerate_rps_with_uv() { let mut rng = ThreadRng256 {}; @@ -474,7 +492,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, ]); @@ -482,8 +500,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -507,7 +525,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -532,7 +550,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -571,7 +589,7 @@ mod test { } ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, ]); @@ -582,8 +600,8 @@ mod test { let mut cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; for _ in 0..NUM_CREDENTIALS { @@ -613,7 +631,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; } @@ -658,7 +676,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, 0x2B, 0x68, ]); @@ -673,8 +691,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(sub_command_params), - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -697,7 +715,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -721,7 +739,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -756,7 +774,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89, 0x9C, 0x64, ]); @@ -774,8 +792,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params.clone()), - pin_uv_auth_protocol: Some(1), - pin_auth: pin_auth.clone(), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: pin_uv_auth_param.clone(), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -792,8 +810,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params), - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -828,7 +846,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD, 0x9D, 0xB7, ]); @@ -852,8 +870,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::UpdateUserInformation, sub_command_params: Some(sub_command_params), - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -882,44 +900,7 @@ mod test { } #[test] - fn test_process_credential_management_invalid_pin_uv_auth_protocol() { - let mut rng = ThreadRng256 {}; - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); - - let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.client_pin = client_pin; - - ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ - 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, - 0xFB, 0x00, - ]); - - let cred_management_params = AuthenticatorCredentialManagementParameters { - sub_command: CredentialManagementSubCommand::GetCredsMetadata, - sub_command_params: None, - pin_uv_auth_protocol: Some(123456), - pin_auth, - }; - let cred_management_response = process_credential_management( - &mut ctap_state.persistent_store, - &mut ctap_state.stateful_command_permission, - &mut ctap_state.client_pin, - cred_management_params, - DUMMY_CLOCK_VALUE, - ); - assert_eq!( - cred_management_response, - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - } - - #[test] - fn test_process_credential_management_invalid_pin_auth() { + fn test_process_credential_management_invalid_pin_uv_auth_param() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -929,8 +910,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth: Some(vec![0u8; 16]), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: Some(vec![0u8; 16]), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 138dd3a..84fe721 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -816,8 +816,8 @@ impl TryFrom for ecdh::PubKey { #[derive(Clone, Copy, Debug, PartialEq)] pub enum PinUvAuthProtocol { - V1, - V2, + V1 = 1, + V2 = 2, } impl TryFrom for PinUvAuthProtocol { diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index d366ce4..ffb98cb 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorLargeBlobsParameters; use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; @@ -91,17 +90,20 @@ impl LargeBlobs { if persistent_store.pin_hash()?.is_some() || persistent_store.has_always_uv()? { let pin_uv_auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; - // TODO(kaczmarczyck) Error codes for PIN protocol differ across commands. - // Change to Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED for None? - check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; - client_pin.has_permission(PinPermission::LargeBlobWrite)?; - let mut message = vec![0xFF; 32]; - message.extend(&[0x0C, 0x00]); + let pin_uv_auth_protocol = + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + let mut large_blob_data = vec![0xFF; 32]; + large_blob_data.extend(&[0x0C, 0x00]); let mut offset_bytes = [0u8; 4]; LittleEndian::write_u32(&mut offset_bytes, offset as u32); - message.extend(&offset_bytes); - message.extend(&Sha256::hash(set.as_slice())); - client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param)?; + large_blob_data.extend(&offset_bytes); + large_blob_data.extend(&Sha256::hash(set.as_slice())); + client_pin.verify_pin_uv_auth_token( + &large_blob_data, + &pin_uv_auth_param, + pin_uv_auth_protocol, + )?; + client_pin.has_permission(PinPermission::LargeBlobWrite)?; } if offset + set.len() > self.expected_length { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -136,6 +138,7 @@ impl LargeBlobs { #[cfg(test)] mod test { use super::super::data_formats::PinUvAuthProtocol; + use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::*; use crypto::rng256::ThreadRng256; @@ -358,14 +361,13 @@ mod test { ); } - #[test] - fn test_process_command_commit_with_pin() { + fn test_helper_process_command_commit_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -374,18 +376,23 @@ mod test { large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_uv_auth_param = Some(vec![ - 0x68, 0x0C, 0x3F, 0x6A, 0x62, 0x47, 0xE6, 0x7C, 0x23, 0x1F, 0x79, 0xE3, 0xDC, 0x6D, - 0xC3, 0xDE, - ]); + let mut large_blob_data = vec![0xFF; 32]; + // Command constant and offset bytes. + large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]); + large_blob_data.extend(&Sha256::hash(&large_blob)); + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &large_blob_data, + pin_uv_auth_protocol, + ); let large_blobs_params = AuthenticatorLargeBlobsParameters { get: None, set: Some(large_blob), offset: 0, length: Some(BLOB_LEN), - pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let large_blobs_response = large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); @@ -394,4 +401,14 @@ mod test { Ok(ResponseData::AuthenticatorLargeBlobs(None)) ); } + + #[test] + fn test_process_command_commit_with_pin_v1() { + test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_command_commit_with_pin_v2() { + test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V2); + } } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8432f4e..83db5a9 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -44,9 +44,9 @@ use self::customization::{ }; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, - GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + GetAssertionExtensions, PackedAttestationStatement, PinUvAuthProtocol, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; @@ -109,9 +109,6 @@ pub const U2F_VERSION_STRING: &str = "U2F_V2"; // TODO(#106) change to final string when ready pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; -// This is the currently supported PIN protocol version. -const PIN_PROTOCOL_VERSION: u64 = 1; - // We currently only support one algorithm for signatures: ES256. // This algorithm is requested in MakeCredential and advertized in GetInfo. pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { @@ -119,16 +116,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa alg: SignatureAlgorithm::ES256, }; -// Checks the PIN protocol parameter against all supported versions. -pub fn check_pin_uv_auth_protocol( - pin_uv_auth_protocol: Option, -) -> Result<(), Ctap2StatusCode> { - match pin_uv_auth_protocol { - Some(PIN_PROTOCOL_VERSION) => Ok(()), - _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - } -} - // This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110 // (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding. // We change the return value, since we don't need the bool. @@ -526,7 +513,7 @@ where fn pin_uv_auth_precheck( &mut self, pin_uv_auth_param: &Option>, - pin_uv_auth_protocol: Option, + pin_uv_auth_protocol: Option, cid: ChannelID, ) -> Result<(), Ctap2StatusCode> { if let Some(auth_param) = &pin_uv_auth_param { @@ -539,11 +526,9 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } } - - check_pin_uv_auth_protocol(pin_uv_auth_protocol) - } else { - Ok(()) + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; } + Ok(()) } fn process_make_credential( @@ -644,13 +629,16 @@ where // User verification depends on the PIN auth inputs, which are checked here. let ed_flag = if has_extension_output { ED_FLAG } else { 0 }; let flags = match pin_uv_auth_param { - Some(pin_auth) => { + Some(pin_uv_auth_param) => { if self.persistent_store.pin_hash()?.is_none() { // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - self.client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth)?; + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; self.client_pin .has_permission(PinPermission::MakeCredential)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; @@ -932,13 +920,16 @@ where // not support internal UV. User presence is requested as an option. let has_uv = pin_uv_auth_param.is_some(); let mut flags = match pin_uv_auth_param { - Some(pin_auth) => { + Some(pin_uv_auth_param) => { if self.persistent_store.pin_hash()?.is_none() { // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - self.client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth)?; + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; self.client_pin .has_permission(PinPermission::GetAssertion)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; @@ -1082,7 +1073,11 @@ where aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(MAX_MSG_SIZE as u64), - pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), + // The order implies preference. We favor the new V2. + pin_protocols: Some(vec![ + PinUvAuthProtocol::V2 as u64, + PinUvAuthProtocol::V1 as u64, + ]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), @@ -1220,12 +1215,14 @@ where #[cfg(test)] mod test { - use super::command::AuthenticatorAttestationMaterial; + use super::client_pin::PIN_TOKEN_LENGTH; + use super::command::{AuthenticatorAttestationMaterial, AuthenticatorClientPinParameters}; use super::data_formats::{ - CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, - MakeCredentialOptions, PinUvAuthProtocol, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; + use super::pin_protocol::{authenticate_pin_uv_auth_token, PinProtocol}; use super::*; use cbor::{cbor_array, cbor_array_vec, cbor_map}; use crypto::rng256::ThreadRng256; @@ -1316,7 +1313,7 @@ mod test { "alwaysUv" => false, }, 0x05 => MAX_MSG_SIZE as u64, - 0x06 => cbor_array_vec![vec![1]], + 0x06 => cbor_array_vec![vec![2, 1]], 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), 0x08 => CREDENTIAL_ID_SIZE as u64, 0x09 => cbor_array_vec![vec!["usb"]], @@ -1755,6 +1752,52 @@ mod test { assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); } + fn test_helper_process_make_credential_with_pin_and_uv( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.client_pin = client_pin; + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let client_data_hash = [0xCD]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &client_data_hash, + pin_uv_auth_protocol, + ); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.uv = true; + make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); + make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x45, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); + } + + #[test] + fn test_process_make_credential_with_pin_and_uv_v1() { + test_helper_process_make_credential_with_pin_and_uv(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_make_credential_with_pin_and_uv_v2() { + test_helper_process_make_credential_with_pin_and_uv(PinUvAuthProtocol::V2); + } + #[test] fn test_non_resident_process_make_credential_with_pin() { let mut rng = ThreadRng256 {}; @@ -1810,7 +1853,7 @@ mod test { ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.pin_uv_auth_param = Some(vec![0xA4; 16]); - make_credential_params.pin_uv_auth_protocol = Some(1); + make_credential_params.pin_uv_auth_protocol = Some(PinUvAuthProtocol::V1); let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert_eq!( @@ -1951,10 +1994,62 @@ mod test { check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); } - #[test] - fn test_process_get_assertion_hmac_secret() { + fn get_assertion_hmac_secret_params( + key_agreement_key: crypto::ecdh::SecKey, + key_agreement_response: ResponseData, + credential_id: Option>, + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> AuthenticatorGetAssertionParameters { let mut rng = ThreadRng256 {}; - let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let platform_public_key = key_agreement_key.genpk(); + let public_key = match key_agreement_response { + ResponseData::AuthenticatorClientPin(Some(client_pin_response)) => { + client_pin_response.key_agreement.unwrap() + } + _ => panic!("Invalid response type"), + }; + let pin_protocol = PinProtocol::new_test(key_agreement_key, [0x91; 32]); + let shared_secret = pin_protocol + .decapsulate(public_key, pin_uv_auth_protocol) + .unwrap(); + + let salt = vec![0x01; 32]; + let salt_enc = shared_secret.as_ref().encrypt(&mut rng, &salt).unwrap(); + let salt_auth = shared_secret.authenticate(&salt_enc); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(platform_public_key), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let get_extensions = GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + ..Default::default() + }; + + let credential_descriptor = credential_id.map(|key_id| PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id, + transports: None, + }); + let allow_list = credential_descriptor.map(|c| vec![c]); + AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list, + extensions: get_extensions, + options: GetAssertionOptions { + up: true, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + } + } + + fn test_helper_process_get_assertion_hmac_secret(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1979,51 +2074,50 @@ mod test { _ => panic!("Invalid response type"), }; - let pk = sk.genpk(); - let hmac_secret_input = GetAssertionHmacSecretInput { - key_agreement: CoseKey::from(pk), - salt_enc: vec![0x02; 32], - salt_auth: vec![0x03; 16], - pin_uv_auth_protocol: PinUvAuthProtocol::V1, - }; - let get_extensions = GetAssertionExtensions { - hmac_secret: Some(hmac_secret_input), - ..Default::default() - }; - - let cred_desc = PublicKeyCredentialDescriptor { - key_type: PublicKeyCredentialType::PublicKey, - key_id: credential_id, - transports: None, - }; - let get_assertion_params = AuthenticatorGetAssertionParameters { - rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], - allow_list: Some(vec![cred_desc]), - extensions: get_extensions, - options: GetAssertionOptions { - up: false, - uv: false, - }, + let client_pin_params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command: ClientPinSubCommand::GetKeyAgreement, + key_agreement: None, pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + permissions_rp_id: None, }; + let key_agreement_response = ctap_state.client_pin.process_command( + ctap_state.rng, + &mut ctap_state.persistent_store, + client_pin_params, + ); + let get_assertion_params = get_assertion_hmac_secret_params( + key_agreement_key, + key_agreement_response.unwrap(), + Some(credential_id), + pin_uv_auth_protocol, + ); let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - - assert_eq!( - get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) - ); + assert!(get_assertion_response.is_ok()); } #[test] - fn test_resident_process_get_assertion_hmac_secret() { + fn test_process_get_assertion_hmac_secret_v1() { + test_helper_process_get_assertion_hmac_secret(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_assertion_hmac_secret_v2() { + test_helper_process_get_assertion_hmac_secret(PinUvAuthProtocol::V2); + } + + fn test_helper_resident_process_get_assertion_hmac_secret( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { let mut rng = ThreadRng256 {}; - let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -2037,40 +2131,43 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); - let pk = sk.genpk(); - let hmac_secret_input = GetAssertionHmacSecretInput { - key_agreement: CoseKey::from(pk), - salt_enc: vec![0x02; 32], - salt_auth: vec![0x03; 16], - pin_uv_auth_protocol: PinUvAuthProtocol::V1, - }; - let get_extensions = GetAssertionExtensions { - hmac_secret: Some(hmac_secret_input), - ..Default::default() - }; - - let get_assertion_params = AuthenticatorGetAssertionParameters { - rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], - allow_list: None, - extensions: get_extensions, - options: GetAssertionOptions { - up: false, - uv: false, - }, + let client_pin_params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command: ClientPinSubCommand::GetKeyAgreement, + key_agreement: None, pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + permissions_rp_id: None, }; + let key_agreement_response = ctap_state.client_pin.process_command( + ctap_state.rng, + &mut ctap_state.persistent_store, + client_pin_params, + ); + let get_assertion_params = get_assertion_hmac_secret_params( + key_agreement_key, + key_agreement_response.unwrap(), + None, + pin_uv_auth_protocol, + ); let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); + assert!(get_assertion_response.is_ok()); + } - assert_eq!( - get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) - ); + #[test] + fn test_process_resident_get_assertion_hmac_secret_v1() { + test_helper_resident_process_get_assertion_hmac_secret(PinUvAuthProtocol::V1); + } + + #[test] + fn test_resident_process_get_assertion_hmac_secret_v2() { + test_helper_resident_process_get_assertion_hmac_secret(PinUvAuthProtocol::V2); } #[test] @@ -2315,13 +2412,14 @@ mod test { assert_eq!(large_blob_key, vec![0x1C; 32]); } - #[test] - fn test_process_get_next_assertion_two_credentials_with_uv() { + fn test_helper_process_get_next_assertion_two_credentials_with_uv( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x88; 32]; let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -2352,22 +2450,24 @@ mod test { // The PIN length is outside of the test scope and most likely incorrect. ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_uv_auth_param = Some(vec![ - 0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79, - 0x90, 0x8D, - ]); + let client_data_hash = vec![0xCD]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &client_data_hash, + pin_uv_auth_protocol, + ); let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash, allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: true, }, - pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, @@ -2404,6 +2504,16 @@ mod test { ); } + #[test] + fn test_process_get_next_assertion_two_credentials_with_uv_v1() { + test_helper_process_get_next_assertion_two_credentials_with_uv(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_next_assertion_two_credentials_with_uv_v2() { + test_helper_process_get_next_assertion_two_credentials_with_uv(PinUvAuthProtocol::V2); + } + #[test] fn test_process_get_next_assertion_three_credentials_no_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs index 912f7aa..44ae53d 100644 --- a/src/ctap/pin_protocol.rs +++ b/src/ctap/pin_protocol.rs @@ -94,6 +94,19 @@ impl PinProtocol { } } +/// Authenticates the pinUvAuthToken for the given PIN protocol. +#[cfg(test)] +pub fn authenticate_pin_uv_auth_token( + token: &[u8; PIN_TOKEN_LENGTH], + message: &[u8], + pin_uv_auth_protocol: PinUvAuthProtocol, +) -> Vec { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => hmac_256::(token, message)[..16].to_vec(), + PinUvAuthProtocol::V2 => hmac_256::(token, message).to_vec(), + } +} + /// Verifies the pinUvAuthToken for the given PIN protocol. pub fn verify_pin_uv_auth_token( token: &[u8; PIN_TOKEN_LENGTH], From 63232cfe60ced13c6abf6876715a02e83c540f73 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Mon, 22 Mar 2021 14:03:51 +0100 Subject: [PATCH 81/86] adds PIN token state with timeouts (#296) --- src/ctap/client_pin.rs | 384 +++++++++++++++++++++++++++++++-------- src/ctap/command.rs | 2 +- src/ctap/data_formats.rs | 6 +- src/ctap/mod.rs | 28 ++- src/ctap/token_state.rs | 277 ++++++++++++++++++++++++++++ src/main.rs | 4 +- 6 files changed, 616 insertions(+), 85 deletions(-) create mode 100644 src/ctap/token_state.rs diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index a580b1b..82e7904 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -20,6 +20,7 @@ use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::token_state::PinUvAuthTokenState; use alloc::boxed::Box; use alloc::str; use alloc::string::String; @@ -30,6 +31,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(test)] use enum_iterator::IntoEnumIterator; +use libtock_drivers::timer::ClockValue; use subtle::ConstantTimeEq; /// The prefix length of the PIN hash that is stored and compared. @@ -104,8 +106,7 @@ pub struct ClientPin { pin_protocol_v1: PinProtocol, pin_protocol_v2: PinProtocol, consecutive_pin_mismatches: u8, - permissions: u8, - permissions_rp_id: Option, + pin_uv_auth_token_state: PinUvAuthTokenState, } impl ClientPin { @@ -114,8 +115,7 @@ impl ClientPin { pin_protocol_v1: PinProtocol::new(rng), pin_protocol_v2: PinProtocol::new(rng), consecutive_pin_mismatches: 0, - permissions: 0, - permissions_rp_id: None, + pin_uv_auth_token_state: PinUvAuthTokenState::new(), } } @@ -290,6 +290,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let AuthenticatorClientPinParameters { pin_uv_auth_protocol, @@ -314,14 +315,17 @@ impl ClientPin { if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt( rng, self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), )?; - self.permissions = 0x03; - self.permissions_rp_id = None; + + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); + self.pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(now); + self.pin_uv_auth_token_state.set_default_permissions(); Ok(AuthenticatorClientPinResponse { key_agreement: None, @@ -350,6 +354,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, mut client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let permissions = ok_or_missing(client_pin_params.permissions)?; // Mutating client_pin_params is just an optimization to move it into @@ -364,10 +369,10 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let response = self.process_get_pin_token(rng, persistent_store, client_pin_params)?; - - self.permissions = permissions; - self.permissions_rp_id = permissions_rp_id; + let response = self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?; + self.pin_uv_auth_token_state.set_permissions(permissions); + self.pin_uv_auth_token_state + .set_permissions_rp_id(permissions_rp_id); Ok(response) } @@ -378,6 +383,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let response = match client_pin_params.sub_command { ClientPinSubCommand::GetPinRetries => { @@ -395,7 +401,7 @@ impl ClientPin { None } ClientPinSubCommand::GetPinToken => { - Some(self.process_get_pin_token(rng, persistent_store, client_pin_params)?) + Some(self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?) } ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, @@ -406,6 +412,7 @@ impl ClientPin { rng, persistent_store, client_pin_params, + now, )?, ), }; @@ -419,6 +426,9 @@ impl ClientPin { pin_uv_auth_param: &[u8], pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { + if !self.pin_uv_auth_token_state.is_in_use() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } verify_pin_uv_auth_token( self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), @@ -435,8 +445,7 @@ impl ClientPin { self.pin_protocol_v2.regenerate(rng); self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; - self.permissions = 0; - self.permissions_rp_id = None; + self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); } /// Verifies, computes and encrypts the HMAC-secret outputs. @@ -476,30 +485,43 @@ impl ClientPin { shared_secret.encrypt(rng, &output) } - /// Check if the required command's token permission is granted. - pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { - // Relies on the fact that all permissions are represented by powers of two. - if permission as u8 & self.permissions != 0 { + /// Consumes flags and permissions related to the pinUvAuthToken. + pub fn clear_token_flags(&mut self) { + self.pin_uv_auth_token_state.clear_user_verified_flag(); + self.pin_uv_auth_token_state + .clear_pin_uv_auth_token_permissions_except_lbw(); + } + + /// Updates the running timers, triggers timeout events. + pub fn update_timeouts(&mut self, now: ClockValue) { + self.pin_uv_auth_token_state + .pin_uv_auth_token_usage_timer_observer(now); + } + + /// Checks if user verification is cached for use of the pinUvAuthToken. + pub fn check_user_verified_flag(&mut self) -> Result<(), Ctap2StatusCode> { + if self.pin_uv_auth_token_state.get_user_verified_flag_value() { Ok(()) } else { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) } } + /// Check if the required command's token permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state.has_permission(permission) + } + /// Check if no RP ID is associated with the token permission. pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { - if self.permissions_rp_id.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - Ok(()) + self.pin_uv_auth_token_state.has_no_permissions_rp_id() } /// Check if no or the passed RP ID is associated with the token permission. pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - match &self.permissions_rp_id { - Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - _ => Ok(()), - } + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id)) } /// Check if no RP ID is associated with the token permission, or it matches the hash. @@ -507,26 +529,28 @@ impl ClientPin { &self, rp_id_hash: &[u8], ) -> Result<(), Ctap2StatusCode> { - match &self.permissions_rp_id { - Some(p) if rp_id_hash != Sha256::hash(p.as_bytes()) => { - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - } - _ => Ok(()), - } + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| { + self.pin_uv_auth_token_state + .has_permissions_rp_id_hash(rp_id_hash) + }) } /// Check if the passed RP ID is associated with the token permission. /// /// If no RP ID is associated, associate the passed RP ID as a side effect. pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - match &self.permissions_rp_id { - Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - None => { - self.permissions_rp_id = Some(String::from(rp_id)); - Ok(()) - } - _ => Ok(()), + if self + .pin_uv_auth_token_state + .has_no_permissions_rp_id() + .is_ok() + { + self.pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from(rp_id))); + return Ok(()); } + self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id) } #[cfg(test)] @@ -541,12 +565,16 @@ impl ClientPin { PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(&mut rng)), PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(&mut rng), key_agreement_key), }; + let mut pin_uv_auth_token_state = PinUvAuthTokenState::new(); + pin_uv_auth_token_state.set_permissions(0xFF); + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + pin_uv_auth_token_state.begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); ClientPin { pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), consecutive_pin_mismatches: 0, - permissions: 0xFF, - permissions_rp_id: None, + pin_uv_auth_token_state, } } } @@ -557,6 +585,10 @@ mod test { use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; + use libtock_drivers::timer::Duration; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); /// Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { @@ -782,7 +814,7 @@ mod test { retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } @@ -810,7 +842,7 @@ mod test { retries: None, }); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } @@ -831,7 +863,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(None)) ); } @@ -865,14 +897,24 @@ mod test { let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); params.pin_uv_auth_param = Some(pin_uv_auth_param); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params.clone()), + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), Ok(ResponseData::AuthenticatorClientPin(None)) ); let mut bad_params = params.clone(); bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); @@ -880,7 +922,7 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) ); } @@ -905,13 +947,41 @@ mod test { set_standard_pin(&mut persistent_store); assert!(client_pin - .process_command(&mut rng, &mut persistent_store, params.clone()) + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ) .is_ok()); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_no_permissions_rp_id(), + Ok(()) + ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -937,7 +1007,7 @@ mod test { assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), ); } @@ -964,32 +1034,65 @@ mod test { set_standard_pin(&mut persistent_store); assert!(client_pin - .process_command(&mut rng, &mut persistent_store, params.clone()) + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ) .is_ok()); - assert_eq!(client_pin.permissions, 0x03); assert_eq!( - client_pin.permissions_rp_id, - Some(String::from("example.com")) + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) ); let mut bad_params = params.clone(); bad_params.permissions = Some(0x00); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params.clone(); bad_params.permissions_rp_id = None; assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -1017,7 +1120,7 @@ mod test { assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -1275,14 +1378,21 @@ mod test { fn test_has_permission() { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); - client_pin.permissions = 0x7F; - for permission in PinPermission::into_enum_iter() { - assert_eq!(client_pin.has_permission(permission), Ok(())); - } - client_pin.permissions = 0x00; + client_pin.pin_uv_auth_token_state.set_permissions(0x7F); for permission in PinPermission::into_enum_iter() { assert_eq!( - client_pin.has_permission(permission), + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + client_pin.pin_uv_auth_token_state.set_permissions(0x00); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1293,8 +1403,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -1306,8 +1417,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin.has_no_or_rp_id_permission("another.example.com"), @@ -1324,8 +1436,9 @@ mod test { client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) @@ -1342,12 +1455,14 @@ mod test { let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - client_pin.permissions_rp_id, - Some(String::from("example.com")) + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) ); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - client_pin.ensure_rp_id_permission("counter-example.com"), + client_pin.ensure_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1355,8 +1470,11 @@ mod test { #[test] fn test_verify_pin_uv_auth_token() { let mut rng = ThreadRng256 {}; - let client_pin = ClientPin::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let message = [0xAA]; + client_pin + .pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); let pin_uv_auth_token_v1 = client_pin .get_pin_protocol(PinUvAuthProtocol::V1) @@ -1423,6 +1541,28 @@ mod test { ); } + #[test] + fn test_verify_pin_uv_auth_token_not_in_use() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_reset() { let mut rng = ThreadRng256 {}; @@ -1431,8 +1571,10 @@ mod test { let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); - client_pin.permissions = 0xFF; - client_pin.permissions_rp_id = Some(String::from("example.com")); + client_pin.pin_uv_auth_token_state.set_permissions(0xFF); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from("example.com"))); client_pin.reset(&mut rng); assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); @@ -1452,4 +1594,94 @@ mod test { } assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); } + + #[test] + fn test_update_timeouts() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) + ); + + let timeout = DUMMY_CLOCK_VALUE.wrapping_add(Duration::from_ms(30001)); + client_pin.update_timeouts(timeout); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_clear_token_flags() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!(client_pin.check_user_verified_flag(), Ok(())); + + client_pin.clear_token_flags(); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + assert_eq!( + client_pin.check_user_verified_flag(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index c0ecb2d..4616b02 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -143,7 +143,7 @@ impl Command { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 84fe721..1721e62 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -148,7 +148,7 @@ impl TryFrom for PublicKeyCredentialType { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -387,7 +387,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -484,7 +484,7 @@ impl From for cbor::Value { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 83db5a9..46d805b 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -29,6 +29,7 @@ pub mod response; pub mod status_code; mod storage; mod timed_permission; +mod token_state; use self::client_pin::{ClientPin, PinPermission}; use self::command::{ @@ -301,11 +302,12 @@ where } } - pub fn update_command_permission(&mut self, now: ClockValue) { + pub fn update_timeouts(&mut self, now: ClockValue) { // Ignore the result, just update. let _ = self .stateful_command_permission .check_command_permission(now); + self.client_pin.update_timeouts(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { @@ -465,6 +467,7 @@ where self.rng, &mut self.persistent_store, params, + now, ), Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorCredentialManagement(params) => { @@ -641,6 +644,10 @@ where )?; self.client_pin .has_permission(PinPermission::MakeCredential)?; + self.client_pin.check_user_verified_flag()?; + // Checking for the correct permissions_rp_id is specified earlier. + // Error codes are identical though, so the implementation can be identical with + // GetAssertion. self.client_pin.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } @@ -660,6 +667,7 @@ where }; (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -932,6 +940,10 @@ where )?; self.client_pin .has_permission(PinPermission::GetAssertion)?; + // Checking for the UV flag is specified earlier for GetAssertion. + // Error codes are identical though, so the implementation can be identical with + // MakeCredential. + self.client_pin.check_user_verified_flag()?; self.client_pin.ensure_rp_id_permission(&rp_id)?; UV_FLAG } @@ -987,6 +999,7 @@ where // For CTAP 2.1, it was moved to a later protocol step. if options.up { (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); } let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; @@ -1777,7 +1790,7 @@ mod test { make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); let make_credential_response = - ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + ctap_state.process_make_credential(make_credential_params.clone(), DUMMY_CHANNEL_ID); check_make_response( make_credential_response, @@ -1786,6 +1799,13 @@ mod test { 0x20, &[], ); + + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ) } #[test] @@ -2088,6 +2108,7 @@ mod test { ctap_state.rng, &mut ctap_state.persistent_store, client_pin_params, + DUMMY_CLOCK_VALUE, ); let get_assertion_params = get_assertion_hmac_secret_params( key_agreement_key, @@ -2145,6 +2166,7 @@ mod test { ctap_state.rng, &mut ctap_state.persistent_store, client_pin_params, + DUMMY_CLOCK_VALUE, ); let get_assertion_params = get_assertion_hmac_secret_params( key_agreement_key, @@ -2423,7 +2445,6 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.client_pin = client_pin; let mut make_credential_params = create_minimal_make_credential_parameters(); let user1 = PublicKeyCredentialUserEntity { @@ -2448,6 +2469,7 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); + ctap_state.client_pin = client_pin; // The PIN length is outside of the test scope and most likely incorrect. ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let client_data_hash = vec![0xCD]; diff --git a/src/ctap/token_state.rs b/src/ctap/token_state.rs new file mode 100644 index 0000000..fea0f3c --- /dev/null +++ b/src/ctap/token_state.rs @@ -0,0 +1,277 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::ctap::client_pin::PinPermission; +use crate::ctap::status_code::Ctap2StatusCode; +use crate::ctap::timed_permission::TimedPermission; +use alloc::string::String; +use crypto::sha256::Sha256; +use crypto::Hash256; +use libtock_drivers::timer::{ClockValue, Duration}; + +/// Timeout for auth tokens. +/// +/// This usage time limit is correct for USB, BLE, and internal. +/// NFC only allows 19.8 seconds. +/// TODO(#15) multiplex over transports, add NFC +const INITIAL_USAGE_TIME_LIMIT: Duration = Duration::from_ms(30000); + +/// Implements pinUvAuthToken state from section 6.5.2.1. +/// +/// The userPresent flag is omitted as the only way to set it to true is +/// built-in user verification. Therefore, we never cache user presence. +/// +/// This implementation does not use a rolling timer. +pub struct PinUvAuthTokenState { + // Relies on the fact that all permissions are represented by powers of two. + permissions_set: u8, + permissions_rp_id: Option, + usage_timer: TimedPermission, + user_verified: bool, + in_use: bool, +} + +impl PinUvAuthTokenState { + /// Creates a pinUvAuthToken state without permissions. + pub fn new() -> PinUvAuthTokenState { + PinUvAuthTokenState { + permissions_set: 0, + permissions_rp_id: None, + usage_timer: TimedPermission::waiting(), + user_verified: false, + in_use: false, + } + } + + /// Returns whether the pinUvAuthToken is active. + pub fn is_in_use(&self) -> bool { + self.in_use + } + + /// Checks if the permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + if permission as u8 & self.permissions_set != 0 { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + } + + /// Checks if there is no associated permissions RPID. + pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> { + if self.permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + Ok(()) + } + + /// Checks if the permissions RPID is associated. + pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id == p => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } + } + + /// Checks if the permissions RPID's association matches the hash. + pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } + } + + /// Sets the permissions, represented as bits in a byte. + pub fn set_permissions(&mut self, permissions: u8) { + self.permissions_set = permissions; + } + + /// Sets the permissions RPID. + pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option) { + self.permissions_rp_id = permissions_rp_id; + } + + /// Sets the default permissions. + /// + /// Allows MakeCredential and GetAssertion, without specifying a RP ID. + pub fn set_default_permissions(&mut self) { + self.set_permissions(0x03); + self.set_permissions_rp_id(None); + } + + /// Starts the timer for pinUvAuthToken usage. + pub fn begin_using_pin_uv_auth_token(&mut self, now: ClockValue) { + self.user_verified = true; + self.usage_timer = TimedPermission::granted(now, INITIAL_USAGE_TIME_LIMIT); + self.in_use = true; + } + + /// Updates the usage timer, and disables the pinUvAuthToken on timeout. + pub fn pin_uv_auth_token_usage_timer_observer(&mut self, now: ClockValue) { + if !self.in_use { + return; + } + self.usage_timer = self.usage_timer.check_expiration(now); + if !self.usage_timer.is_granted(now) { + self.stop_using_pin_uv_auth_token(); + } + } + + /// Returns whether the user is verified. + pub fn get_user_verified_flag_value(&self) -> bool { + self.in_use && self.user_verified + } + + /// Consumes the user verification. + pub fn clear_user_verified_flag(&mut self) { + self.user_verified = false; + } + + /// Clears all permissions except Large Blob Write. + pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) { + self.permissions_set &= PinPermission::LargeBlobWrite as u8; + } + + /// Resets to the initial state. + pub fn stop_using_pin_uv_auth_token(&mut self) { + self.permissions_rp_id = None; + self.permissions_set = 0; + self.usage_timer = TimedPermission::waiting(); + self.user_verified = false; + self.in_use = false; + } +} + +#[cfg(test)] +mod test { + use super::*; + use enum_iterator::IntoEnumIterator; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const START_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + const SMALL_DURATION: Duration = Duration::from_ms(100); + + #[test] + fn test_observer() { + let mut token_state = PinUvAuthTokenState::new(); + let mut now = START_CLOCK_VALUE; + token_state.begin_using_pin_uv_auth_token(now); + assert!(token_state.is_in_use()); + now = now.wrapping_add(SMALL_DURATION); + token_state.pin_uv_auth_token_usage_timer_observer(now); + assert!(token_state.is_in_use()); + now = now.wrapping_add(INITIAL_USAGE_TIME_LIMIT); + token_state.pin_uv_auth_token_usage_timer_observer(now); + assert!(!token_state.is_in_use()); + } + + #[test] + fn test_stop() { + let mut token_state = PinUvAuthTokenState::new(); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.is_in_use()); + token_state.stop_using_pin_uv_auth_token(); + assert!(!token_state.is_in_use()); + } + + #[test] + fn test_permissions() { + let mut token_state = PinUvAuthTokenState::new(); + token_state.set_permissions(0xFF); + for permission in PinPermission::into_enum_iter() { + assert_eq!(token_state.has_permission(permission), Ok(())); + } + token_state.clear_pin_uv_auth_token_permissions_except_lbw(); + assert_eq!( + token_state.has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + token_state.stop_using_pin_uv_auth_token(); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + token_state.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + } + + #[test] + fn test_permissions_rp_id_none() { + let mut token_state = PinUvAuthTokenState::new(); + let example_hash = Sha256::hash(b"example.com"); + token_state.set_permissions_rp_id(None); + assert_eq!(token_state.has_no_permissions_rp_id(), Ok(())); + assert_eq!( + token_state.has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_permissions_rp_id_some() { + let mut token_state = PinUvAuthTokenState::new(); + let example_hash = Sha256::hash(b"example.com"); + token_state.set_permissions_rp_id(Some(String::from("example.com"))); + + assert_eq!( + token_state.has_no_permissions_rp_id(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(())); + assert_eq!( + token_state.has_permissions_rp_id("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Ok(()) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&[0x1D; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + + token_state.stop_using_pin_uv_auth_token(); + assert_eq!( + token_state.has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_user_verified_flag() { + let mut token_state = PinUvAuthTokenState::new(); + assert!(!token_state.get_user_verified_flag_value()); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.get_user_verified_flag_value()); + token_state.clear_user_verified_flag(); + assert!(!token_state.get_user_verified_flag_value()); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.get_user_verified_flag_value()); + token_state.stop_using_pin_uv_auth_token(); + assert!(!token_state.get_user_verified_flag_value()); + } +} diff --git a/src/main.rs b/src/main.rs index 2ca12a8..202a1ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,8 +120,8 @@ fn main() { } // These calls are making sure that even for long inactivity, wrapping clock values - // never randomly wink or grant user presence for U2F. - ctap_state.update_command_permission(now); + // don't cause problems with timers. + ctap_state.update_timeouts(now); ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now); if has_packet { From c596f785fff92d7b96a472b1e09ae230afd6518c Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Tue, 23 Mar 2021 12:07:15 +0100 Subject: [PATCH 82/86] Output parameters for CTAP2.1 (#297) * finalizes output parameters for CTAP2.1 * explanation for internal UV --- src/ctap/client_pin.rs | 24 +++++++++++++++++++++- src/ctap/mod.rs | 1 + src/ctap/response.rs | 45 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 82e7904..96c3f02 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -200,6 +200,7 @@ impl ClientPin { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries()? as u64), + power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), }) } @@ -215,6 +216,7 @@ impl ClientPin { key_agreement, pin_token: None, retries: None, + power_cycle_state: None, }) } @@ -331,6 +333,7 @@ impl ClientPin { key_agreement: None, pin_token: Some(pin_token), retries: None, + power_cycle_state: None, }) } @@ -812,6 +815,24 @@ mod test { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), + power_cycle_state: Some(false), + }); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + + client_pin.consecutive_pin_mismatches = 3; + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(persistent_store.pin_retries().unwrap() as u64), + power_cycle_state: Some(true), }); assert_eq!( client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), @@ -840,6 +861,7 @@ mod test { key_agreement: params.key_agreement.clone(), pin_token: None, retries: None, + power_cycle_state: None, }); assert_eq!( client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), @@ -1266,7 +1288,7 @@ mod test { let salt_enc = vec![0x01; 32]; let mut salt_auth = shared_secret.authenticate(&salt_enc); - salt_auth[0] = 0x00; + salt_auth[0] ^= 0x01; let hmac_secret_input = GetAssertionHmacSecretInput { key_agreement: client_pin .get_pin_protocol(pin_uv_auth_protocol) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 46d805b..d47a248 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1101,6 +1101,7 @@ where firmware_version: None, max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), max_rp_ids_for_set_min_pin_length: Some(MAX_RP_IDS_LENGTH as u64), + certifications: None, remaining_discoverable_credentials: Some( self.persistent_store.remaining_credentials()? as u64, ), diff --git a/src/ctap/response.rs b/src/ctap/response.rs index b6b3d25..ddc186b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -20,7 +20,7 @@ use super::data_formats::{ use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use cbor::{cbor_array_vec, cbor_bool, cbor_map_btree, cbor_map_options, cbor_text}; +use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_btree, cbor_map_options, cbor_text}; #[derive(Debug, PartialEq)] pub enum ResponseData { @@ -92,6 +92,7 @@ pub struct AuthenticatorGetAssertionResponse { pub signature: Vec, pub user: Option, pub number_of_credentials: Option, + // 0x06: userSelected missing as we don't support displays. pub large_blob_key: Option>, } @@ -135,7 +136,14 @@ pub struct AuthenticatorGetInfoResponse { pub firmware_version: Option, pub max_cred_blob_length: Option, pub max_rp_ids_for_set_min_pin_length: Option, + // Missing response fields as they are only relevant for internal UV: + // - 0x11: preferredPlatformUvAttempts + // - 0x12: uvModality + // Add them when your hardware supports any kind of user verification within + // the boundary of the device, e.g. fingerprint or built-in keyboard. + pub certifications: Option>, pub remaining_discoverable_credentials: Option, + // - 0x15: vendorPrototypeConfigCommands missing as we don't support it. } impl From for cbor::Value { @@ -157,15 +165,24 @@ impl From for cbor::Value { firmware_version, max_cred_blob_length, max_rp_ids_for_set_min_pin_length, + certifications, remaining_discoverable_credentials, } = get_info_response; let options_cbor: Option = options.map(|options| { - let option_map: BTreeMap<_, _> = options + let options_map: BTreeMap<_, _> = options .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) .collect(); - cbor_map_btree!(option_map) + cbor_map_btree!(options_map) + }); + + let certifications_cbor: Option = certifications.map(|certifications| { + let certifications_map: BTreeMap<_, _> = certifications + .into_iter() + .map(|(key, value)| (cbor_text!(key), cbor_int!(value))) + .collect(); + cbor_map_btree!(certifications_map) }); cbor_map_options! { @@ -185,6 +202,7 @@ impl From for cbor::Value { 0x0E => firmware_version, 0x0F => max_cred_blob_length, 0x10 => max_rp_ids_for_set_min_pin_length, + 0x13 => certifications_cbor, 0x14 => remaining_discoverable_credentials, } } @@ -195,6 +213,8 @@ pub struct AuthenticatorClientPinResponse { pub key_agreement: Option, pub pin_token: Option>, pub retries: Option, + pub power_cycle_state: Option, + // - 0x05: uvRetries missing as we don't support internal UV. } impl From for cbor::Value { @@ -203,12 +223,14 @@ impl From for cbor::Value { key_agreement, pin_token, retries, + power_cycle_state, } = client_pin_response; cbor_map_options! { 0x01 => key_agreement.map(cbor::Value::from), 0x02 => pin_token, 0x03 => retries, + 0x04 => power_cycle_state, } } } @@ -401,6 +423,7 @@ mod test { firmware_version: None, max_cred_blob_length: None, max_rp_ids_for_set_min_pin_length: None, + certifications: None, remaining_discoverable_credentials: None, }; let response_cbor: Option = @@ -417,6 +440,8 @@ mod test { fn test_get_info_optionals_into_cbor() { let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); + let mut certifications_map = BTreeMap::new(); + certifications_map.insert(String::from("example-cert"), 1); let get_info_response = AuthenticatorGetInfoResponse { versions: vec!["FIDO_2_0".to_string()], extensions: Some(vec!["extension".to_string()]), @@ -434,6 +459,7 @@ mod test { firmware_version: Some(0), max_cred_blob_length: Some(1024), max_rp_ids_for_set_min_pin_length: Some(8), + certifications: Some(certifications_map), remaining_discoverable_credentials: Some(150), }; let response_cbor: Option = @@ -455,6 +481,7 @@ mod test { 0x0E => 0, 0x0F => 1024, 0x10 => 8, + 0x13 => cbor_map! {"example-cert" => 1}, 0x14 => 150, }; assert_eq!(response_cbor, Some(expected_cbor)); @@ -462,15 +489,23 @@ mod test { #[test] fn test_used_client_pin_into_cbor() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); let client_pin_response = AuthenticatorClientPinResponse { - key_agreement: None, + key_agreement: Some(cose_key.clone()), pin_token: Some(vec![70]), - retries: None, + retries: Some(8), + power_cycle_state: Some(false), }; let response_cbor: Option = ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into(); let expected_cbor = cbor_map_options! { + 0x01 => cbor::Value::from(cose_key), 0x02 => vec![70], + 0x03 => 8, + 0x04 => false, }; assert_eq!(response_cbor, Some(expected_cbor)); } From e7797a5683f4818cc3a90f3db353887911e337b6 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:41:20 +0200 Subject: [PATCH 83/86] Separate file crypto wrappers, starting with AES-CBC (#298) * refactor key wrapping with tests * remove backwards compatiblity tests * adds AES-CBC tests for IV and RNG --- src/ctap/crypto_wrapper.rs | 147 +++++++++++++++++++++++++++++++++++++ src/ctap/mod.rs | 43 +++-------- src/ctap/pin_protocol.rs | 58 +-------------- 3 files changed, 157 insertions(+), 91 deletions(-) create mode 100644 src/ctap/crypto_wrapper.rs diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs new file mode 100644 index 0000000..2587e76 --- /dev/null +++ b/src/ctap/crypto_wrapper.rs @@ -0,0 +1,147 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::ctap::status_code::Ctap2StatusCode; +use alloc::vec; +use alloc::vec::Vec; +use crypto::cbc::{cbc_decrypt, cbc_encrypt}; +use crypto::rng256::Rng256; + +/// Wraps the AES256-CBC encryption to match what we need in CTAP. +pub fn aes256_cbc_encrypt( + rng: &mut dyn Rng256, + aes_enc_key: &crypto::aes256::EncryptionKey, + plaintext: &[u8], + embeds_iv: bool, +) -> Result, Ctap2StatusCode> { + if plaintext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let iv = if embeds_iv { + let random_bytes = rng.gen_uniform_u8x32(); + *array_ref!(random_bytes, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(plaintext.len() / 16); + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + for block in plaintext.chunks_exact(16) { + blocks.push(*array_ref!(block, 0, 16)); + } + cbc_encrypt(aes_enc_key, iv, &mut blocks); + let mut ciphertext = if embeds_iv { iv.to_vec() } else { vec![] }; + ciphertext.extend(blocks.iter().flatten()); + Ok(ciphertext) +} + +/// Wraps the AES256-CBC decryption to match what we need in CTAP. +pub fn aes256_cbc_decrypt( + aes_enc_key: &crypto::aes256::EncryptionKey, + ciphertext: &[u8], + embeds_iv: bool, +) -> Result, Ctap2StatusCode> { + if ciphertext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut block_len = ciphertext.len() / 16; + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + let mut block_iter = ciphertext.chunks_exact(16); + let iv = if embeds_iv { + block_len -= 1; + let iv_block = block_iter + .next() + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + *array_ref!(iv_block, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(block_len); + for block in block_iter { + blocks.push(*array_ref!(block, 0, 16)); + } + let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + Ok(blocks.iter().flatten().cloned().collect::>()) +} + +#[cfg(test)] +mod test { + use super::*; + use crypto::rng256::ThreadRng256; + + #[test] + fn test_encrypt_decrypt_with_iv() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_encrypt_decrypt_without_iv() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, false).unwrap(); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_correct_iv_usage() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let mut ciphertext_no_iv = + aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, false).unwrap(); + let mut ciphertext_with_iv = vec![0u8; 16]; + ciphertext_with_iv.append(&mut ciphertext_no_iv); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_iv_manipulation_property() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let mut ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + let mut expected_plaintext = plaintext; + for i in 0..16 { + ciphertext[i] ^= 0xBB; + expected_plaintext[i] ^= 0xBB; + } + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); + assert_eq!(decrypted, expected_plaintext); + } + + #[test] + fn test_chaining() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext1 = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + let ciphertext2 = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + assert_eq!(ciphertext1.len(), 80); + assert_eq!(ciphertext2.len(), 80); + // The ciphertext should mutate in all blocks with a different IV. + let block_iter1 = ciphertext1.chunks_exact(16); + let block_iter2 = ciphertext2.chunks_exact(16); + for (block1, block2) in block_iter1.zip(block_iter2) { + assert_ne!(block1, block2); + } + } +} diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d47a248..5d07789 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -17,6 +17,7 @@ mod client_pin; pub mod command; mod config_command; mod credential_management; +mod crypto_wrapper; #[cfg(feature = "with_ctap1")] mod ctap1; mod customization; @@ -38,6 +39,7 @@ use self::command::{ }; use self::config_command::process_config; use self::credential_management::process_credential_management; +use self::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use self::customization::{ DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, @@ -71,7 +73,6 @@ use cbor::cbor_map_options; use core::convert::TryFrom; #[cfg(feature = "debug_ctap")] use core::fmt::Write; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hmac::{hmac_256, verify_hmac_256}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; @@ -338,23 +339,11 @@ where ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let mut sk_bytes = [0; 32]; - private_key.to_bytes(&mut sk_bytes); - let mut iv = [0; 16]; - iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); + let mut plaintext = [0; 64]; + private_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); + plaintext[32..64].copy_from_slice(application); - let mut blocks = [[0u8; 16]; 4]; - blocks[0].copy_from_slice(&sk_bytes[..16]); - blocks[1].copy_from_slice(&sk_bytes[16..]); - blocks[2].copy_from_slice(&application[..16]); - blocks[3].copy_from_slice(&application[16..]); - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - - let mut encrypted_id = Vec::with_capacity(0x70); - encrypted_id.extend(&iv); - for b in &blocks { - encrypted_id.extend(b); - } + let mut encrypted_id = aes256_cbc_encrypt(self.rng, &aes_enc_key, &plaintext, true)?; let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); encrypted_id.extend(&id_hmac); Ok(encrypted_id) @@ -381,26 +370,12 @@ where return Ok(None); } let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - let mut iv = [0; 16]; - iv.copy_from_slice(&credential_id[..16]); - let mut blocks = [[0u8; 16]; 4]; - for i in 0..4 { - blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); - } - cbc_decrypt(&aes_dec_key, iv, &mut blocks); - let mut decrypted_sk = [0; 32]; - let mut decrypted_rp_id_hash = [0; 32]; - decrypted_sk[..16].clone_from_slice(&blocks[0]); - decrypted_sk[16..].clone_from_slice(&blocks[1]); - decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); - decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); - if rp_id_hash != decrypted_rp_id_hash { + let decrypted_id = aes256_cbc_decrypt(&aes_enc_key, &credential_id[..payload_size], true)?; + if rp_id_hash != &decrypted_id[32..64] { return Ok(None); } - - let sk_option = crypto::ecdsa::SecKey::from_bytes(&decrypted_sk); + let sk_option = crypto::ecdsa::SecKey::from_bytes(array_ref!(decrypted_id, 0, 32)); Ok(sk_option.map(|sk| PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs index 44ae53d..d1e8f2b 100644 --- a/src/ctap/pin_protocol.rs +++ b/src/ctap/pin_protocol.rs @@ -13,13 +13,12 @@ // limitations under the License. use crate::ctap::client_pin::PIN_TOKEN_LENGTH; +use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol}; use crate::ctap::status_code::Ctap2StatusCode; use alloc::boxed::Box; -use alloc::vec; use alloc::vec::Vec; use core::convert::TryInto; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hkdf::hkdf_empty_salt_256; #[cfg(test)] use crypto::hmac::hmac_256; @@ -135,61 +134,6 @@ pub trait SharedSecret { fn authenticate(&self, message: &[u8]) -> Vec; } -fn aes256_cbc_encrypt( - rng: &mut dyn Rng256, - aes_enc_key: &crypto::aes256::EncryptionKey, - plaintext: &[u8], - has_iv: bool, -) -> Result, Ctap2StatusCode> { - if plaintext.len() % 16 != 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let iv = if has_iv { - let random_bytes = rng.gen_uniform_u8x32(); - *array_ref!(random_bytes, 0, 16) - } else { - [0u8; 16] - }; - let mut blocks = Vec::with_capacity(plaintext.len() / 16); - // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. - for block in plaintext.chunks_exact(16) { - blocks.push(*array_ref!(block, 0, 16)); - } - cbc_encrypt(aes_enc_key, iv, &mut blocks); - let mut ciphertext = if has_iv { iv.to_vec() } else { vec![] }; - ciphertext.extend(blocks.iter().flatten()); - Ok(ciphertext) -} - -fn aes256_cbc_decrypt( - aes_enc_key: &crypto::aes256::EncryptionKey, - ciphertext: &[u8], - has_iv: bool, -) -> Result, Ctap2StatusCode> { - if ciphertext.len() % 16 != 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let mut block_len = ciphertext.len() / 16; - // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. - let mut block_iter = ciphertext.chunks_exact(16); - let iv = if has_iv { - block_len -= 1; - let iv_block = block_iter - .next() - .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; - *array_ref!(iv_block, 0, 16) - } else { - [0u8; 16] - }; - let mut blocks = Vec::with_capacity(block_len); - for block in block_iter { - blocks.push(*array_ref!(block, 0, 16)); - } - let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); - cbc_decrypt(&aes_dec_key, iv, &mut blocks); - Ok(blocks.iter().flatten().cloned().collect::>()) -} - fn verify_v1(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { if signature.len() != 16 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); From 6216a3214d88de381407e49afa3c6a90ef2139bf Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 7 Apr 2021 09:07:46 +0200 Subject: [PATCH 84/86] final changes to CTAP2.1 algorithm (#299) --- src/ctap/client_pin.rs | 91 +++++++++++++++++++------ src/ctap/data_formats.rs | 7 +- src/ctap/mod.rs | 143 ++++++++++++++++++++------------------- src/ctap/response.rs | 8 +-- 4 files changed, 154 insertions(+), 95 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 96c3f02..8b6588e 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -198,7 +198,7 @@ impl ClientPin { ) -> Result { Ok(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: None, + pin_uv_auth_token: None, retries: Some(persistent_store.pin_retries()? as u64), power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), }) @@ -214,7 +214,7 @@ impl ClientPin { ); Ok(AuthenticatorClientPinResponse { key_agreement, - pin_token: None, + pin_uv_auth_token: None, retries: None, power_cycle_state: None, }) @@ -298,10 +298,15 @@ impl ClientPin { pin_uv_auth_protocol, key_agreement, pin_hash_enc, + permissions, + permissions_rp_id, .. } = client_pin_params; let key_agreement = ok_or_missing(key_agreement)?; let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if permissions.is_some() || permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); @@ -317,21 +322,21 @@ impl ClientPin { if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt( - rng, - self.get_pin_protocol(pin_uv_auth_protocol) - .get_pin_uv_auth_token(), - )?; self.pin_protocol_v1.reset_pin_uv_auth_token(rng); self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.pin_uv_auth_token_state .begin_using_pin_uv_auth_token(now); self.pin_uv_auth_token_state.set_default_permissions(); + let pin_uv_auth_token = shared_secret.encrypt( + rng, + self.get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token(), + )?; Ok(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: Some(pin_token), + pin_uv_auth_token: Some(pin_uv_auth_token), retries: None, power_cycle_state: None, }) @@ -359,9 +364,10 @@ impl ClientPin { mut client_pin_params: AuthenticatorClientPinParameters, now: ClockValue, ) -> Result { - let permissions = ok_or_missing(client_pin_params.permissions)?; // Mutating client_pin_params is just an optimization to move it into // process_get_pin_token, without cloning permissions_rp_id here. + // getPinToken requires permissions* to be None. + let permissions = ok_or_missing(client_pin_params.permissions.take())?; let permissions_rp_id = client_pin_params.permissions_rp_id.take(); if permissions == 0 { @@ -657,6 +663,13 @@ mod test { .as_ref() .encrypt(&mut rng, &pin_hash[..16]) .unwrap(); + let (permissions, permissions_rp_id) = match sub_command { + ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions + | ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { + (Some(0x03), Some("example.com".to_string())) + } + _ => (None, None), + }; let params = AuthenticatorClientPinParameters { pin_uv_auth_protocol, sub_command, @@ -668,8 +681,8 @@ mod test { pin_uv_auth_param: Some(pin_uv_auth_param), new_pin_enc: Some(new_pin_enc), pin_hash_enc: Some(pin_hash_enc), - permissions: Some(0x03), - permissions_rp_id: Some("example.com".to_string()), + permissions, + permissions_rp_id, }; (client_pin, params) } @@ -813,7 +826,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: None, + pin_uv_auth_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), power_cycle_state: Some(false), }); @@ -830,7 +843,7 @@ mod test { client_pin.consecutive_pin_mismatches = 3; let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: None, + pin_uv_auth_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), power_cycle_state: Some(true), }); @@ -859,7 +872,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: params.key_agreement.clone(), - pin_token: None, + pin_uv_auth_token: None, retries: None, power_cycle_state: None, }); @@ -964,18 +977,37 @@ mod test { pin_uv_auth_protocol, ClientPinSubCommand::GetPinToken, ); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - assert!(client_pin + let response = client_pin .process_command( &mut rng, &mut persistent_store, params.clone(), - DUMMY_CLOCK_VALUE + DUMMY_CLOCK_VALUE, ) - .is_ok()); + .unwrap(); + let encrypted_token = match response { + ResponseData::AuthenticatorClientPin(Some(response)) => { + response.pin_uv_auth_token.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!( + &shared_secret.decrypt(&encrypted_token).unwrap(), + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token() + ); assert_eq!( client_pin .pin_uv_auth_token_state @@ -1051,18 +1083,37 @@ mod test { pin_uv_auth_protocol, ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, ); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - assert!(client_pin + let response = client_pin .process_command( &mut rng, &mut persistent_store, params.clone(), - DUMMY_CLOCK_VALUE + DUMMY_CLOCK_VALUE, ) - .is_ok()); + .unwrap(); + let encrypted_token = match response { + ResponseData::AuthenticatorClientPin(Some(response)) => { + response.pin_uv_auth_token.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!( + &shared_secret.decrypt(&encrypted_token).unwrap(), + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token() + ); assert_eq!( client_pin .pin_uv_auth_token_state diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 1721e62..b135a11 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -409,8 +409,11 @@ impl TryFrom for MakeCredentialOptions { Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if up.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + // In CTAP2.0, the up option is supposed to always fail when present. + if let Some(options_entry) = up { + if !extract_bool(options_entry)? { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } } let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 5d07789..1ecac53 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -554,6 +554,68 @@ where false }; + // MakeCredential always requires user presence. + // User verification depends on the PIN auth inputs, which are checked here. + // The ED flag is added later, if applicable. + let has_uv = pin_uv_auth_param.is_some(); + let mut flags = match pin_uv_auth_param { + Some(pin_uv_auth_param) => { + // This case is not mentioned in CTAP2.1, so we keep 2.0 logic. + if self.persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + self.client_pin + .has_permission(PinPermission::MakeCredential)?; + self.client_pin.check_user_verified_flag()?; + // Checking for the correct permissions_rp_id is specified earlier. + // Error codes are identical though, so the implementation can be identical with + // GetAssertion. + self.client_pin.ensure_rp_id_permission(&rp_id)?; + UV_FLAG + } + None => { + if options.uv { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + if self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } + // Corresponds to makeCredUvNotRqd set to true. + if options.rk && self.persistent_store.pin_hash()?.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } + 0x00 + } + }; + flags |= UP_FLAG | AT_FLAG; + + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + if let Some(exclude_list) = exclude_list { + for cred_desc in exclude_list { + if self + .persistent_store + .find_credential(&rp_id, &cred_desc.key_id, !has_uv)? + .is_some() + || self + .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? + .is_some() + { + // Perform this check, so bad actors can't brute force exclude_list + // without user interaction. + let _ = (self.check_user_presence)(cid); + return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); + } + } + } + + (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); + let mut cred_protect_policy = extensions.cred_protect; if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) @@ -576,74 +638,17 @@ where None }; let has_extension_output = extensions.hmac_secret - || cred_protect_policy.is_some() + || extensions.cred_protect.is_some() || min_pin_length || has_cred_blob_output; + if has_extension_output { + flags |= ED_FLAG + }; let large_blob_key = match (options.rk, extensions.large_blob_key) { (true, Some(true)) => Some(self.rng.gen_uniform_u8x32().to_vec()), _ => None, }; - let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - if let Some(exclude_list) = exclude_list { - for cred_desc in exclude_list { - if self - .persistent_store - .find_credential(&rp_id, &cred_desc.key_id, pin_uv_auth_param.is_none())? - .is_some() - || self - .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? - .is_some() - { - // Perform this check, so bad actors can't brute force exclude_list - // without user interaction. - (self.check_user_presence)(cid)?; - return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); - } - } - } - - // MakeCredential always requires user presence. - // User verification depends on the PIN auth inputs, which are checked here. - let ed_flag = if has_extension_output { ED_FLAG } else { 0 }; - let flags = match pin_uv_auth_param { - Some(pin_uv_auth_param) => { - if self.persistent_store.pin_hash()?.is_none() { - // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } - self.client_pin.verify_pin_uv_auth_token( - &client_data_hash, - &pin_uv_auth_param, - pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - self.client_pin - .has_permission(PinPermission::MakeCredential)?; - self.client_pin.check_user_verified_flag()?; - // Checking for the correct permissions_rp_id is specified earlier. - // Error codes are identical though, so the implementation can be identical with - // GetAssertion. - self.client_pin.ensure_rp_id_permission(&rp_id)?; - UP_FLAG | UV_FLAG | AT_FLAG | ed_flag - } - None => { - if self.persistent_store.has_always_uv()? { - return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); - } - // Corresponds to makeCredUvNotRqd set to true. - if options.rk && self.persistent_store.pin_hash()?.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); - } - if options.uv { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); - } - UP_FLAG | AT_FLAG | ed_flag - } - }; - - (self.check_user_presence)(cid)?; - self.client_pin.clear_token_flags(); - let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -699,9 +704,10 @@ where } else { None }; + let cred_protect_output = extensions.cred_protect.and(cred_protect_policy); let extensions_output = cbor_map_options! { "hmac-secret" => hmac_secret_output, - "credProtect" => cred_protect_policy, + "credProtect" => cred_protect_output, "minPinLength" => min_pin_length_output, "credBlob" => cred_blob_output, }; @@ -904,8 +910,8 @@ where let has_uv = pin_uv_auth_param.is_some(); let mut flags = match pin_uv_auth_param { Some(pin_uv_auth_param) => { + // This case is not mentioned in CTAP2.1, so we keep 2.0 logic. if self.persistent_store.pin_hash()?.is_none() { - // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } self.client_pin.verify_pin_uv_auth_token( @@ -923,12 +929,12 @@ where UV_FLAG } None => { - if self.persistent_store.has_always_uv()? { - return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); - } if options.uv { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } + if options.up && self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } 0x00 } }; @@ -970,15 +976,14 @@ where (credential, stored_credentials) }; + let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. - // For CTAP 2.1, it was moved to a later protocol step. if options.up { (self.check_user_presence)(cid)?; self.client_pin.clear_token_flags(); } - let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; - self.increment_global_signature_counter()?; let assertion_input = AssertionInput { diff --git a/src/ctap/response.rs b/src/ctap/response.rs index ddc186b..6a47172 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -211,7 +211,7 @@ impl From for cbor::Value { #[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinResponse { pub key_agreement: Option, - pub pin_token: Option>, + pub pin_uv_auth_token: Option>, pub retries: Option, pub power_cycle_state: Option, // - 0x05: uvRetries missing as we don't support internal UV. @@ -221,14 +221,14 @@ impl From for cbor::Value { fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self { let AuthenticatorClientPinResponse { key_agreement, - pin_token, + pin_uv_auth_token, retries, power_cycle_state, } = client_pin_response; cbor_map_options! { 0x01 => key_agreement.map(cbor::Value::from), - 0x02 => pin_token, + 0x02 => pin_uv_auth_token, 0x03 => retries, 0x04 => power_cycle_state, } @@ -495,7 +495,7 @@ mod test { let cose_key = CoseKey::from(pk); let client_pin_response = AuthenticatorClientPinResponse { key_agreement: Some(cose_key.clone()), - pin_token: Some(vec![70]), + pin_uv_auth_token: Some(vec![70]), retries: Some(8), power_cycle_state: Some(false), }; From 054e303d112a63c61b7c0b1041e34fe9e8a9cca6 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Fri, 9 Apr 2021 07:40:11 +0200 Subject: [PATCH 85/86] move MAX_MSG_SIZE to customization and use it in HID (#302) --- src/ctap/customization.rs | 11 +++++++++++ src/ctap/hid/mod.rs | 3 +++ src/ctap/hid/receive.rs | 22 +++++++++++++--------- src/ctap/large_blobs.rs | 5 +---- src/ctap/mod.rs | 4 ++-- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/ctap/customization.rs b/src/ctap/customization.rs index 1aebadf..7c28a2f 100644 --- a/src/ctap/customization.rs +++ b/src/ctap/customization.rs @@ -119,6 +119,15 @@ pub const ENTERPRISE_ATTESTATION_MODE: Option = None; /// VendorFacilitated. pub const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; +/// Maximum message size send for CTAP commands. +/// +/// The maximum value is 7609, as HID packets can not encode longer messages. +/// 1024 is the default mentioned in the authenticatorLargeBlobs commands. +/// Larger values are preferred, as that allows more parameters in commands. +/// If long commands are too unreliable on your hardware, consider decreasing +/// this value. +pub const MAX_MSG_SIZE: usize = 7609; + /// Sets the number of consecutive failed PINs before blocking interaction. /// /// # Invariant @@ -256,6 +265,8 @@ mod test { } else { assert!(ENTERPRISE_RP_ID_LIST.is_empty()); } + assert!(MAX_MSG_SIZE >= 1024); + assert!(MAX_MSG_SIZE <= 7609); assert!(MAX_PIN_RETRIES <= 8); assert!(MAX_CRED_BLOB_LENGTH >= 32); if let Some(count) = MAX_CREDENTIAL_COUNT_IN_LIST { diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 03cb637..c6fc418 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -322,6 +322,9 @@ impl CtapHid { receive::Error::UnexpectedSeq => { CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ) } + receive::Error::UnexpectedLen => { + CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN) + } receive::Error::Timeout => { CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT) } diff --git a/src/ctap/hid/receive.rs b/src/ctap/hid/receive.rs index 8efdb1c..caf9ffc 100644 --- a/src/ctap/hid/receive.rs +++ b/src/ctap/hid/receive.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::super::customization::MAX_MSG_SIZE; use super::{ChannelID, CtapHid, HidPacket, Message, ProcessedPacket}; use alloc::vec::Vec; use core::mem::swap; @@ -45,6 +46,8 @@ pub enum Error { UnexpectedContinuation, // Expected a continuation packet with a specific sequence number, got another sequence number. UnexpectedSeq, + // The length of a message is too big. + UnexpectedLen, // This packet arrived after a timeout. Timeout, } @@ -107,7 +110,7 @@ impl MessageAssembler { // Expecting an initialization packet. match processed_packet { ProcessedPacket::InitPacket { cmd, len, data } => { - Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp)) + self.parse_init_packet(*cid, cmd, len, data, timestamp) } ProcessedPacket::ContinuationPacket { .. } => { // CTAP specification (version 20190130) section 8.1.5.4 @@ -129,7 +132,7 @@ impl MessageAssembler { ProcessedPacket::InitPacket { cmd, len, data } => { self.reset(); if cmd == CtapHid::COMMAND_INIT { - Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp)) + self.parse_init_packet(*cid, cmd, len, data, timestamp) } else { Err((*cid, Error::UnexpectedInit)) } @@ -151,24 +154,25 @@ impl MessageAssembler { } } - fn accept_init_packet( + fn parse_init_packet( &mut self, cid: ChannelID, cmd: u8, len: usize, data: &[u8], timestamp: Timestamp, - ) -> Option { - // TODO: Should invalid commands/payload lengths be rejected early, i.e. as soon as the - // initialization packet is received, or should we build a message and then catch the - // error? - // The specification (version 20190130) isn't clear on this point. + ) -> Result, (ChannelID, Error)> { + // Reject invalid lengths early to reduce the risk of running out of memory. + // TODO: also reject invalid commands early? + if len > MAX_MSG_SIZE { + return Err((cid, Error::UnexpectedLen)); + } self.cid = cid; self.last_timestamp = timestamp; self.cmd = cmd; self.seq = 0; self.remaining_payload_len = len; - self.append_payload(data) + Ok(self.append_payload(data)) } fn append_payload(&mut self, data: &[u8]) -> Option { diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index ffb98cb..846bc33 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -14,6 +14,7 @@ use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorLargeBlobsParameters; +use super::customization::MAX_MSG_SIZE; use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; @@ -23,10 +24,6 @@ use byteorder::{ByteOrder, LittleEndian}; use crypto::sha256::Sha256; use crypto::Hash256; -/// This is maximum message size supported by the authenticator. 1024 is the default. -/// Increasing this values can speed up commands with longer responses, but lead to -/// packets dropping or unexpected failures. -pub const MAX_MSG_SIZE: usize = 1024; /// The length of the truncated hash that as appended to the large blob data. const TRUNCATED_HASH_LEN: usize = 16; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1ecac53..548523a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -42,7 +42,7 @@ use self::credential_management::process_credential_management; use self::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use self::customization::{ DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, - MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, + MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_MSG_SIZE, MAX_RP_IDS_LENGTH, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, }; use self::data_formats::{ @@ -52,7 +52,7 @@ use self::data_formats::{ PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; -use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; +use self::large_blobs::LargeBlobs; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, From 78b7767682e4e3f0b36ae577882827f22db38176 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:46:28 +0200 Subject: [PATCH 86/86] CBOR maps use Vec instead of BTreeMap (#303) * CBOR uses Vec for map internally * remove BTreeMap from get_info * rename cbor_map_btree and clean up cbor_array_vec * destructure now takes Vec, not BTreeMap * adds dedup in CBOR writer * fail to write CBOR maps with duplicates * CBOR interface refinements * macro documentation for CBOR map and array --- libraries/cbor/src/macros.rs | 130 ++++++++++++++++++++++------------- libraries/cbor/src/reader.rs | 9 ++- libraries/cbor/src/values.rs | 9 ++- libraries/cbor/src/writer.rs | 97 ++++++++++++++++++++++---- src/ctap/command.rs | 6 +- src/ctap/data_formats.rs | 49 ++++++------- src/ctap/mod.rs | 70 ++++++++++--------- src/ctap/response.rs | 39 +++++------ src/ctap/storage.rs | 12 ++-- 9 files changed, 258 insertions(+), 163 deletions(-) diff --git a/libraries/cbor/src/macros.rs b/libraries/cbor/src/macros.rs index 40669d1..758925e 100644 --- a/libraries/cbor/src/macros.rs +++ b/libraries/cbor/src/macros.rs @@ -13,14 +13,14 @@ // limitations under the License. use crate::values::{KeyType, Value}; -use alloc::collections::btree_map; +use alloc::vec; 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. +/// This macro generates code to extract multiple values from a `Vec<(KeyType, Value)>` at once +/// in an optimized manner, consuming the input vector. /// -/// It takes as input a `BTreeMap` as well as a list of identifiers and keys, and generates code +/// It takes as input a `Vec` 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. /// @@ -32,16 +32,14 @@ use core::iter::Peekable; /// 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. +/// Example usage: /// /// ```rust /// # extern crate alloc; /// # use cbor::destructure_cbor_map; /// # /// # fn main() { -/// # let map = alloc::collections::BTreeMap::new(); +/// # let map = alloc::vec::Vec::new(); /// destructure_cbor_map! { /// let { /// 1 => x, @@ -50,17 +48,6 @@ use core::iter::Peekable; /// } /// # } /// ``` -/// -/// ```rust -/// # extern crate alloc; -/// # -/// # 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; ) => { @@ -100,7 +87,7 @@ macro_rules! destructure_cbor_map { /// 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>, + it: &mut Peekable>, needle: KeyType, ) -> Option { loop { @@ -145,6 +132,23 @@ macro_rules! assert_sorted_keys { }; } +/// Creates a CBOR Value of type Map with the specified key-value pairs. +/// +/// Keys and values are expressions and converted into CBOR Keys and Values. +/// The syntax for these pairs is `key_expression => value_expression,`. +/// Duplicate keys will lead to invalid CBOR, i.e. writing these values fails. +/// Keys do not have to be sorted. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_map; +/// let map = cbor_map! { +/// 0x01 => false, +/// "02" => -3, +/// }; +/// ``` #[macro_export] macro_rules! cbor_map { // trailing comma case @@ -157,15 +161,35 @@ macro_rules! cbor_map { // The import is unused if the list is empty. #[allow(unused_imports)] use $crate::values::{IntoCborKey, IntoCborValue}; - let mut _map = ::alloc::collections::BTreeMap::new(); + let mut _map = ::alloc::vec::Vec::new(); $( - _map.insert($key.into_cbor_key(), $value.into_cbor_value()); + _map.push(($key.into_cbor_key(), $value.into_cbor_value())); )* $crate::values::Value::Map(_map) } }; } +/// Creates a CBOR Value of type Map with key-value pairs where values can be Options. +/// +/// Keys and values are expressions and converted into CBOR Keys and Value Options. +/// The map entry is included iff the Value is not an Option or Option is Some. +/// The syntax for these pairs is `key_expression => value_expression,`. +/// Duplicate keys will lead to invalid CBOR, i.e. writing these values fails. +/// Keys do not have to be sorted. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_map_options; +/// let missing_value: Option = None; +/// let map = cbor_map_options! { +/// 0x01 => Some(false), +/// "02" => -3, +/// "not in map" => missing_value, +/// }; +/// ``` #[macro_export] macro_rules! cbor_map_options { // trailing comma case @@ -178,12 +202,12 @@ macro_rules! cbor_map_options { // 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 = ::alloc::vec::Vec::<(_, $crate::values::Value)>::new(); $( { let opt: Option<$crate::values::Value> = $value.into_cbor_value_option(); if let Some(val) = opt { - _map.insert($key.into_cbor_key(), val); + _map.push(($key.into_cbor_key(), val)); } } )* @@ -192,13 +216,25 @@ macro_rules! cbor_map_options { }; } +/// Creates a CBOR Value of type Map from a Vec<(KeyType, Value)>. #[macro_export] -macro_rules! cbor_map_btree { - ( $tree:expr ) => { - $crate::values::Value::Map($tree) - }; +macro_rules! cbor_map_collection { + ( $tree:expr ) => {{ + $crate::values::Value::from($tree) + }}; } +/// Creates a CBOR Value of type Array with the given elements. +/// +/// Elements are expressions and converted into CBOR Values. Elements are comma-separated. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_array; +/// let array = cbor_array![1, "2"]; +/// ``` #[macro_export] macro_rules! cbor_array { // trailing comma case @@ -216,6 +252,7 @@ macro_rules! cbor_array { }; } +/// Creates a CBOR Value of type Array from a Vec. #[macro_export] macro_rules! cbor_array_vec { ( $vec:expr ) => {{ @@ -329,7 +366,6 @@ macro_rules! cbor_key_bytes { #[cfg(test)] mod test { use super::super::values::{KeyType, SimpleValue, Value}; - use alloc::collections::BTreeMap; #[test] fn test_cbor_simple_values() { @@ -421,7 +457,7 @@ mod test { Value::KeyValue(KeyType::Unsigned(0)), Value::KeyValue(KeyType::Unsigned(1)), ]), - Value::Map(BTreeMap::new()), + Value::Map(Vec::new()), Value::Map( [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] .iter() @@ -518,7 +554,7 @@ mod test { Value::KeyValue(KeyType::Unsigned(1)), ]), ), - (KeyType::Unsigned(9), Value::Map(BTreeMap::new())), + (KeyType::Unsigned(9), Value::Map(Vec::new())), ( KeyType::Unsigned(10), Value::Map( @@ -589,7 +625,7 @@ mod test { Value::KeyValue(KeyType::Unsigned(1)), ]), ), - (KeyType::Unsigned(9), Value::Map(BTreeMap::new())), + (KeyType::Unsigned(9), Value::Map(Vec::new())), ( KeyType::Unsigned(10), Value::Map( @@ -608,30 +644,26 @@ mod test { } #[test] - fn test_cbor_map_btree_empty() { - let a = cbor_map_btree!(BTreeMap::new()); - let b = Value::Map(BTreeMap::new()); + fn test_cbor_map_collection_empty() { + let a = cbor_map_collection!(Vec::<(_, _)>::new()); + let b = Value::Map(Vec::new()); assert_eq!(a, b); } #[test] - fn test_cbor_map_btree_foo() { - let a = cbor_map_btree!( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] - .iter() - .cloned() - .collect() - ); - let b = Value::Map( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] - .iter() - .cloned() - .collect(), - ); + fn test_cbor_map_collection_foo() { + let a = cbor_map_collection!(vec![( + KeyType::Unsigned(2), + Value::KeyValue(KeyType::Unsigned(3)) + )]); + let b = Value::Map(vec![( + KeyType::Unsigned(2), + Value::KeyValue(KeyType::Unsigned(3)), + )]); assert_eq!(a, b); } - fn extract_map(cbor_value: Value) -> BTreeMap { + fn extract_map(cbor_value: Value) -> Vec<(KeyType, Value)> { match cbor_value { Value::Map(map) => map, _ => panic!("Expected CBOR map."), diff --git a/libraries/cbor/src/reader.rs b/libraries/cbor/src/reader.rs index a1061a0..0634d84 100644 --- a/libraries/cbor/src/reader.rs +++ b/libraries/cbor/src/reader.rs @@ -13,8 +13,7 @@ // limitations under the License. use super::values::{Constants, KeyType, SimpleValue, Value}; -use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_btree, cbor_text, cbor_unsigned}; -use alloc::collections::BTreeMap; +use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_collection, cbor_text, cbor_unsigned}; use alloc::str; use alloc::vec::Vec; @@ -174,7 +173,7 @@ impl<'a> Reader<'a> { size_value: u64, remaining_depth: i8, ) -> Result { - let mut value_map = BTreeMap::new(); + let mut value_map = Vec::new(); let mut last_key_option = None; for _ in 0..size_value { let key_value = self.decode_complete_data_item(remaining_depth - 1)?; @@ -185,12 +184,12 @@ impl<'a> Reader<'a> { } } last_key_option = Some(key.clone()); - value_map.insert(key, self.decode_complete_data_item(remaining_depth - 1)?); + value_map.push((key, self.decode_complete_data_item(remaining_depth - 1)?)); } else { return Err(DecoderError::IncorrectMapKeyType); } } - Ok(cbor_map_btree!(value_map)) + Ok(cbor_map_collection!(value_map)) } fn decode_to_simple_value( diff --git a/libraries/cbor/src/values.rs b/libraries/cbor/src/values.rs index b20d109..c2f7b72 100644 --- a/libraries/cbor/src/values.rs +++ b/libraries/cbor/src/values.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::cmp::Ordering; @@ -21,7 +20,7 @@ use core::cmp::Ordering; pub enum Value { KeyValue(KeyType), Array(Vec), - Map(BTreeMap), + Map(Vec<(KeyType, Value)>), // TAG is omitted Simple(SimpleValue), } @@ -183,6 +182,12 @@ where } } +impl From> for Value { + fn from(map: Vec<(KeyType, Value)>) -> Self { + Value::Map(map) + } +} + impl From for Value { fn from(b: bool) -> Self { Value::bool_value(b) diff --git a/libraries/cbor/src/writer.rs b/libraries/cbor/src/writer.rs index 592048d..386f0b5 100644 --- a/libraries/cbor/src/writer.rs +++ b/libraries/cbor/src/writer.rs @@ -56,8 +56,14 @@ impl<'a> Writer<'a> { } } } - Value::Map(map) => { - self.start_item(5, map.len() as u64); + Value::Map(mut map) => { + map.sort_by(|a, b| a.0.cmp(&b.0)); + let map_len = map.len(); + map.dedup_by(|a, b| a.0.eq(&b.0)); + if map_len != map.len() { + return false; + } + self.start_item(5, map_len as u64); for (k, v) in map { if !self.encode_cbor(Value::KeyValue(k), remaining_depth - 1) { return false; @@ -209,9 +215,16 @@ mod test { #[test] fn test_write_map() { let value_map = cbor_map! { - "aa" => "AA", - "e" => "E", - "" => ".", + 0 => "a", + 23 => "b", + 24 => "c", + std::u8::MAX as i64 => "d", + 256 => "e", + std::u16::MAX as i64 => "f", + 65536 => "g", + std::u32::MAX as i64 => "h", + 4294967296_i64 => "i", + std::i64::MAX => "j", -1 => "k", -24 => "l", -25 => "m", @@ -224,16 +237,9 @@ mod test { b"a" => 2, b"bar" => 3, b"foo" => 4, - 0 => "a", - 23 => "b", - 24 => "c", - std::u8::MAX as i64 => "d", - 256 => "e", - std::u16::MAX as i64 => "f", - 65536 => "g", - std::u32::MAX as i64 => "h", - 4294967296_i64 => "i", - std::i64::MAX => "j", + "" => ".", + "e" => "E", + "aa" => "AA", }; let expected_cbor = vec![ 0xb8, 0x19, // map of 25 pairs: @@ -288,6 +294,67 @@ mod test { assert_eq!(write_return(value_map), Some(expected_cbor)); } + #[test] + fn test_write_map_sorted() { + let sorted_map = cbor_map! { + 0 => "a", + 1 => "b", + -1 => "c", + -2 => "d", + b"a" => "e", + b"b" => "f", + "" => "g", + "c" => "h", + }; + let unsorted_map = cbor_map! { + 1 => "b", + -2 => "d", + b"b" => "f", + "c" => "h", + "" => "g", + b"a" => "e", + -1 => "c", + 0 => "a", + }; + assert_eq!(write_return(sorted_map), write_return(unsorted_map)); + } + + #[test] + fn test_write_map_duplicates() { + let duplicate0 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + 0 => "b", + }; + assert_eq!(write_return(duplicate0), None); + let duplicate1 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + -1 => "d", + }; + assert_eq!(write_return(duplicate1), None); + let duplicate2 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + b"a" => "f", + }; + assert_eq!(write_return(duplicate2), None); + let duplicate3 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + "c" => "h", + }; + assert_eq!(write_return(duplicate3), None); + } + #[test] fn test_write_map_with_array() { let value_map = cbor_map! { diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 4616b02..387a687 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -594,14 +594,14 @@ mod test { 0x01 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], 0x02 => cbor_map! { "id" => "example.com", - "name" => "Example", "icon" => "example.com/icon.png", + "name" => "Example", }, 0x03 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png", "name" => "foo", "displayName" => "bar", - "icon" => "example.com/foo/icon.png", }, 0x04 => cbor_array![ES256_CRED_PARAM], 0x05 => cbor_array![], @@ -656,8 +656,8 @@ mod test { 0x01 => "example.com", 0x02 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], 0x03 => cbor_array![ cbor_map! { - "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], } ], 0x06 => vec![0x12, 0x34], diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index b135a11..673b850 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -13,7 +13,6 @@ // limitations under the License. use super::status_code::Ctap2StatusCode; -use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; @@ -62,8 +61,8 @@ impl From for cbor::Value { fn from(entity: PublicKeyCredentialRpEntity) -> Self { cbor_map_options! { "id" => entity.rp_id, - "name" => entity.rp_name, "icon" => entity.rp_icon, + "name" => entity.rp_name, } } } @@ -108,9 +107,9 @@ impl From for cbor::Value { fn from(entity: PublicKeyCredentialUserEntity) -> Self { cbor_map_options! { "id" => entity.user_id, + "icon" => entity.user_icon, "name" => entity.user_name, "displayName" => entity.user_display_name, - "icon" => entity.user_icon, } } } @@ -174,8 +173,8 @@ impl TryFrom for PublicKeyCredentialParameter { impl From for cbor::Value { fn from(cred_param: PublicKeyCredentialParameter) -> Self { cbor_map_options! { - "type" => cred_param.cred_type, "alg" => cred_param.alg, + "type" => cred_param.cred_type, } } } @@ -262,8 +261,8 @@ impl TryFrom for PublicKeyCredentialDescriptor { impl From for cbor::Value { fn from(desc: PublicKeyCredentialDescriptor) -> Self { cbor_map_options! { - "type" => desc.key_type, "id" => desc.key_id, + "type" => desc.key_type, "transports" => desc.transports.map(|vec| cbor_array_vec!(vec)), } } @@ -1119,7 +1118,7 @@ pub(super) fn extract_array(cbor_value: cbor::Value) -> Result, pub(super) fn extract_map( cbor_value: cbor::Value, -) -> Result, Ctap2StatusCode> { +) -> Result, Ctap2StatusCode> { match cbor_value { cbor::Value::Map(map) => Ok(map), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), @@ -1142,7 +1141,6 @@ pub(super) fn ok_or_missing(value_option: Option) -> Result cbor_false!(), - "foo" => b"bar", b"bin" => -42, + "foo" => b"bar", }), - Ok([ + Ok(vec![ (cbor_unsigned!(1), cbor_false!()), - (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), (cbor_bytes_lit!(b"bin"), cbor_int!(-42)), - ] - .iter() - .cloned() - .collect::>()) + (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), + ]) ); } @@ -1419,8 +1414,8 @@ mod test { fn test_from_public_key_credential_rp_entity() { let cbor_rp_entity = cbor_map! { "id" => "example.com", - "name" => "Example", "icon" => "example.com/icon.png", + "name" => "Example", }; let rp_entity = PublicKeyCredentialRpEntity::try_from(cbor_rp_entity); let expected_rp_entity = PublicKeyCredentialRpEntity { @@ -1435,9 +1430,9 @@ mod test { fn test_from_into_public_key_credential_user_entity() { let cbor_user_entity = cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png", "name" => "foo", "displayName" => "bar", - "icon" => "example.com/foo/icon.png", }; let user_entity = PublicKeyCredentialUserEntity::try_from(cbor_user_entity.clone()); let expected_user_entity = PublicKeyCredentialUserEntity { @@ -1541,8 +1536,8 @@ mod test { #[test] fn test_from_into_public_key_credential_parameter() { let cbor_credential_parameter = cbor_map! { - "type" => "public-key", "alg" => ES256_ALGORITHM, + "type" => "public-key", }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); @@ -1558,8 +1553,8 @@ mod test { #[test] fn test_from_into_public_key_credential_descriptor() { let cbor_credential_descriptor = cbor_map! { - "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], }; let credential_descriptor = @@ -1577,11 +1572,11 @@ mod test { #[test] fn test_from_make_credential_extensions() { let cbor_extensions = cbor_map! { - "hmac-secret" => true, - "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, - "minPinLength" => true, "credBlob" => vec![0xCB], + "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, + "hmac-secret" => true, "largeBlobKey" => true, + "minPinLength" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { @@ -1601,12 +1596,12 @@ mod test { let pk = sk.genpk(); let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { + "credBlob" => true, "hmac-secret" => cbor_map! { 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, - "credBlob" => true, "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); @@ -1631,13 +1626,13 @@ mod test { let pk = sk.genpk(); let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { + "credBlob" => true, "hmac-secret" => cbor_map! { 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], 4 => 2, }, - "credBlob" => true, "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); @@ -1690,7 +1685,7 @@ mod test { let cbor_packed_attestation_statement = cbor_map! { "alg" => 1, "sig" => vec![0x55, 0x55, 0x55, 0x55], - "x5c" => cbor_array_vec![vec![certificate]], + "x5c" => cbor_array![certificate], "ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D], }; let packed_attestation_statement = PackedAttestationStatement { @@ -1878,7 +1873,7 @@ mod test { }; let cbor_params = cbor_map! { 0x01 => 6, - 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x02 => cbor_array!("example.com".to_string()), 0x03 => true, }; assert_eq!(cbor::Value::from(params.clone()), cbor_params); @@ -1897,7 +1892,7 @@ mod test { ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params); let cbor_params = cbor_map! { 0x01 => 6, - 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x02 => cbor_array!("example.com".to_string()), 0x03 => true, }; assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 548523a..eaca021 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -63,7 +63,6 @@ use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; use alloc::boxed::Box; -use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; @@ -706,10 +705,10 @@ where }; let cred_protect_output = extensions.cred_protect.and(cred_protect_policy); let extensions_output = cbor_map_options! { - "hmac-secret" => hmac_secret_output, - "credProtect" => cred_protect_output, - "minPinLength" => min_pin_length_output, "credBlob" => cred_blob_output, + "credProtect" => cred_protect_output, + "hmac-secret" => hmac_secret_output, + "minPinLength" => min_pin_length_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -805,8 +804,8 @@ where None }; let extensions_output = cbor_map_options! { - "hmac-secret" => encrypted_output, "credBlob" => cred_blob, + "hmac-secret" => encrypted_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -1033,26 +1032,29 @@ where versions.insert(0, String::from(U2F_VERSION_STRING)) } } - let mut options_map = BTreeMap::new(); - options_map.insert(String::from("rk"), true); - options_map.insert( - String::from("clientPin"), - self.persistent_store.pin_hash()?.is_some(), - ); - options_map.insert(String::from("up"), true); - options_map.insert(String::from("pinUvAuthToken"), true); - options_map.insert(String::from("largeBlobs"), true); + let mut options = vec![]; if ENTERPRISE_ATTESTATION_MODE.is_some() { - options_map.insert( + options.push(( String::from("ep"), self.persistent_store.enterprise_attestation()?, - ); + )); } - options_map.insert(String::from("authnrCfg"), true); - options_map.insert(String::from("credMgmt"), true); - options_map.insert(String::from("setMinPINLength"), true); - options_map.insert(String::from("makeCredUvNotRqd"), !has_always_uv); - options_map.insert(String::from("alwaysUv"), has_always_uv); + options.append(&mut vec![ + (String::from("rk"), true), + (String::from("up"), true), + (String::from("alwaysUv"), has_always_uv), + (String::from("credMgmt"), true), + (String::from("authnrCfg"), true), + ( + String::from("clientPin"), + self.persistent_store.pin_hash()?.is_some(), + ), + (String::from("largeBlobs"), true), + (String::from("pinUvAuthToken"), true), + (String::from("setMinPINLength"), true), + (String::from("makeCredUvNotRqd"), !has_always_uv), + ]); + Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions, @@ -1064,7 +1066,7 @@ where String::from("largeBlobKey"), ]), aaguid: self.persistent_store.aaguid()?, - options: Some(options_map), + options: Some(options), max_msg_size: Some(MAX_MSG_SIZE as u64), // The order implies preference. We favor the new V2. pin_protocols: Some(vec![ @@ -1285,33 +1287,33 @@ mod test { String::from(FIDO2_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING), ]], - 0x02 => cbor_array_vec![vec![ + 0x02 => cbor_array![ String::from("hmac-secret"), String::from("credProtect"), String::from("minPinLength"), String::from("credBlob"), String::from("largeBlobKey"), - ]], + ], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), 0x04 => cbor_map_options! { - "rk" => true, - "clientPin" => false, - "up" => true, - "pinUvAuthToken" => true, - "largeBlobs" => true, "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), - "authnrCfg" => true, + "rk" => true, + "up" => true, + "alwaysUv" => false, "credMgmt" => true, + "authnrCfg" => true, + "clientPin" => false, + "largeBlobs" => true, + "pinUvAuthToken" => true, "setMinPINLength" => true, "makeCredUvNotRqd" => true, - "alwaysUv" => false, }, 0x05 => MAX_MSG_SIZE as u64, - 0x06 => cbor_array_vec![vec![2, 1]], + 0x06 => cbor_array![2, 1], 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), 0x08 => CREDENTIAL_ID_SIZE as u64, - 0x09 => cbor_array_vec![vec!["usb"]], - 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], + 0x09 => cbor_array!["usb"], + 0x0A => cbor_array![ES256_CRED_PARAM], 0x0B => MAX_LARGE_BLOB_ARRAY_SIZE as u64, 0x0C => false, 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 6a47172..2568bae 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -17,10 +17,9 @@ use super::data_formats::{ PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; -use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_btree, cbor_map_options, cbor_text}; +use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_collection, cbor_map_options, cbor_text}; #[derive(Debug, PartialEq)] pub enum ResponseData { @@ -123,7 +122,7 @@ pub struct AuthenticatorGetInfoResponse { pub versions: Vec, pub extensions: Option>, pub aaguid: [u8; 16], - pub options: Option>, + pub options: Option>, pub max_msg_size: Option, pub pin_protocols: Option>, pub max_credential_count_in_list: Option, @@ -141,7 +140,7 @@ pub struct AuthenticatorGetInfoResponse { // - 0x12: uvModality // Add them when your hardware supports any kind of user verification within // the boundary of the device, e.g. fingerprint or built-in keyboard. - pub certifications: Option>, + pub certifications: Option>, pub remaining_discoverable_credentials: Option, // - 0x15: vendorPrototypeConfigCommands missing as we don't support it. } @@ -170,19 +169,19 @@ impl From for cbor::Value { } = get_info_response; let options_cbor: Option = options.map(|options| { - let options_map: BTreeMap<_, _> = options + let options_map: Vec<(_, _)> = options .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) .collect(); - cbor_map_btree!(options_map) + cbor_map_collection!(options_map) }); let certifications_cbor: Option = certifications.map(|certifications| { - let certifications_map: BTreeMap<_, _> = certifications + let certifications_map: Vec<(_, _)> = certifications .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_int!(value))) .collect(); - cbor_map_btree!(certifications_map) + cbor_map_collection!(certifications_map) }); cbor_map_options! { @@ -337,7 +336,7 @@ mod test { let cbor_packed_attestation_statement = cbor_map! { "alg" => 1, "sig" => vec![0x55, 0x55, 0x55, 0x55], - "x5c" => cbor_array_vec![vec![certificate]], + "x5c" => cbor_array![certificate], "ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D], }; @@ -385,17 +384,17 @@ mod test { ResponseData::AuthenticatorGetAssertion(get_assertion_response).into(); let expected_cbor = cbor_map_options! { 0x01 => cbor_map! { - "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], }, 0x02 => vec![0xAD], 0x03 => vec![0x51], 0x04 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png".to_string(), "name" => "foo".to_string(), "displayName" => "bar".to_string(), - "icon" => "example.com/foo/icon.png".to_string(), }, 0x05 => 2, 0x07 => vec![0x1B], @@ -438,15 +437,11 @@ mod test { #[test] fn test_get_info_optionals_into_cbor() { - let mut options_map = BTreeMap::new(); - options_map.insert(String::from("rk"), true); - let mut certifications_map = BTreeMap::new(); - certifications_map.insert(String::from("example-cert"), 1); let get_info_response = AuthenticatorGetInfoResponse { versions: vec!["FIDO_2_0".to_string()], extensions: Some(vec!["extension".to_string()]), aaguid: [0x00; 16], - options: Some(options_map), + options: Some(vec![(String::from("rk"), true)]), max_msg_size: Some(1024), pin_protocols: Some(vec![1]), max_credential_count_in_list: Some(20), @@ -459,22 +454,22 @@ mod test { firmware_version: Some(0), max_cred_blob_length: Some(1024), max_rp_ids_for_set_min_pin_length: Some(8), - certifications: Some(certifications_map), + certifications: Some(vec![(String::from("example-cert"), 1)]), remaining_discoverable_credentials: Some(150), }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); let expected_cbor = cbor_map_options! { - 0x01 => cbor_array_vec![vec!["FIDO_2_0"]], - 0x02 => cbor_array_vec![vec!["extension"]], + 0x01 => cbor_array!["FIDO_2_0"], + 0x02 => cbor_array!["extension"], 0x03 => vec![0x00; 16], 0x04 => cbor_map! {"rk" => true}, 0x05 => 1024, - 0x06 => cbor_array_vec![vec![1]], + 0x06 => cbor_array![1], 0x07 => 20, 0x08 => 256, - 0x09 => cbor_array_vec![vec!["usb"]], - 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], + 0x09 => cbor_array!["usb"], + 0x0A => cbor_array![ES256_CRED_PARAM], 0x0B => 1024, 0x0C => false, 0x0D => 4, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a650d4d..2ba8d77 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -1370,13 +1370,13 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - user_display_name: None, - cred_protect_policy: None, + user_display_name: Some(String::from("Display Name")), + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), creation_order: 0, - user_name: None, - user_icon: None, - cred_blob: None, - large_blob_key: None, + user_name: Some(String::from("name")), + user_icon: Some(String::from("icon")), + cred_blob: Some(vec![0xCB]), + large_blob_key: Some(vec![0x1B]), }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap();