use std::time::Duration;
use async_trait::async_trait;
use tracing::{debug, instrument, trace, warn};
use crate::proto::ctap2::cbor::{self, CborRequest};
use crate::proto::ctap2::{Ctap2BioEnrollmentResponse, Ctap2CommandCode};
use crate::transport::Channel;
use crate::unwrap_field;
use crate::webauthn::error::{CtapError, Error, PlatformError};
use super::model::Ctap2ClientPinResponse;
use super::{
Ctap2AuthenticatorConfigRequest, Ctap2BioEnrollmentRequest, Ctap2ClientPinRequest,
Ctap2CredentialManagementRequest, Ctap2CredentialManagementResponse, Ctap2GetAssertionRequest,
Ctap2GetAssertionResponse, Ctap2GetInfoResponse, Ctap2MakeCredentialRequest,
Ctap2MakeCredentialResponse,
};
const TIMEOUT_GET_INFO: Duration = Duration::from_millis(250);
macro_rules! parse_cbor {
($type:ty, $data:expr) => {{
match cbor::from_slice::<$type>($data) {
Ok(f) => f,
Err(e) => {
tracing::error!(
"Failed to parse {} from CBOR-data provided by the device. Parsing error: {:?}",
stringify!($type),
e
);
return Err(Error::Platform(PlatformError::InvalidDeviceResponse));
}
}
}};
}
#[async_trait]
pub trait Ctap2 {
async fn ctap2_get_info(&mut self) -> Result<Ctap2GetInfoResponse, Error>;
async fn ctap2_make_credential(
&mut self,
request: &Ctap2MakeCredentialRequest,
timeout: Duration,
) -> Result<Ctap2MakeCredentialResponse, Error>;
async fn ctap2_client_pin(
&mut self,
request: &Ctap2ClientPinRequest,
timeout: Duration,
) -> Result<Ctap2ClientPinResponse, Error>;
async fn ctap2_get_assertion(
&mut self,
request: &Ctap2GetAssertionRequest,
timeout: Duration,
) -> Result<Ctap2GetAssertionResponse, Error>;
async fn ctap2_get_next_assertion(
&mut self,
timeout: Duration,
) -> Result<Ctap2GetAssertionResponse, Error>;
async fn ctap2_selection(&mut self, timeout: Duration) -> Result<(), Error>;
async fn ctap2_authenticator_config(
&mut self,
request: &Ctap2AuthenticatorConfigRequest,
timeout: Duration,
) -> Result<(), Error>;
async fn ctap2_bio_enrollment(
&mut self,
request: &Ctap2BioEnrollmentRequest,
timeout: Duration,
) -> Result<Ctap2BioEnrollmentResponse, Error>;
async fn ctap2_credential_management(
&mut self,
request: &Ctap2CredentialManagementRequest,
timeout: Duration,
) -> Result<Ctap2CredentialManagementResponse, Error>;
}
#[async_trait]
impl<C> Ctap2 for C
where
C: Channel,
{
#[instrument(skip_all)]
async fn ctap2_get_info(&mut self) -> Result<Ctap2GetInfoResponse, Error> {
let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo);
self.cbor_send(&cbor_request, TIMEOUT_GET_INFO).await?;
let cbor_response = self.cbor_recv(TIMEOUT_GET_INFO).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
let data = unwrap_field!(cbor_response.data);
let ctap_response = parse_cbor!(Ctap2GetInfoResponse, &data);
debug!("CTAP2 GetInfo successful");
trace!(?ctap_response);
Ok(ctap_response)
}
#[instrument(skip_all)]
async fn ctap2_make_credential(
&mut self,
request: &Ctap2MakeCredentialRequest,
timeout: Duration,
) -> Result<Ctap2MakeCredentialResponse, Error> {
trace!(?request);
self.cbor_send(&request.try_into()?, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
let data = unwrap_field!(cbor_response.data);
trace!("MakeCredential: {:?}", data);
let ctap_response = parse_cbor!(Ctap2MakeCredentialResponse, &data);
debug!("CTAP2 MakeCredential successful");
trace!(?ctap_response);
Ok(ctap_response)
}
#[instrument(skip_all)]
async fn ctap2_get_assertion(
&mut self,
request: &Ctap2GetAssertionRequest,
timeout: Duration,
) -> Result<Ctap2GetAssertionResponse, Error> {
trace!(?request);
self.cbor_send(&request.try_into()?, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
let data = unwrap_field!(cbor_response.data);
trace!("GetAssertion: {:?}", data);
let ctap_response = parse_cbor!(Ctap2GetAssertionResponse, &data);
debug!("CTAP2 GetAssertion successful");
trace!(?ctap_response);
Ok(ctap_response)
}
#[instrument(skip_all)]
async fn ctap2_get_next_assertion(
&mut self,
timeout: Duration,
) -> Result<Ctap2GetAssertionResponse, Error> {
debug!("CTAP2 GetNextAssertion request");
let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetNextAssertion);
self.cbor_send(&cbor_request, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
let data = unwrap_field!(cbor_response.data);
let ctap_response = parse_cbor!(Ctap2GetAssertionResponse, &data);
debug!("CTAP2 GetNextAssertion successful");
trace!(?ctap_response);
Ok(ctap_response)
}
#[instrument(skip_all)]
async fn ctap2_selection(&mut self, timeout: Duration) -> Result<(), Error> {
debug!("CTAP2 Authenticator Selection request");
let cbor_request = CborRequest::new(Ctap2CommandCode::AuthenticatorSelection);
self.cbor_send(&cbor_request, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => {
return Ok(());
}
error => {
warn!(?error, "Selection request failed with status code");
return Err(Error::Ctap(error));
}
}
}
#[instrument(skip_all)]
async fn ctap2_client_pin(
&mut self,
request: &Ctap2ClientPinRequest,
timeout: Duration,
) -> Result<Ctap2ClientPinResponse, Error> {
trace!(?request);
self.cbor_send(&request.try_into()?, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
if let Some(data) = cbor_response.data {
let ctap_response = parse_cbor!(Ctap2ClientPinResponse, &data);
debug!("CTAP2 ClientPin successful");
trace!(?ctap_response);
Ok(ctap_response)
} else {
Ok(Ctap2ClientPinResponse::default())
}
}
#[instrument(skip_all)]
async fn ctap2_authenticator_config(
&mut self,
request: &Ctap2AuthenticatorConfigRequest,
timeout: Duration,
) -> Result<(), Error> {
trace!(?request);
self.cbor_send(&request.try_into()?, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => {
return Ok(());
}
error => {
warn!(
?error,
"Authenticator config request failed with status code"
);
return Err(Error::Ctap(error));
}
}
}
#[instrument(skip_all)]
async fn ctap2_bio_enrollment(
&mut self,
request: &Ctap2BioEnrollmentRequest,
timeout: Duration,
) -> Result<Ctap2BioEnrollmentResponse, Error> {
trace!(?request);
self.cbor_send(&request.try_into()?, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
if let Some(data) = cbor_response.data {
let ctap_response = parse_cbor!(Ctap2BioEnrollmentResponse, &data);
debug!("CTAP2 BioEnrollment successful");
trace!(?ctap_response);
Ok(ctap_response)
} else {
Ok(Ctap2BioEnrollmentResponse::default())
}
}
#[instrument(skip_all)]
async fn ctap2_credential_management(
&mut self,
request: &Ctap2CredentialManagementRequest,
timeout: Duration,
) -> Result<Ctap2CredentialManagementResponse, Error> {
trace!(?request);
self.cbor_send(&request.try_into()?, timeout).await?;
let cbor_response = self.cbor_recv(timeout).await?;
match cbor_response.status_code {
CtapError::Ok => (),
error => return Err(Error::Ctap(error)),
};
if let Some(data) = cbor_response.data {
let ctap_response = parse_cbor!(Ctap2CredentialManagementResponse, &data);
debug!("CTAP2 CredentialManagement successful");
trace!(?ctap_response);
Ok(ctap_response)
} else {
Ok(Ctap2CredentialManagementResponse::default())
}
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use serde_bytes::ByteBuf;
use crate::proto::ctap2::cbor::{CborRequest, CborResponse};
use crate::proto::ctap2::model::{
Ctap2AuthenticatorConfigRequest, Ctap2BioEnrollmentRequest, Ctap2ClientPinRequest,
Ctap2CredentialManagementRequest, Ctap2GetAssertionRequest, Ctap2MakeCredentialRequest,
Ctap2PinUvAuthProtocol,
};
use crate::proto::ctap2::Ctap2CommandCode;
use crate::transport::mock::channel::MockChannel;
use crate::webauthn::error::{CtapError, Error};
use super::Ctap2;
const TIMEOUT: Duration = Duration::from_secs(1);
fn error_response(status_code: CtapError) -> CborResponse {
CborResponse {
status_code,
data: None,
}
}
#[tokio::test]
async fn ctap2_get_info_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let expected_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo);
channel.push_command_pair(expected_request, error_response(CtapError::Other));
let result = channel.ctap2_get_info().await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::Other)));
}
#[tokio::test]
async fn ctap2_get_info_tolerates_slow_cbor_send() {
let mut channel = MockChannel::new();
channel.set_pre_send_delay(super::TIMEOUT_GET_INFO + Duration::from_millis(50));
let expected_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetInfo);
channel.push_command_pair(expected_request, error_response(CtapError::Other));
let result = channel.ctap2_get_info().await;
assert_eq!(
result.err(),
Some(Error::Ctap(CtapError::Other)),
"GetInfo must not impose a wall-clock timeout that fires before \
the channel's own cbor_send returns"
);
}
#[tokio::test]
async fn ctap2_make_credential_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let request = Ctap2MakeCredentialRequest::dummy();
let expected_request: CborRequest = (&request).try_into().unwrap();
channel.push_command_pair(expected_request, error_response(CtapError::OperationDenied));
let result = channel.ctap2_make_credential(&request, TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::OperationDenied)));
}
#[tokio::test]
async fn ctap2_get_assertion_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let request = Ctap2GetAssertionRequest {
relying_party_id: "example.org".to_owned(),
client_data_hash: ByteBuf::from(vec![0u8; 32]),
allow: vec![],
extensions: None,
options: None,
pin_auth_param: None,
pin_auth_proto: None,
};
let expected_request: CborRequest = (&request).try_into().unwrap();
channel.push_command_pair(expected_request, error_response(CtapError::NoCredentials));
let result = channel.ctap2_get_assertion(&request, TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::NoCredentials)));
}
#[tokio::test]
async fn ctap2_get_next_assertion_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let expected_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetNextAssertion);
channel.push_command_pair(expected_request, error_response(CtapError::NotAllowed));
let result = channel.ctap2_get_next_assertion(TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::NotAllowed)));
}
#[tokio::test]
async fn ctap2_get_next_assertion_does_not_parse_data_on_error() {
let mut channel = MockChannel::new();
let expected_request = CborRequest::new(Ctap2CommandCode::AuthenticatorGetNextAssertion);
let response = CborResponse {
status_code: CtapError::Other,
data: Some(vec![0xff, 0xff, 0xff, 0xff]),
};
channel.push_command_pair(expected_request, response);
let result = channel.ctap2_get_next_assertion(TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::Other)));
}
#[tokio::test]
async fn ctap2_client_pin_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let request = Ctap2ClientPinRequest::new_get_key_agreement(Ctap2PinUvAuthProtocol::One);
let expected_request: CborRequest = (&request).try_into().unwrap();
channel.push_command_pair(expected_request, error_response(CtapError::PINBlocked));
let result = channel.ctap2_client_pin(&request, TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::PINBlocked)));
}
#[tokio::test]
async fn ctap2_selection_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let expected_request = CborRequest::new(Ctap2CommandCode::AuthenticatorSelection);
channel.push_command_pair(
expected_request,
error_response(CtapError::UserActionTimeout),
);
let result = channel.ctap2_selection(TIMEOUT).await;
assert_eq!(
result.err(),
Some(Error::Ctap(CtapError::UserActionTimeout))
);
}
#[tokio::test]
async fn ctap2_authenticator_config_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let request = Ctap2AuthenticatorConfigRequest::new_toggle_always_uv();
let expected_request: CborRequest = (&request).try_into().unwrap();
channel.push_command_pair(
expected_request,
error_response(CtapError::UnauthorizedPermission),
);
let result = channel.ctap2_authenticator_config(&request, TIMEOUT).await;
assert_eq!(
result.err(),
Some(Error::Ctap(CtapError::UnauthorizedPermission))
);
}
#[tokio::test]
async fn ctap2_bio_enrollment_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let request = Ctap2BioEnrollmentRequest {
modality: None,
subcommand: None,
subcommand_params: None,
protocol: None,
uv_auth_param: None,
get_modality: Some(true),
use_legacy_preview: false,
};
let expected_request: CborRequest = (&request).try_into().unwrap();
channel.push_command_pair(expected_request, error_response(CtapError::InvalidOption));
let result = channel.ctap2_bio_enrollment(&request, TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::InvalidOption)));
}
#[tokio::test]
async fn ctap2_credential_management_propagates_non_ok_status() {
let mut channel = MockChannel::new();
let request = Ctap2CredentialManagementRequest {
subcommand: None,
subcommand_params: None,
protocol: None,
uv_auth_param: None,
use_legacy_preview: false,
};
let expected_request: CborRequest = (&request).try_into().unwrap();
channel.push_command_pair(expected_request, error_response(CtapError::PINRequired));
let result = channel.ctap2_credential_management(&request, TIMEOUT).await;
assert_eq!(result.err(), Some(Error::Ctap(CtapError::PINRequired)));
}
}