Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend credential management tests #109

Merged
merged 1 commit into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions tests/authenticator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use super::{
virt::{Ctap2, Ctap2Error},
webauthn::{
AttStmtFormat, ClientPin, CredentialData, CredentialManagement, CredentialManagementParams,
KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredParam,
PublicKey, Rp, SharedSecret, User,
KeyAgreementKey, MakeCredential, MakeCredentialOptions, PinToken, PubKeyCredDescriptor,
PubKeyCredParam, PublicKey, Rp, SharedSecret, User,
},
};

Expand Down Expand Up @@ -142,11 +142,12 @@ impl Authenticator<'_, Pin> {
rps
}

pub fn list_credentials(&mut self, rp_id: &str) -> Vec<User> {
pub fn list_credentials(&mut self, rp_id: &str) -> Vec<(User, PubKeyCredDescriptor)> {
let rp_id_hash = rp_id_hash(rp_id);
let pin_token = self.get_pin_token(0x04, Some(rp_id.to_owned()));
let params = CredentialManagementParams {
rp_id_hash: rp_id_hash.to_vec(),
rp_id_hash: Some(rp_id_hash.to_vec()),
..Default::default()
};
let mut pin_auth_param = vec![0x04];
pin_auth_param.extend_from_slice(&params.serialized());
Expand All @@ -161,17 +162,54 @@ impl Authenticator<'_, Pin> {
// TODO: check other fields
let total_credentials = reply.total_credentials.unwrap();
let mut credentials = Vec::with_capacity(total_credentials);
credentials.push(reply.user.unwrap().into());
credentials.push((reply.user.unwrap().into(), reply.credential_id.unwrap()));

for _ in 1..total_credentials {
let request = CredentialManagement::new(0x05);
let reply = self.ctap2.exec(request).unwrap();
// TODO: check other fields
credentials.push(reply.user.unwrap().into());
credentials.push((reply.user.unwrap().into(), reply.credential_id.unwrap()));
}

credentials
}

pub fn delete_credential(&mut self, id: &[u8]) {
let pin_token = self.get_pin_token(0x04, None);
let params = CredentialManagementParams {
credential_id: Some(PubKeyCredDescriptor::new("public-key", id)),
..Default::default()
};
let mut pin_auth_param = vec![0x06];
pin_auth_param.extend_from_slice(&params.serialized());
let pin_auth = pin_token.authenticate(&pin_auth_param);
let request = CredentialManagement {
subcommand: 0x06,
subcommand_params: Some(params),
pin_protocol: Some(2),
pin_auth: Some(pin_auth),
};
self.ctap2.exec(request).unwrap();
}

pub fn update_user(&mut self, id: &[u8], user: User) -> Result<(), Ctap2Error> {
let pin_token = self.get_pin_token(0x04, None);
let params = CredentialManagementParams {
credential_id: Some(PubKeyCredDescriptor::new("public-key", id)),
user: Some(user),
..Default::default()
};
let mut pin_auth_param = vec![0x07];
pin_auth_param.extend_from_slice(&params.serialized());
let pin_auth = pin_token.authenticate(&pin_auth_param);
let request = CredentialManagement {
subcommand: 0x07,
subcommand_params: Some(params),
pin_protocol: Some(2),
pin_auth: Some(pin_auth),
};
self.ctap2.exec(request).map(|_| ())
}
}

pub struct CredentialsMetadata {
Expand Down
3 changes: 2 additions & 1 deletion tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,8 @@ impl TestListCredentials {
pin_token_rp_id,
);
let params = CredentialManagementParams {
rp_id_hash: reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned(),
rp_id_hash: Some(reply.rp_id_hash.unwrap().as_bytes().unwrap().to_owned()),
..Default::default()
};
let mut pin_auth_param = vec![0x04];
pin_auth_param.extend_from_slice(&params.serialized());
Expand Down
258 changes: 231 additions & 27 deletions tests/cred_mgmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,208 @@ pub mod authenticator;
pub mod virt;
pub mod webauthn;

use std::collections::BTreeMap;
use std::collections::BTreeSet;

use littlefs2::path::PathBuf;

use authenticator::Authenticator;
use authenticator::{Authenticator, Pin};
use virt::{Ctap2Error, Options};
use webauthn::{Rp, User};
use webauthn::{CredentialData, PubKeyCredDescriptor, Rp, User};

struct CredMgmt<'a> {
authenticator: Authenticator<'a, Pin>,
credentials: Vec<(Rp, User, CredentialData)>,
}

impl<'a> CredMgmt<'a> {
fn new(authenticator: Authenticator<'a, Pin>) -> Self {
Self {
authenticator,
credentials: Default::default(),
}
}

fn make_credential(&mut self, rp: Rp, user: User) -> Result<CredentialData, Ctap2Error> {
self.authenticator
.make_credential(rp.clone(), user.clone())
.inspect(|credential_data| {
self.credentials.push((rp, user, credential_data.clone()));
})
}

fn delete_credential(&mut self, id: Vec<u8>) -> Result<(), Ctap2Error> {
self.authenticator.delete_credential(&id);
self.credentials.retain(|(_, _, data)| data.id != id);
Ok(())
}

fn delete_credential_at(&mut self, i: usize) -> Result<(), Ctap2Error> {
assert!(i < self.credentials.len());
let id = self.credentials[i].2.id.clone();
self.delete_credential(id)
}

fn update_user(&mut self, id: Vec<u8>, user: User) -> Result<(), Ctap2Error> {
self.authenticator.update_user(&id, user.clone())?;
self.credentials
.iter_mut()
.filter(|(_, _, data)| data.id == id)
.for_each(|cred| cred.1 = user.clone());
Ok(())
}

fn update_user_at(&mut self, i: usize, user: User) -> Result<(), Ctap2Error> {
assert!(i < self.credentials.len());
let id = self.credentials[i].2.id.clone();
self.update_user(id, user)
}

fn list(&mut self) {
let expected_rp_ids = self.rp_ids();
let actual_rps = self.authenticator.list_rps();
let actual_rp_ids: BTreeSet<_> = actual_rps.iter().map(|rp| rp.id.clone()).collect();
assert_eq!(expected_rp_ids, actual_rp_ids);
// TODO: check other RP fields than ID

for rp_id in expected_rp_ids {
assert!(actual_rps.iter().any(|rp| rp.id == rp_id));
let expected_credentials = self.credentials(&rp_id);
let actual_credentials = self.authenticator.list_credentials(&rp_id);
let actual_credentials: BTreeSet<_> = actual_credentials.into_iter().collect();
assert_eq!(expected_credentials, actual_credentials);
}
}

fn rp_ids(&self) -> BTreeSet<String> {
self.credentials
.iter()
.map(|(rp, _, _)| rp.id.clone())
.collect()
}

fn credentials(&self, rp_id: &str) -> BTreeSet<(User, PubKeyCredDescriptor)> {
self.credentials
.iter()
.filter(|(rp, _, _)| rp.id == rp_id)
.map(|(_, user, data)| {
(
user.clone(),
PubKeyCredDescriptor::new("public-key", data.id.clone()),
)
})
.collect()
}
}

fn generate_rp(i: usize) -> Rp {
// TODO: set other fields than id
let rp_id = format!("rp{i}");
Rp::new(rp_id)
}

fn generate_user(i: u8) -> User {
// TODO: set other fields than id
let mut user = Vec::from(b"john.doe");
user.push(i);
User::new(user)
}

#[test]
fn test_list_credentials() {
virt::run_ctap2(|device| {
let mut authenticator = Authenticator::new(device).set_pin(b"123456");
let mut credentials: BTreeMap<_, _> = (0..10)
.map(|i| {
// TODO: set other fields than id
let rp_id = format!("rp{i}");
let user = b"john.doe";
authenticator
.make_credential(Rp::new(rp_id.clone()), User::new(user))
.unwrap();
(rp_id, user)
})
.collect();
let authenticator = Authenticator::new(device).set_pin(b"123456");
let mut cred_mgmt = CredMgmt::new(authenticator);
for i in 0..10 {
let rp = generate_rp(i);
let user = generate_user(0);
cred_mgmt.make_credential(rp, user).unwrap();
}

let rps = authenticator.list_rps();
assert_eq!(rps.len(), 10);
for rp in &rps {
assert_eq!(rp.name, None);
let expected = credentials.remove(&rp.id).unwrap();
cred_mgmt.list();
})
}

let mut credentials = authenticator.list_credentials(&rp.id);
assert_eq!(credentials.len(), 1);
let actual = credentials.pop().unwrap();
#[test]
fn test_list_credentials_multi() {
virt::run_ctap2(|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();
}
}

cred_mgmt.list();
})
}

assert_eq!(actual.id, expected);
assert_eq!(actual.name, None);
assert_eq!(actual.display_name, None);
#[test]
fn test_list_credentials_delete() {
virt::run_ctap2(|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();
}
}

// deletes the only credential for rp2
cred_mgmt.delete_credential_at(4).unwrap();
// deletes one of three credentials for rp1
cred_mgmt.delete_credential_at(2).unwrap();

cred_mgmt.list();
})
}

#[test]
fn test_list_credentials_update_user() {
virt::run_ctap2(|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();
}
}
assert!(credentials.is_empty());

// case 1: updates the only credential for rp2

// changing the user ID fails
let user = generate_user(98);
assert_eq!(cred_mgmt.update_user_at(4, user), Err(Ctap2Error(0x02)));

cred_mgmt.list();

// setting the display name works
let mut user = generate_user(0);
user.display_name = Some("John Doe".into());
cred_mgmt.update_user_at(4, user).unwrap();

cred_mgmt.list();

// case 2: updates one of three credentials for rp1

// changing the user ID fails
let user = generate_user(99);
assert_eq!(cred_mgmt.update_user_at(2, user), Err(Ctap2Error(0x02)));

cred_mgmt.list();

// setting the display name works
let mut user = generate_user(1);
user.display_name = Some("John Doe".into());
cred_mgmt.update_user_at(2, user).unwrap();

cred_mgmt.list();
})
}

Expand Down Expand Up @@ -137,3 +300,44 @@ fn test_filesystem_full() {
assert_eq!(metadata.remaining, 0);
})
}

#[test]
fn test_filesystem_full_update_user() {
let mut options = Options {
max_resident_credential_count: Some(10),
..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);

let mut i = 0;
loop {
let rp = generate_rp(i);
let user = generate_user(0);
let result = cred_mgmt.make_credential(rp, user);

if result == Err(Ctap2Error(0x28)) {
break;
}
result.unwrap();

i += 1;
}

cred_mgmt.list();

// filesystem is now full, we cannot create new credentials
// but: we still want to be able to update existing credentials
let mut user = generate_user(0);
user.display_name = Some("John Doe".into());
cred_mgmt.update_user_at(1, user).unwrap();

cred_mgmt.list();
})
}
Loading
Loading