webauthn_authenticator_rs/ctap2/
ctap21_bio.rs

1//! CTAP 2.1 Biometrics functionality.
2#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
3use std::{
4    ops::{Deref, DerefMut},
5    time::Duration,
6};
7
8#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
9use async_trait::async_trait;
10#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
11use unicode_normalization::UnicodeNormalization;
12#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
13use webauthn_rs_proto::UserVerificationPolicy;
14
15use crate::ui::UiCallback;
16#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
17use crate::{
18    error::{CtapError, WebauthnCError},
19    transport::Token,
20};
21
22#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
23use super::{
24    commands::{
25        BioEnrollmentRequestTrait, BioEnrollmentResponse, BioSubCommand, Modality, Permissions,
26        TemplateInfo,
27    },
28    ctap20::AuthSession,
29    Ctap20Authenticator,
30};
31
32/// Trait to provide a [BiometricAuthenticator] implementation.
33pub trait BiometricAuthenticatorInfo<U: UiCallback>: Sync + Send {
34    #[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
35    /// Request type for biometric commands.
36    type RequestType: BioEnrollmentRequestTrait;
37
38    /// Checks if the authenticator supports and has configured biometric
39    /// authentication.
40    ///
41    /// # Returns
42    ///
43    /// * `None`: if not supported.
44    /// * `Some(false)`: if supported, but not configured.
45    /// * `Some(true)`: if supported and configured.
46    fn biometrics(&self) -> Option<bool>;
47
48    /// Returns `true` if the authenticator supports biometric authentication.
49    #[inline]
50    fn supports_biometrics(&self) -> bool {
51        self.biometrics().is_some()
52    }
53
54    /// Returns `true` if the authenticator has configured biometric
55    /// authentication.
56    #[inline]
57    fn configured_biometrics(&self) -> bool {
58        self.biometrics().unwrap_or_default()
59    }
60}
61
62#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
63/// Internal support methods for biometric authentication.
64#[async_trait]
65trait BiometricAuthenticatorSupport<T, U, R>
66where
67    T: BiometricAuthenticatorInfo<U, RequestType = R>,
68    U: UiCallback,
69    R: BioEnrollmentRequestTrait,
70{
71    async fn bio(
72        &mut self,
73        sub_command: BioSubCommand,
74    ) -> Result<BioEnrollmentResponse, WebauthnCError>;
75
76    /// Send a [BioSubCommand] using a provided `pin_uv_auth_token` session.
77    async fn bio_with_session(
78        &mut self,
79        sub_command: BioSubCommand,
80        auth_session: &AuthSession,
81    ) -> Result<BioEnrollmentResponse, WebauthnCError>;
82}
83
84#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
85#[async_trait]
86impl<'a, K, T, U, R> BiometricAuthenticatorSupport<T, U, R> for T
87where
88    K: Token,
89    T: BiometricAuthenticatorInfo<U, RequestType = R>
90        + Deref<Target = Ctap20Authenticator<'a, K, U>>
91        + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
92    U: UiCallback + 'a,
93    R: BioEnrollmentRequestTrait,
94{
95    async fn bio(
96        &mut self,
97        sub_command: BioSubCommand,
98    ) -> Result<BioEnrollmentResponse, WebauthnCError> {
99        let (pin_uv_auth_proto, pin_uv_auth_param) = self
100            .get_pin_uv_auth_token(
101                sub_command.prf().as_slice(),
102                Permissions::BIO_ENROLLMENT,
103                None,
104                UserVerificationPolicy::Required,
105            )
106            .await?
107            .into_pin_uv_params();
108
109        let ui = self.ui_callback;
110        let r = self
111            .token
112            .transmit(
113                T::RequestType::new(sub_command, pin_uv_auth_proto, pin_uv_auth_param),
114                ui,
115            )
116            .await?;
117        self.refresh_info().await?;
118        Ok(r)
119    }
120
121    async fn bio_with_session(
122        &mut self,
123        sub_command: BioSubCommand,
124        auth_session: &AuthSession,
125    ) -> Result<BioEnrollmentResponse, WebauthnCError> {
126        let client_data_hash = sub_command.prf();
127
128        let (pin_uv_protocol, pin_uv_auth_param) = match auth_session {
129            AuthSession::InterfaceToken(iface, pin_token) => {
130                let mut pin_uv_auth_param =
131                    iface.authenticate(pin_token, client_data_hash.as_slice())?;
132                pin_uv_auth_param.truncate(16);
133
134                (Some(iface.get_pin_uv_protocol()), Some(pin_uv_auth_param))
135            }
136
137            _ => (None, None),
138        };
139
140        let ui = self.ui_callback;
141        self.token
142            .transmit(
143                T::RequestType::new(sub_command, pin_uv_protocol, pin_uv_auth_param),
144                ui,
145            )
146            .await
147    }
148}
149
150#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
151/// Biometric management commands for [Ctap21Authenticator][] and
152/// [Ctap21PreAuthenticator][].
153///
154/// [Ctap21Authenticator]: super::Ctap21Authenticator
155/// [Ctap21PreAuthenticator]: super::Ctap21PreAuthenticator
156#[async_trait]
157pub trait BiometricAuthenticator {
158    /// Checks that the device supports fingerprints.
159    ///
160    /// Returns [WebauthnCError::NotSupported] if the token does not support
161    /// fingerprint authentication.
162    async fn check_fingerprint_support(&mut self) -> Result<(), WebauthnCError>;
163
164    /// Checks that a given `friendly_name` complies with authenticator limits,
165    /// and returns the value in Unicode Normal Form C.
166    ///
167    /// Returns [WebauthnCError::FriendlyNameTooLong] if it does not comply with
168    /// limits.
169    async fn check_friendly_name(
170        &mut self,
171        friendly_name: String,
172    ) -> Result<String, WebauthnCError>;
173
174    /// Gets information about the token's fingerprint sensor.
175    ///
176    /// Returns [WebauthnCError::NotSupported] if the token does not support
177    /// fingerprint authentication.
178    async fn get_fingerprint_sensor_info(
179        &mut self,
180    ) -> Result<BioEnrollmentResponse, WebauthnCError>;
181
182    /// Lists all enrolled fingerprints in the device.
183    ///
184    /// Returns an empty [Vec] if no fingerprints have been enrolled.
185    ///
186    /// Returns [WebauthnCError::NotSupported] if the token does not support
187    /// fingerprint authentication.
188    async fn list_fingerprints(&mut self) -> Result<Vec<TemplateInfo>, WebauthnCError>;
189
190    /// Enrolls a fingerprint with the token.
191    ///
192    /// This generally takes multiple user interactions (touches or swipes) of
193    /// the sensor.
194    ///
195    /// If enrollment is successful, returns the fingerprint ID.
196    ///
197    /// Returns [WebauthnCError::NotSupported] if the token does not support
198    /// fingerprint authentication.
199    async fn enroll_fingerprint(
200        &mut self,
201        timeout: Duration,
202        friendly_name: Option<String>,
203    ) -> Result<Vec<u8>, WebauthnCError>;
204
205    /// Renames an enrolled fingerprint.
206    async fn rename_fingerprint(
207        &mut self,
208        id: Vec<u8>,
209        friendly_name: String,
210    ) -> Result<(), WebauthnCError>;
211
212    /// Removes an enrolled fingerprint.
213    async fn remove_fingerprint(&mut self, id: Vec<u8>) -> Result<(), WebauthnCError>;
214
215    /// Removes multiple enrolled fingerprints.
216    ///
217    /// **Warning:** this is not an atomic operation. If any command fails,
218    /// further processing will stop, and the request may be incomplete.
219    /// Call [Self::list_fingerprints] to check what was actually done.
220    async fn remove_fingerprints(&mut self, ids: Vec<Vec<u8>>) -> Result<(), WebauthnCError>;
221}
222
223#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
224/// Implementation of biometric management commands for [Ctap21Authenticator][]
225/// and [Ctap21PreAuthenticator][].
226///
227/// [Ctap21Authenticator]: super::Ctap21Authenticator
228/// [Ctap21PreAuthenticator]: super::Ctap21PreAuthenticator
229#[async_trait]
230impl<'a, K, T, U, R> BiometricAuthenticator for T
231where
232    K: Token,
233    T: BiometricAuthenticatorInfo<U, RequestType = R>
234        + Deref<Target = Ctap20Authenticator<'a, K, U>>
235        + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
236    U: UiCallback + 'a,
237    R: BioEnrollmentRequestTrait,
238{
239    async fn check_fingerprint_support(&mut self) -> Result<(), WebauthnCError> {
240        if !self.supports_biometrics() {
241            return Err(WebauthnCError::NotSupported);
242        }
243
244        let ui = self.ui_callback;
245        let r = self.token.transmit(R::GET_MODALITY, ui).await?;
246        if r.modality != Some(Modality::Fingerprint) {
247            return Err(WebauthnCError::NotSupported);
248        }
249
250        Ok(())
251    }
252
253    async fn check_friendly_name(
254        &mut self,
255        friendly_name: String,
256    ) -> Result<String, WebauthnCError> {
257        let ui = self.ui_callback;
258        let r = self
259            .token
260            .transmit(R::GET_FINGERPRINT_SENSOR_INFO, ui)
261            .await?;
262
263        // Normalise into Normal Form C
264        let friendly_name = friendly_name.nfc().collect::<String>();
265        if friendly_name.len() > r.get_max_template_friendly_name() {
266            return Err(WebauthnCError::FriendlyNameTooLong);
267        }
268
269        Ok(friendly_name)
270    }
271
272    async fn get_fingerprint_sensor_info(
273        &mut self,
274    ) -> Result<BioEnrollmentResponse, WebauthnCError> {
275        self.check_fingerprint_support().await?;
276        let ui = self.ui_callback;
277        self.token
278            .transmit(R::GET_FINGERPRINT_SENSOR_INFO, ui)
279            .await
280    }
281
282    async fn enroll_fingerprint(
283        &mut self,
284        timeout: Duration,
285        friendly_name: Option<String>,
286    ) -> Result<Vec<u8>, WebauthnCError> {
287        self.check_fingerprint_support().await?;
288        let friendly_name = match friendly_name {
289            Some(n) => Some(self.check_friendly_name(n).await?),
290            None => None,
291        };
292
293        let session = self
294            .get_pin_uv_auth_session(
295                Permissions::BIO_ENROLLMENT,
296                None,
297                UserVerificationPolicy::Required,
298            )
299            .await?;
300
301        let mut r = self
302            .bio_with_session(BioSubCommand::FingerprintEnrollBegin(timeout), &session)
303            .await?;
304
305        trace!("began enrollment: {:?}", r);
306        let id = r.template_id.ok_or(WebauthnCError::MissingRequiredField)?;
307
308        let mut remaining_samples = r
309            .remaining_samples
310            .ok_or(WebauthnCError::MissingRequiredField)?;
311        while remaining_samples > 0 {
312            self.ui_callback
313                .fingerprint_enrollment_feedback(remaining_samples, r.last_enroll_sample_status);
314
315            r = self
316                .bio_with_session(
317                    BioSubCommand::FingerprintEnrollCaptureNextSample(id.clone(), timeout),
318                    &session,
319                )
320                .await?;
321
322            remaining_samples = r
323                .remaining_samples
324                .ok_or(WebauthnCError::MissingRequiredField)?;
325        }
326
327        // Now it's enrolled, give it a name.
328        if friendly_name.is_some() {
329            self.bio_with_session(
330                BioSubCommand::FingerprintSetFriendlyName(TemplateInfo {
331                    id: id.clone(),
332                    friendly_name,
333                }),
334                &session,
335            )
336            .await?;
337        }
338
339        // This may have been the first enrolled fingerprint.
340        self.refresh_info().await?;
341
342        Ok(id)
343    }
344
345    async fn list_fingerprints(&mut self) -> Result<Vec<TemplateInfo>, WebauthnCError> {
346        self.check_fingerprint_support().await?;
347        if !self.configured_biometrics() {
348            // Fingerprint authentication is supported, but not configured, ie:
349            // there are no enrolled fingerprints and don't need to ask.
350            //
351            // When there is no PIN or UV auth available, then `bio()` would
352            // throw UserVerificationRequired; so we short-cut this to be nice.
353            trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
354            return Ok(vec![]);
355        }
356
357        // works without authentication if alwaysUv = false?
358        let templates = self
359            .bio(BioSubCommand::FingerprintEnumerateEnrollments)
360            .await;
361
362        match templates {
363            Ok(templates) => Ok(templates.template_infos),
364            Err(e) => {
365                if let WebauthnCError::Ctap(e) = &e {
366                    if matches!(e, CtapError::Ctap2InvalidOption) {
367                        // "If there are no enrollments existing on
368                        // authenticator, it returns CTAP2_ERR_INVALID_OPTION."
369                        // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#enumerateEnrollments
370                        return Ok(vec![]);
371                    }
372                }
373
374                Err(e)
375            }
376        }
377    }
378
379    async fn rename_fingerprint(
380        &mut self,
381        id: Vec<u8>,
382        friendly_name: String,
383    ) -> Result<(), WebauthnCError> {
384        self.check_fingerprint_support().await?;
385        if !self.configured_biometrics() {
386            // "If there are no enrollments existing on authenticator for the
387            // passed templateId, it returns CTAP2_ERR_INVALID_OPTION."
388            trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
389            return Err(CtapError::Ctap2InvalidOption.into());
390        }
391
392        let friendly_name = Some(self.check_friendly_name(friendly_name).await?);
393        self.bio(BioSubCommand::FingerprintSetFriendlyName(TemplateInfo {
394            id,
395            friendly_name,
396        }))
397        .await?;
398        Ok(())
399    }
400
401    async fn remove_fingerprint(&mut self, id: Vec<u8>) -> Result<(), WebauthnCError> {
402        self.check_fingerprint_support().await?;
403        if !self.configured_biometrics() {
404            // "If there are no enrollments existing on authenticator for the
405            // passed templateId, it returns CTAP2_ERR_INVALID_OPTION."
406            trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
407            return Err(CtapError::Ctap2InvalidOption.into());
408        }
409
410        self.bio(BioSubCommand::FingerprintRemoveEnrollment(id))
411            .await?;
412
413        // The previous command could have removed the last enrolled
414        // fingerprint.
415        self.refresh_info().await?;
416        Ok(())
417    }
418
419    async fn remove_fingerprints(&mut self, ids: Vec<Vec<u8>>) -> Result<(), WebauthnCError> {
420        self.check_fingerprint_support().await?;
421        if !self.configured_biometrics() {
422            // "If there are no enrollments existing on authenticator for the
423            // passed templateId, it returns CTAP2_ERR_INVALID_OPTION."
424            trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
425            return Err(CtapError::Ctap2InvalidOption.into());
426        }
427
428        let session = self
429            .get_pin_uv_auth_session(
430                Permissions::BIO_ENROLLMENT,
431                None,
432                UserVerificationPolicy::Required,
433            )
434            .await?;
435
436        for id in ids {
437            self.bio_with_session(BioSubCommand::FingerprintRemoveEnrollment(id), &session)
438                .await?;
439        }
440
441        // The previous command could have removed all enrolled fingerprints.
442        self.refresh_info().await?;
443        Ok(())
444    }
445}