diff --git a/tests/basic.rs b/tests/basic.rs index b276416..036a6c2 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,5 +1,6 @@ #![cfg(feature = "dispatch")] +pub mod fs; pub mod virt; pub mod webauthn; @@ -12,7 +13,8 @@ use ciborium::Value; use exhaustive::Exhaustive; use hex_literal::hex; -use virt::{Ctap2, Ctap2Error}; +use fs::list_fs; +use virt::{Ctap2, Ctap2Error, Options}; use webauthn::{ AttStmtFormat, ClientPin, CredentialManagement, CredentialManagementParams, ExtensionsInput, GetAssertion, GetAssertionOptions, GetInfo, GetNextAssertion, KeyAgreementKey, MakeCredential, @@ -51,7 +53,15 @@ fn test_ping() { #[test] fn test_get_info() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(|ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.assert_empty(); + })), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { let reply = device.exec(GetInfo).unwrap(); assert!(reply.versions.contains(&"FIDO_2_0".to_owned())); assert!(reply.versions.contains(&"FIDO_2_1".to_owned())); @@ -93,7 +103,16 @@ fn set_pin( #[test] fn test_set_pin() { let key_agreement_key = KeyAgreementKey::generate(); - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(|ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + files.assert_empty(); + })), + ..Default::default() + }; + virt::run_ctap2_with_options(options, |device| { let reply = device.exec(GetInfo).unwrap(); let options = reply.options.unwrap(); assert_eq!(options.get("clientPin"), Some(&Value::from(false))); @@ -576,7 +595,27 @@ impl Test for TestMakeCredential { // TODO: client data let client_data_hash = b""; - virt::run_ctap2(|device| { + let is_rk = self + .options + .and_then(|options| options.rk) + .unwrap_or_default(); + let is_successful = self.expected_error().is_none(); + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.try_remove_state(); + let n = files.try_remove_keys(); + assert!(n <= 2, "n: {n}, files: {files:?}"); + if is_rk && is_successful { + assert_eq!(files.try_remove_rks(), 1, "{files:?}"); + } + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let mut pin_auth = None; match &self.pin_auth { PinAuth::NoPin => {} @@ -850,7 +889,19 @@ fn run_test_get_next_assertion(device: &Ctap2) { #[test] fn test_get_next_assertion() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 4); + assert_eq!(files.try_remove_rks(), 3); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { run_test_get_next_assertion(&device); }); } @@ -858,7 +909,19 @@ fn test_get_next_assertion() { #[test] fn test_get_next_assertion_multi_rp() { let client_data_hash = b""; - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 10); + assert_eq!(files.try_remove_rks(), 9); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let pub_key_cred_params = vec![PubKeyCredParam::new("public-key", -7)]; for rp in ["test.com", "something.dev", "else.foobar"] { for user in [b"john.doe", b"jane.doe"] { diff --git a/tests/cred_mgmt.rs b/tests/cred_mgmt.rs index b86c4ac..fc46c6b 100644 --- a/tests/cred_mgmt.rs +++ b/tests/cred_mgmt.rs @@ -1,6 +1,7 @@ #![cfg(feature = "dispatch")] pub mod authenticator; +pub mod fs; pub mod virt; pub mod webauthn; @@ -9,6 +10,7 @@ use std::collections::BTreeSet; use littlefs2::path::PathBuf; use authenticator::{Authenticator, Pin}; +use fs::list_fs; use virt::{Ctap2Error, Options}; use webauthn::{CredentialData, PubKeyCredDescriptor, Rp, User}; @@ -112,7 +114,19 @@ fn generate_user(i: u8) -> User { #[test] fn test_list_credentials() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for i in 0..10 { @@ -127,7 +141,19 @@ fn test_list_credentials() { #[test] fn test_list_credentials_multi() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { @@ -144,7 +170,19 @@ fn test_list_credentials_multi() { #[test] fn test_list_credentials_delete() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 9); + assert_eq!(files.try_remove_rks(), 8); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { @@ -164,9 +202,52 @@ fn test_list_credentials_delete() { }) } +#[test] +fn test_list_credentials_delete_all() { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 1); + files.remove_empty_dir("fido/dat/rk"); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { + let authenticator = Authenticator::new(device).set_pin(b"123456"); + let mut cred_mgmt = CredMgmt::new(authenticator); + for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { + let rp = generate_rp(i); + for j in 0..n { + let user = generate_user(j); + cred_mgmt.make_credential(rp.clone(), user).unwrap(); + } + } + + for _ in 0..10 { + cred_mgmt.delete_credential_at(0).unwrap(); + } + }) +} + #[test] fn test_list_credentials_update_user() { - virt::run_ctap2(|device| { + let options = Options { + inspect_ifs: Some(Box::new(move |ifs| { + let mut files = list_fs(ifs); + files.remove_standard(); + files.remove_state(); + assert_eq!(files.try_remove_keys(), 11); + assert_eq!(files.try_remove_rks(), 10); + files.assert_empty(); + })), + ..Default::default() + }; + + virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); for (i, n) in [1, 3, 1, 3, 2].into_iter().enumerate() { @@ -256,13 +337,17 @@ fn test_max_credential_count() { fn test_filesystem_full() { let mut options = Options { max_resident_credential_count: Some(10), + inspect_ifs: Some(Box::new(|ifs| { + let blocks = ifs.available_blocks().unwrap(); + assert!(blocks < 5, "{blocks}"); + assert!(blocks > 1, "{blocks}"); + })), ..Default::default() }; for i in 0..80 { let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); options.files.push((path, vec![0; 512])); } - // TODO: inspect filesystem after run and check remaining blocks virt::run_ctap2_with_options(options, |device| { let mut authenticator = Authenticator::new(device).set_pin(b"123456"); let metadata = authenticator.credentials_metadata(); @@ -305,13 +390,17 @@ fn test_filesystem_full() { fn test_filesystem_full_update_user() { let mut options = Options { max_resident_credential_count: Some(10), + inspect_ifs: Some(Box::new(|ifs| { + let blocks = ifs.available_blocks().unwrap(); + assert!(blocks < 5, "{blocks}"); + assert!(blocks > 1, "{blocks}"); + })), ..Default::default() }; for i in 0..80 { let path = PathBuf::try_from(format!("/test/{i}").as_str()).unwrap(); options.files.push((path, vec![0; 512])); } - // TODO: inspect filesystem after run and check remaining blocks virt::run_ctap2_with_options(options, |device| { let authenticator = Authenticator::new(device).set_pin(b"123456"); let mut cred_mgmt = CredMgmt::new(authenticator); diff --git a/tests/fs/mod.rs b/tests/fs/mod.rs new file mode 100644 index 0000000..25dd201 --- /dev/null +++ b/tests/fs/mod.rs @@ -0,0 +1,97 @@ +use std::collections::BTreeMap; + +use littlefs2_core::{path, DynFilesystem, FileType, Path}; + +#[derive(Debug, PartialEq)] +pub enum Entry { + File, + EmptyDir, +} + +#[derive(Debug, Default, PartialEq)] +pub struct Entries(pub BTreeMap); + +impl Entries { + pub fn remove_standard(&mut self) { + self.remove_file("fido/sec/00"); + self.remove_file("fido/x5c/00"); + self.remove_file("trussed/dat/rng-state.bin"); + } + + pub fn remove_state(&mut self) { + self.remove_file("fido/dat/persistent-state.cbor"); + } + + pub fn try_remove_state(&mut self) { + self.0.remove("fido/dat/persistent-state.cbor"); + } + + pub fn try_remove_keys(&mut self) -> usize { + self.try_remove_dir("fido/sec") + } + + pub fn try_remove_rks(&mut self) -> usize { + let n = self.0.len(); + self.0.retain(|path, _| { + let (start, _) = path.rsplit_once('/').unwrap(); + let start = start.rsplit_once('/').map(|(start, _)| start); + start != Some("fido/dat/rk") + }); + n - self.0.len() + } + + pub fn try_remove_dir(&mut self, dir: &str) -> usize { + let n = self.0.len(); + self.0.retain(|path, _| { + let (start, _) = path.rsplit_once('/').unwrap(); + start != dir + }); + n - self.0.len() + } + + pub fn remove_file(&mut self, path: &str) { + let entry = self.0.remove(path); + assert_eq!(entry, Some(Entry::File), "{path}"); + } + + pub fn remove_empty_dir(&mut self, path: &str) { + let entry = self.0.remove(path); + assert_eq!(entry, Some(Entry::EmptyDir), "{path}"); + } + + pub fn assert_empty(&self) { + assert_eq!(self.0, Default::default()); + } +} + +pub fn list_fs(fs: &dyn DynFilesystem) -> Entries { + fn list_dir(fs: &dyn DynFilesystem, dir: &Path, files: &mut BTreeMap) -> usize { + fs.read_dir_and_then(dir, &mut |iter| { + let mut child_count = 0; + for entry in iter { + let entry = entry.unwrap(); + if entry.file_name().as_str() == "." || entry.file_name().as_str() == ".." { + continue; + } + child_count += 1; + match entry.file_type() { + FileType::File => { + files.insert(entry.path().as_str().to_owned(), Entry::File); + } + FileType::Dir => { + let n = list_dir(fs, entry.path(), files); + if n == 0 { + files.insert(entry.path().as_str().to_owned(), Entry::EmptyDir); + } + } + } + } + Ok(child_count) + }) + .unwrap() + } + + let mut entries = BTreeMap::new(); + list_dir(fs, path!(""), &mut entries); + Entries(entries) +} diff --git a/tests/virt/mod.rs b/tests/virt/mod.rs index 84439ef..f78c372 100644 --- a/tests/virt/mod.rs +++ b/tests/virt/mod.rs @@ -4,6 +4,7 @@ use std::{ borrow::Cow, cell::RefCell, fmt::{self, Debug, Formatter}, + ops::Deref as _, sync::{ atomic::{AtomicBool, Ordering}, Arc, Once, @@ -20,7 +21,7 @@ use ctaphid::{ }; use ctaphid_dispatch::{Channel, Dispatch, Requester}; use fido_authenticator::{Authenticator, Config, Conforming}; -use littlefs2::{path, path::PathBuf}; +use littlefs2::{object_safe::DynFilesystem, path, path::PathBuf}; use rand::{ distributions::{Distribution, Uniform}, RngCore as _, @@ -62,45 +63,53 @@ where let mut files = options.files; files.push((path!("fido/x5c/00").into(), ATTESTATION_CERT.into())); files.push((path!("fido/sec/00").into(), ATTESTATION_KEY.into())); - with_client(&files, |client| { - let mut authenticator = Authenticator::new( - client, - Conforming {}, - Config { - max_msg_size: 0, - skip_up_timeout: None, - max_resident_credential_count: options.max_resident_credential_count, - large_blobs: None, - nfc_transport: false, - }, - ); + with_client( + &files, + |client| { + let mut authenticator = Authenticator::new( + client, + Conforming {}, + Config { + max_msg_size: 0, + skip_up_timeout: None, + max_resident_credential_count: options.max_resident_credential_count, + large_blobs: None, + nfc_transport: false, + }, + ); - let channel = Channel::new(); - let (rq, rp) = channel.split().unwrap(); + let channel = Channel::new(); + let (rq, rp) = channel.split().unwrap(); - thread::scope(|s| { - let stop = Arc::new(AtomicBool::new(false)); - let poller_stop = stop.clone(); - let poller = s.spawn(move || { - let mut dispatch = Dispatch::new(rp); - while !poller_stop.load(Ordering::Relaxed) { - dispatch.poll(&mut [&mut authenticator]); - thread::sleep(Duration::from_millis(1)); - } - }); + thread::scope(|s| { + let stop = Arc::new(AtomicBool::new(false)); + let poller_stop = stop.clone(); + let poller = s.spawn(move || { + let mut dispatch = Dispatch::new(rp); + while !poller_stop.load(Ordering::Relaxed) { + dispatch.poll(&mut [&mut authenticator]); + thread::sleep(Duration::from_millis(1)); + } + }); - let runner = s.spawn(move || { - let device = Device::new(rq); - let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); - f(device) - }); + let runner = s.spawn(move || { + let device = Device::new(rq); + let device = ctaphid::Device::new(device, DeviceInfo).unwrap(); + f(device) + }); - let result = runner.join(); - stop.store(true, Ordering::Relaxed); - poller.join().unwrap(); - result.unwrap() - }) - }) + let result = runner.join(); + stop.store(true, Ordering::Relaxed); + poller.join().unwrap(); + result.unwrap() + }) + }, + |ifs| { + if let Some(inspect_ifs) = options.inspect_ifs { + inspect_ifs(ifs); + } + }, + ) } pub fn run_ctap2(f: F) -> T @@ -119,10 +128,13 @@ where run_ctaphid_with_options(options, |device| f(Ctap2(device))) } -#[derive(Debug, Default)] +pub type InspectFsFn = Box; + +#[derive(Default)] pub struct Options { pub files: Vec<(PathBuf, Vec)>, pub max_resident_credential_count: Option, + pub inspect_ifs: Option, } pub struct Ctap2<'a>(ctaphid::Device>); @@ -235,9 +247,10 @@ impl HidDevice for Device<'_> { } } -fn with_client(files: &[(PathBuf, Vec)], f: F) -> T +fn with_client(files: &[(PathBuf, Vec)], f: F, inspect_ifs: F2) -> T where F: FnOnce(Client) -> T, + F2: FnOnce(&dyn DynFilesystem), { virt::with_platform(Ram::default(), |mut platform| { // virt always uses the same seed -- request some random bytes to reach a somewhat random @@ -257,7 +270,7 @@ where ifs.write(path, content).unwrap(); } - platform.run_client_with_backends( + let result = platform.run_client_with_backends( "fido", Dispatcher::default(), &[ @@ -265,6 +278,10 @@ where BackendId::Core, ], f, - ) + ); + + inspect_ifs(ifs.deref()); + + result }) }