use super::client_data::ClientDataHash;
use super::commands::get_assertion::{GetAssertion, GetAssertionExtensions, GetAssertionOptions};
use super::commands::{CtapResponse, PinUvAuthCommand, RequestCtap1, Retryable};
use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED};
use crate::crypto::PinUvAuthToken;
use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingParty};
use crate::errors::AuthenticatorError;
use crate::transport::errors::{ApduErrorStatus, HIDError};
use crate::transport::{FidoDevice, FidoProtocol, VirtualFidoDevice};
use crate::u2ftypes::CTAP1RequestAPDU;
use sha2::{Digest, Sha256};
#[derive(Debug)]
pub struct CheckKeyHandle<'assertion> {
pub key_handle: &'assertion [u8],
pub client_data_hash: &'assertion [u8],
pub rp: &'assertion RelyingParty,
}
type EmptyResponse = ();
impl CtapResponse for EmptyResponse {}
impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> {
type Output = EmptyResponse;
type AdditionalInfo = ();
fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> {
let flags = U2F_CHECK_IS_REGISTERED;
let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len());
auth_data.extend_from_slice(self.client_data_hash);
auth_data.extend_from_slice(self.rp.hash().as_ref());
auth_data.extend_from_slice(&[self.key_handle.len() as u8]);
auth_data.extend_from_slice(self.key_handle);
let cmd = U2F_AUTHENTICATE;
let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?;
Ok((apdu, ()))
}
fn handle_response_ctap1<Dev: FidoDevice>(
&self,
_dev: &mut Dev,
status: Result<(), ApduErrorStatus>,
_input: &[u8],
_add_info: &Self::AdditionalInfo,
) -> Result<Self::Output, Retryable<HIDError>> {
match status {
Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()),
Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))),
}
}
fn send_to_virtual_device<Dev: VirtualFidoDevice>(
&self,
dev: &mut Dev,
) -> Result<Self::Output, HIDError> {
dev.check_key_handle(self)
}
}
pub(crate) fn do_credential_list_filtering_ctap1<Dev: FidoDevice>(
dev: &mut Dev,
cred_list: &[PublicKeyCredentialDescriptor],
rp: &RelyingParty,
client_data_hash: &ClientDataHash,
) -> Option<PublicKeyCredentialDescriptor> {
let key_handle = cred_list
.iter()
.filter(|key_handle| key_handle.id.len() < 256)
.find_map(|key_handle| {
let check_command = CheckKeyHandle {
key_handle: key_handle.id.as_ref(),
client_data_hash: client_data_hash.as_ref(),
rp,
};
let res = dev.send_ctap1(&check_command);
match res {
Ok(_) => Some(key_handle.clone()),
_ => None,
}
});
key_handle
}
pub(crate) fn do_credential_list_filtering_ctap2<Dev: FidoDevice>(
dev: &mut Dev,
cred_list: &[PublicKeyCredentialDescriptor],
rp: &RelyingParty,
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<Vec<PublicKeyCredentialDescriptor>, AuthenticatorError> {
let info = dev
.get_authenticator_info()
.ok_or(HIDError::DeviceNotInitialized)?;
let mut cred_list = cred_list.to_vec();
let mut chunk_size = match info.max_credential_count_in_list {
None | Some(0) => 1,
Some(x) => x,
};
match info.max_credential_id_length {
None => { }
Some(0) => {
chunk_size = 1;
}
Some(max_key_length) => {
cred_list.retain(|k| k.id.len() <= max_key_length);
}
}
let chunked_list = cred_list.chunks(chunk_size);
let mut final_list = Vec::new();
for chunk in chunked_list {
let mut silent_assert = GetAssertion::new(
ClientDataHash(Sha256::digest("").into()),
rp.clone(),
chunk.to_vec(),
GetAssertionOptions {
user_verification: None, user_presence: Some(false),
},
GetAssertionExtensions::default(),
);
silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?;
match dev.send_msg(&silent_assert) {
Ok(mut response) => {
let credential_ids = response
.iter_mut()
.filter_map(|result| {
if chunk.len() == 1 && result.assertion.credentials.is_none() {
Some(chunk[0].clone())
} else {
result.assertion.credentials.take()
}
})
.collect();
final_list = credential_ids;
break;
}
Err(_) => {
continue;
}
}
}
Ok(final_list)
}
pub(crate) fn silently_discover_credentials<Dev: FidoDevice>(
dev: &mut Dev,
cred_list: &[PublicKeyCredentialDescriptor],
rp: &RelyingParty,
client_data_hash: &ClientDataHash,
) -> Vec<PublicKeyCredentialDescriptor> {
if dev.get_protocol() == FidoProtocol::CTAP2 {
if let Ok(cred_list) = do_credential_list_filtering_ctap2(dev, cred_list, rp, None) {
return cred_list;
}
} else if let Some(key_handle) =
do_credential_list_filtering_ctap1(dev, cred_list, rp, client_data_hash)
{
return vec![key_handle];
}
vec![]
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::{
crypto::{COSEAlgorithm, COSEEC2Key, COSEKey, COSEKeyType, Curve},
ctap2::{
attestation::{
AAGuid, AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags,
Extension,
},
commands::{CommandError, StatusCode},
server::{AuthenticationExtensionsClientOutputs, AuthenticatorAttachment, Transport},
},
transport::{
device_selector::tests::{make_device_simple_u2f, make_device_with_pin},
hid::HIDDevice,
platform::device::Device,
},
Assertion, GetAssertionResult,
};
fn new_relying_party(name: &str) -> RelyingParty {
RelyingParty {
id: String::from(name),
name: Some(String::from(name)),
}
}
fn new_silent_assert(
rp: &RelyingParty,
allow_list: &[PublicKeyCredentialDescriptor],
) -> GetAssertion {
GetAssertion::new(
ClientDataHash(Sha256::digest("").into()),
rp.clone(),
allow_list.to_vec(),
GetAssertionOptions {
user_verification: None, user_presence: Some(false),
},
GetAssertionExtensions::default(),
)
}
fn new_credential(fill: u8, repeat: usize) -> PublicKeyCredentialDescriptor {
PublicKeyCredentialDescriptor {
id: vec![fill; repeat],
transports: vec![Transport::USB],
}
}
fn new_assertion_response(
rp: &RelyingParty,
cred: Option<&PublicKeyCredentialDescriptor>,
) -> GetAssertionResult {
let credential_data = cred.map(|cred| AttestedCredentialData {
aaguid: AAGuid::default(),
credential_id: cred.id.clone(),
credential_public_key: COSEKey {
alg: COSEAlgorithm::RS256,
key: COSEKeyType::EC2(COSEEC2Key {
curve: Curve::SECP256R1,
x: vec![],
y: vec![],
}),
},
});
GetAssertionResult {
assertion: Assertion {
credentials: cred.cloned(),
auth_data: AuthenticatorData {
rp_id_hash: rp.hash(),
flags: AuthenticatorDataFlags::empty(),
counter: 0,
credential_data,
extensions: Extension::default(),
},
signature: vec![],
user: None,
},
attachment: AuthenticatorAttachment::Platform,
extensions: AuthenticationExtensionsClientOutputs::default(),
}
}
fn new_check_key_handle<'a>(
rp: &'a RelyingParty,
client_data_hash: &'a ClientDataHash,
cred: &'a PublicKeyCredentialDescriptor,
) -> CheckKeyHandle<'a> {
CheckKeyHandle {
key_handle: cred.id.as_ref(),
client_data_hash: client_data_hash.as_ref(),
rp,
}
}
#[test]
fn test_preflight_ctap1_empty() {
let mut dev = Device::new("preflight").unwrap();
make_device_simple_u2f(&mut dev);
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let rp = new_relying_party("preflight test");
let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash);
assert!(res.is_empty());
}
#[test]
fn test_preflight_ctap1_multiple_replies() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_simple_u2f(&mut dev);
let rp = new_relying_party("preflight test");
let cdh = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![
new_credential(4, 4),
new_credential(3, 4),
new_credential(2, 4),
new_credential(1, 4),
];
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[0]));
dev.add_upcoming_ctap_error(HIDError::ApduStatus(
ApduErrorStatus::WrongData, ));
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1]));
dev.add_upcoming_ctap_error(HIDError::ApduStatus(
ApduErrorStatus::WrongData, ));
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2]));
dev.add_upcoming_ctap_response(());
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh);
assert_eq!(res, vec![allow_list[2].clone()]);
}
#[test]
fn test_preflight_ctap1_too_long_entries() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_simple_u2f(&mut dev);
let rp = new_relying_party("preflight test");
let cdh = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![
new_credential(4, 300), new_credential(3, 4),
new_credential(2, 4),
new_credential(1, 4),
];
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[1]));
dev.add_upcoming_ctap_error(HIDError::ApduStatus(
ApduErrorStatus::WrongData, ));
dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2]));
dev.add_upcoming_ctap_response(());
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh);
assert_eq!(res, vec![allow_list[2].clone()]);
}
#[test]
fn test_preflight_ctap2_empty() {
let mut dev = Device::new("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash);
assert!(res.is_empty());
}
#[test]
fn test_preflight_ctap20_no_cred_data() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![new_credential(1, 4)];
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, None)]);
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
assert_eq!(res, allow_list);
}
#[test]
fn test_preflight_ctap2_one_valid_entry() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![new_credential(1, 4)];
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[0]))]);
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
assert_eq!(res, allow_list);
}
#[test]
fn test_preflight_ctap2_multiple_entries() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![
new_credential(3, 4),
new_credential(2, 4),
new_credential(1, 4),
new_credential(0, 4),
];
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[0].clone()]));
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[1].clone()]));
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &[allow_list[2].clone()]));
dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode(
StatusCode::NoCredentials,
None,
)));
dev.add_upcoming_ctap_error(HIDError::Command(CommandError::StatusCode(
StatusCode::NoCredentials,
None,
)));
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[2]))]);
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
assert_eq!(res, vec![allow_list[2].clone()]);
}
#[test]
fn test_preflight_ctap2_multiple_replies() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![
new_credential(4, 4),
new_credential(3, 4),
new_credential(2, 4),
new_credential(1, 4),
];
let mut info = dev.get_authenticator_info().unwrap().clone();
info.max_credential_count_in_list = Some(5);
dev.set_authenticator_info(info);
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
dev.add_upcoming_ctap_response(vec![
new_assertion_response(&rp, Some(&allow_list[1])),
new_assertion_response(&rp, Some(&allow_list[2])),
new_assertion_response(&rp, Some(&allow_list[3])),
]);
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
assert_eq!(res, allow_list[1..].to_vec());
}
#[test]
fn test_preflight_ctap2_multiple_replies_some_invalid() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![
new_credential(4, 4),
new_credential(3, 4),
new_credential(2, 4),
new_credential(1, 4),
];
let mut info = dev.get_authenticator_info().unwrap().clone();
info.max_credential_count_in_list = Some(5);
dev.set_authenticator_info(info);
dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list));
dev.add_upcoming_ctap_response(vec![
new_assertion_response(&rp, Some(&allow_list[1])),
new_assertion_response(&rp, None), new_assertion_response(&rp, Some(&allow_list[2])),
new_assertion_response(&rp, None), ]);
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
assert_eq!(res, allow_list[1..=2].to_vec());
}
#[test]
fn test_preflight_ctap2_too_long_entries() {
let mut dev = Device::new_skipping_serialization("preflight").unwrap();
make_device_with_pin(&mut dev);
let rp = new_relying_party("preflight test");
let client_data_hash = ClientDataHash(Sha256::digest("").into());
let allow_list = vec![
new_credential(4, 50), new_credential(3, 4),
new_credential(2, 50), new_credential(1, 4),
];
let mut info = dev.get_authenticator_info().unwrap().clone();
info.max_credential_count_in_list = Some(5);
info.max_credential_id_length = Some(20);
dev.set_authenticator_info(info);
dev.add_upcoming_ctap2_request(&new_silent_assert(
&rp,
&[allow_list[1].clone(), allow_list[3].clone()],
));
dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[1]))]);
let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash);
assert_eq!(res, vec![allow_list[1].clone()]);
}
}