Revision control
Copy as Markdown
Other Tools
use crate::{
crypto::{PinUvAuthParam, PinUvAuthToken},
ctap2::server::UserVerificationRequirement,
errors::AuthenticatorError,
transport::errors::HIDError,
AuthenticatorInfo, FidoDevice,
};
use serde::{
de::{Error as SerdeError, IgnoredAny, MapAccess, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use serde_bytes::ByteBuf;
use serde_cbor::{from_slice, to_vec, Value};
use std::fmt;
use super::{Command, CommandError, CtapResponse, PinUvAuthCommand, RequestCtap2, StatusCode};
#[derive(Debug, Clone, Copy)]
pub enum BioEnrollmentModality {
Fingerprint,
Other(u8),
}
impl From<u8> for BioEnrollmentModality {
fn from(value: u8) -> Self {
match value {
0x01 => BioEnrollmentModality::Fingerprint,
x => BioEnrollmentModality::Other(x),
}
}
}
impl From<BioEnrollmentModality> for u8 {
fn from(value: BioEnrollmentModality) -> Self {
match value {
BioEnrollmentModality::Fingerprint => 0x01,
BioEnrollmentModality::Other(x) => x,
}
}
}
impl Serialize for BioEnrollmentModality {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u8((*self).into())
}
}
impl<'de> Deserialize<'de> for BioEnrollmentModality {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct BioEnrollmentModalityVisitor;
impl<'de> Visitor<'de> for BioEnrollmentModalityVisitor {
type Value = BioEnrollmentModality;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer")
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: SerdeError,
{
Ok(BioEnrollmentModality::from(v))
}
}
deserializer.deserialize_u8(BioEnrollmentModalityVisitor)
}
}
pub type BioTemplateId = Vec<u8>;
#[derive(Debug, Clone, Deserialize, Default)]
struct BioEnrollmentParams {
template_id: Option<BioTemplateId>, // Template Identifier.
template_friendly_name: Option<String>, // Template Friendly Name.
timeout_milliseconds: Option<u64>, // Timeout in milliSeconds.
}
impl BioEnrollmentParams {
fn has_some(&self) -> bool {
self.template_id.is_some()
|| self.template_friendly_name.is_some()
|| self.timeout_milliseconds.is_some()
}
}
impl Serialize for BioEnrollmentParams {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_map_optional!(
serializer,
&0x01 => self.template_id.as_deref().map(ByteBuf::from),
&0x02 => &self.template_friendly_name,
&0x03 => self.timeout_milliseconds,
)
}
}
#[derive(Debug)]
pub enum BioEnrollmentCommand {
EnrollBegin(Option<u64>),
EnrollCaptureNextSample((BioTemplateId, Option<u64>)),
CancelCurrentEnrollment,
EnumerateEnrollments,
SetFriendlyName((BioTemplateId, String)),
RemoveEnrollment(BioTemplateId),
GetFingerprintSensorInfo,
}
impl BioEnrollmentCommand {
fn to_id_and_param(&self) -> (u8, BioEnrollmentParams) {
let mut params = BioEnrollmentParams::default();
match &self {
BioEnrollmentCommand::EnrollBegin(timeout) => {
params.timeout_milliseconds = *timeout;
(0x01, params)
}
BioEnrollmentCommand::EnrollCaptureNextSample((id, timeout)) => {
params.template_id = Some(id.clone());
params.timeout_milliseconds = *timeout;
(0x02, params)
}
BioEnrollmentCommand::CancelCurrentEnrollment => (0x03, params),
BioEnrollmentCommand::EnumerateEnrollments => (0x04, params),
BioEnrollmentCommand::SetFriendlyName((id, name)) => {
params.template_id = Some(id.clone());
params.template_friendly_name = Some(name.clone());
(0x05, params)
}
BioEnrollmentCommand::RemoveEnrollment(id) => {
params.template_id = Some(id.clone());
(0x06, params)
}
BioEnrollmentCommand::GetFingerprintSensorInfo => (0x07, params),
}
}
}
#[derive(Debug)]
pub struct BioEnrollment {
/// The user verification modality being requested
modality: BioEnrollmentModality,
/// The authenticator user verification sub command currently being requested
pub(crate) subcommand: BioEnrollmentCommand,
/// First 16 bytes of HMAC-SHA-256 of contents using pinUvAuthToken.
pin_uv_auth_param: Option<PinUvAuthParam>,
use_legacy_preview: bool,
}
impl BioEnrollment {
pub(crate) fn new(subcommand: BioEnrollmentCommand, use_legacy_preview: bool) -> Self {
Self {
modality: BioEnrollmentModality::Fingerprint, // As per spec: Currently always "Fingerprint"
subcommand,
pin_uv_auth_param: None,
use_legacy_preview,
}
}
}
impl Serialize for BioEnrollment {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let (id, params) = self.subcommand.to_id_and_param();
serialize_map_optional!(
serializer,
&0x01 => Some(&self.modality), // Per spec currently always Fingerprint
&0x02 => Some(&id),
&0x03 => params.has_some().then_some(¶ms),
&0x04 => self.pin_uv_auth_param.as_ref().map(|p| p.pin_protocol.id()),
&0x05 => &self.pin_uv_auth_param,
)
}
}
impl PinUvAuthCommand for BioEnrollment {
fn get_rp_id(&self) -> Option<&String> {
None
}
fn set_pin_uv_auth_param(
&mut self,
pin_uv_auth_token: Option<PinUvAuthToken>,
) -> Result<(), AuthenticatorError> {
let mut param = None;
if let Some(token) = pin_uv_auth_token {
// pinUvAuthParam (0x04): the result of calling
// authenticate(pinUvAuthToken, fingerprint (0x01) || uint8(subCommand) || subCommandParams).
let (id, params) = self.subcommand.to_id_and_param();
let modality = self.modality.into();
let mut data = vec![modality, id];
if params.has_some() {
data.extend(to_vec(¶ms).map_err(CommandError::Serializing)?);
}
param = Some(token.derive(&data).map_err(CommandError::Crypto)?);
}
self.pin_uv_auth_param = param;
Ok(())
}
fn can_skip_user_verification(
&mut self,
_info: &crate::AuthenticatorInfo,
_uv: UserVerificationRequirement,
) -> bool {
// "discouraged" does not exist for BioEnrollment
false
}
fn set_uv_option(&mut self, _uv: Option<bool>) {
/* No-op */
}
fn get_pin_uv_auth_param(&self) -> Option<&PinUvAuthParam> {
self.pin_uv_auth_param.as_ref()
}
}
impl RequestCtap2 for BioEnrollment {
type Output = BioEnrollmentResponse;
fn command(&self) -> Command {
if self.use_legacy_preview {
Command::BioEnrollmentPreview
} else {
Command::BioEnrollment
}
}
fn wire_format(&self) -> Result<Vec<u8>, HIDError> {
let output = to_vec(&self).map_err(CommandError::Serializing)?;
trace!("client subcommmand: {:04X?}", &output);
Ok(output)
}
fn handle_response_ctap2<Dev>(
&self,
_dev: &mut Dev,
input: &[u8],
) -> Result<Self::Output, HIDError>
where
Dev: FidoDevice,
{
if input.is_empty() {
return Err(CommandError::InputTooSmall.into());
}
let status: StatusCode = input[0].into();
if status.is_ok() {
if input.len() > 1 {
trace!("parsing bio enrollment response data: {:#04X?}", &input);
let bio_enrollment =
from_slice(&input[1..]).map_err(CommandError::Deserializing)?;
Ok(bio_enrollment)
} else {
// Some subcommands return only an OK-status without any data
Ok(BioEnrollmentResponse::default())
}
} else {
let data: Option<Value> = if input.len() > 1 {
Some(from_slice(&input[1..]).map_err(CommandError::Deserializing)?)
} else {
None
};
Err(CommandError::StatusCode(status, data).into())
}
}
fn send_to_virtual_device<Dev: crate::VirtualFidoDevice>(
&self,
_dev: &mut Dev,
) -> Result<Self::Output, HIDError> {
unimplemented!()
}
}
#[derive(Debug, Copy, Clone, Serialize)]
pub enum LastEnrollmentSampleStatus {
/// Good fingerprint capture.
Ctap2EnrollFeedbackFpGood,
/// Fingerprint was too high.
Ctap2EnrollFeedbackFpTooHigh,
/// Fingerprint was too low.
Ctap2EnrollFeedbackFpTooLow,
/// Fingerprint was too left.
Ctap2EnrollFeedbackFpTooLeft,
/// Fingerprint was too right.
Ctap2EnrollFeedbackFpTooRight,
/// Fingerprint was too fast.
Ctap2EnrollFeedbackFpTooFast,
/// Fingerprint was too slow.
Ctap2EnrollFeedbackFpTooSlow,
/// Fingerprint was of poor quality.
Ctap2EnrollFeedbackFpPoorQuality,
/// Fingerprint was too skewed.
Ctap2EnrollFeedbackFpTooSkewed,
/// Fingerprint was too short.
Ctap2EnrollFeedbackFpTooShort,
/// Merge failure of the capture.
Ctap2EnrollFeedbackFpMergeFailure,
/// Fingerprint already exists.
Ctap2EnrollFeedbackFpExists,
/// (this error number is available)
Unused,
/// User did not touch/swipe the authenticator.
Ctap2EnrollFeedbackNoUserActivity,
/// User did not lift the finger off the sensor.
Ctap2EnrollFeedbackNoUserPresenceTransition,
/// Other possible failure cases that are not (yet) defined by the spec
Ctap2EnrollFeedbackOther(u8),
}
impl<'de> Deserialize<'de> for LastEnrollmentSampleStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct LastEnrollmentSampleStatusVisitor;
impl<'de> Visitor<'de> for LastEnrollmentSampleStatusVisitor {
type Value = LastEnrollmentSampleStatus;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer")
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: SerdeError,
{
match v {
0x00 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpGood),
0x01 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooHigh),
0x02 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLow),
0x03 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooLeft),
0x04 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooRight),
0x05 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooFast),
0x06 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSlow),
0x07 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpPoorQuality),
0x08 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooSkewed),
0x09 => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpTooShort),
0x0A => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpMergeFailure),
0x0B => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackFpExists),
0x0C => Ok(LastEnrollmentSampleStatus::Unused),
0x0D => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserActivity),
0x0E => {
Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackNoUserPresenceTransition)
}
x => Ok(LastEnrollmentSampleStatus::Ctap2EnrollFeedbackOther(x)),
}
}
}
deserializer.deserialize_u8(LastEnrollmentSampleStatusVisitor)
}
}
#[derive(Debug, Copy, Clone, Serialize)]
pub enum FingerprintKind {
TouchSensor,
SwipeSensor,
// Not (yet) defined by the spec
Other(u8),
}
impl<'de> Deserialize<'de> for FingerprintKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct FingerprintKindVisitor;
impl<'de> Visitor<'de> for FingerprintKindVisitor {
type Value = FingerprintKind;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer")
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: SerdeError,
{
match v {
0x01 => Ok(FingerprintKind::TouchSensor),
0x02 => Ok(FingerprintKind::SwipeSensor),
x => Ok(FingerprintKind::Other(x)),
}
}
}
deserializer.deserialize_u8(FingerprintKindVisitor)
}
}
#[derive(Debug, Serialize)]
pub(crate) struct BioTemplateInfo {
template_id: BioTemplateId,
template_friendly_name: Option<String>,
}
impl<'de> Deserialize<'de> for BioTemplateInfo {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct BioTemplateInfoResponseVisitor;
impl<'de> Visitor<'de> for BioTemplateInfoResponseVisitor {
type Value = BioTemplateInfo;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut template_id = None; // (0x01)
let mut template_friendly_name = None; // (0x02)
while let Some(key) = map.next_key()? {
match key {
0x01 => {
if template_id.is_some() {
return Err(SerdeError::duplicate_field("template_id"));
}
template_id = Some(map.next_value::<ByteBuf>()?.into_vec());
}
0x02 => {
if template_friendly_name.is_some() {
return Err(SerdeError::duplicate_field("template_friendly_name"));
}
template_friendly_name = Some(map.next_value()?);
}
k => {
warn!("BioTemplateInfo: unexpected key: {:?}", k);
let _ = map.next_value::<IgnoredAny>()?;
continue;
}
}
}
if let Some(template_id) = template_id {
Ok(BioTemplateInfo {
template_id,
template_friendly_name,
})
} else {
Err(SerdeError::missing_field("template_id"))
}
}
}
deserializer.deserialize_bytes(BioTemplateInfoResponseVisitor)
}
}
#[derive(Default, Debug)]
pub struct BioEnrollmentResponse {
/// The user verification modality.
pub(crate) modality: Option<BioEnrollmentModality>,
/// Indicates the type of fingerprint sensor. For touch type sensor, its value is 1. For swipe type sensor its value is 2.
pub(crate) fingerprint_kind: Option<FingerprintKind>,
/// Indicates the maximum good samples required for enrollment.
pub(crate) max_capture_samples_required_for_enroll: Option<u64>,
/// Template Identifier.
pub(crate) template_id: Option<BioTemplateId>,
/// Last enrollment sample status.
pub(crate) last_enroll_sample_status: Option<LastEnrollmentSampleStatus>,
/// Number of more sample required for enrollment to complete
pub(crate) remaining_samples: Option<u64>,
/// Array of templateInfo’s
pub(crate) template_infos: Vec<BioTemplateInfo>,
/// Indicates the maximum number of bytes the authenticator will accept as a templateFriendlyName.
pub(crate) max_template_friendly_name: Option<u64>,
}
impl CtapResponse for BioEnrollmentResponse {}
impl<'de> Deserialize<'de> for BioEnrollmentResponse {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct BioEnrollmentResponseVisitor;
impl<'de> Visitor<'de> for BioEnrollmentResponseVisitor {
type Value = BioEnrollmentResponse;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut modality = None; // (0x01)
let mut fingerprint_kind = None; // (0x02)
let mut max_capture_samples_required_for_enroll = None; // (0x03)
let mut template_id = None; // (0x04)
let mut last_enroll_sample_status = None; // (0x05)
let mut remaining_samples = None; // (0x06)
let mut template_infos = None; // (0x07)
let mut max_template_friendly_name = None; // (0x08)
while let Some(key) = map.next_key()? {
match key {
0x01 => {
if modality.is_some() {
return Err(SerdeError::duplicate_field("modality"));
}
modality = Some(map.next_value()?);
}
0x02 => {
if fingerprint_kind.is_some() {
return Err(SerdeError::duplicate_field("fingerprint_kind"));
}
fingerprint_kind = Some(map.next_value()?);
}
0x03 => {
if max_capture_samples_required_for_enroll.is_some() {
return Err(SerdeError::duplicate_field(
"max_capture_samples_required_for_enroll",
));
}
max_capture_samples_required_for_enroll = Some(map.next_value()?);
}
0x04 => {
if template_id.is_some() {
return Err(SerdeError::duplicate_field("template_id"));
}
template_id = Some(map.next_value::<ByteBuf>()?.into_vec());
}
0x05 => {
if last_enroll_sample_status.is_some() {
return Err(SerdeError::duplicate_field(
"last_enroll_sample_status",
));
}
last_enroll_sample_status = Some(map.next_value()?);
}
0x06 => {
if remaining_samples.is_some() {
return Err(SerdeError::duplicate_field("remaining_samples"));
}
remaining_samples = Some(map.next_value()?);
}
0x07 => {
if template_infos.is_some() {
return Err(SerdeError::duplicate_field("template_infos"));
}
template_infos = Some(map.next_value()?);
}
0x08 => {
if max_template_friendly_name.is_some() {
return Err(SerdeError::duplicate_field(
"max_template_friendly_name",
));
}
max_template_friendly_name = Some(map.next_value()?);
}
k => {
warn!("BioEnrollmentResponse: unexpected key: {:?}", k);
let _ = map.next_value::<IgnoredAny>()?;
continue;
}
}
}
Ok(BioEnrollmentResponse {
modality,
fingerprint_kind,
max_capture_samples_required_for_enroll,
template_id,
last_enroll_sample_status,
remaining_samples,
template_infos: template_infos.unwrap_or_default(),
max_template_friendly_name,
})
}
}
deserializer.deserialize_bytes(BioEnrollmentResponseVisitor)
}
}
#[derive(Debug, Serialize)]
pub struct EnrollmentInfo {
pub template_id: Vec<u8>,
pub template_friendly_name: Option<String>,
}
impl From<&BioTemplateInfo> for EnrollmentInfo {
fn from(value: &BioTemplateInfo) -> Self {
Self {
template_id: value.template_id.to_vec(),
template_friendly_name: value.template_friendly_name.clone(),
}
}
}
#[derive(Debug, Serialize)]
pub struct FingerprintSensorInfo {
pub fingerprint_kind: FingerprintKind,
pub max_capture_samples_required_for_enroll: u64,
pub max_template_friendly_name: Option<u64>,
}
#[derive(Debug, Serialize)]
pub enum BioEnrollmentResult {
EnrollmentList(Vec<EnrollmentInfo>),
DeleteSuccess(AuthenticatorInfo),
UpdateSuccess,
AddSuccess(AuthenticatorInfo),
FingerprintSensorInfo(FingerprintSensorInfo),
SampleStatus(LastEnrollmentSampleStatus, u64),
}
#[cfg(test)]
mod test {
use crate::{crypto::PinUvAuthParam, ctap2::commands::client_pin::PinUvAuthTokenPermission};
use super::{BioEnrollment, BioEnrollmentCommand, BioEnrollmentModality, BioEnrollmentParams};
#[test]
fn test_serialize_bio_enrollment_params() {
let bio_enrollment_params = BioEnrollmentParams {
template_id: Some(vec![1, 2, 3, 4]),
template_friendly_name: Some("thumb".to_string()),
timeout_milliseconds: Some(1337),
};
let serialized =
serde_cbor::ser::to_vec(&bio_enrollment_params).expect("Failed to serialize to CBOR");
assert_eq!(
serialized,
[
// Value copied from test failure output as regression test snapshot
163, 1, 68, 1, 2, 3, 4, 2, 101, 116, 104, 117, 109, 98, 3, 25, 5, 57
]
);
}
#[test]
fn test_serialize_bio_enrollment() {
let bio_enrollment_params = BioEnrollment {
modality: BioEnrollmentModality::Other(42),
subcommand: BioEnrollmentCommand::SetFriendlyName((
vec![1, 2, 3, 4],
"thumb".to_string(),
)),
pin_uv_auth_param: Some(PinUvAuthParam::create_test(
2,
vec![1, 2, 3, 4],
PinUvAuthTokenPermission::CredentialManagement,
)),
use_legacy_preview: true,
};
let serialized =
serde_cbor::ser::to_vec(&bio_enrollment_params).expect("Failed to serialize to CBOR");
assert_eq!(
serialized,
[
// Value copied from test failure output as regression test snapshot
165, 1, 24, 42, 2, 5, 3, 162, 1, 68, 1, 2, 3, 4, 2, 101, 116, 104, 117, 109, 98, 4,
2, 5, 68, 1, 2, 3, 4
]
);
}
}