tests: Inspect filesystem after test runs

This commit is contained in:
Robin Krahl
2025-02-19 12:34:22 +01:00
parent dfcaf94096
commit 2c8efe16c2
4 changed files with 318 additions and 52 deletions
+69 -6
View File
@@ -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"] {
+95 -6
View File
@@ -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);
+97
View File
@@ -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<String, Entry>);
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<String, Entry>) -> 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)
}
+57 -40
View File
@@ -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, T>(f: F) -> T
@@ -119,10 +128,13 @@ where
run_ctaphid_with_options(options, |device| f(Ctap2(device)))
}
#[derive(Debug, Default)]
pub type InspectFsFn = Box<dyn Fn(&dyn DynFilesystem)>;
#[derive(Default)]
pub struct Options {
pub files: Vec<(PathBuf, Vec<u8>)>,
pub max_resident_credential_count: Option<u32>,
pub inspect_ifs: Option<InspectFsFn>,
}
pub struct Ctap2<'a>(ctaphid::Device<Device<'a>>);
@@ -235,9 +247,10 @@ impl HidDevice for Device<'_> {
}
}
fn with_client<F, T>(files: &[(PathBuf, Vec<u8>)], f: F) -> T
fn with_client<F, F2, T>(files: &[(PathBuf, Vec<u8>)], f: F, inspect_ifs: F2) -> T
where
F: FnOnce(Client<Ram>) -> 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
})
}