webauthn_authenticator_rs/ctap2/commands/
bio_enrollment.rs

1//! `authenticatorBioEnrollment` commands.
2//!
3//! CTAP 2.1 defines two `authenticatorBioEnrollment` command and response
4//! types, [standard][ctap21] and [prototype][ctap21pre]. Both have the same
5//! parameters.
6//!
7//! In order to provide a consistent API and only have to write this once, the
8//! [bio_struct!][] macro provides all the BioEnrollment request functionality,
9//! and creates two structs:
10//!
11//! * [BioEnrollmentRequest]: [CTAP 2.1 version][ctap21] (`0x09`)
12//! * [PrototypeBioEnrollmentRequest]: [CTAP 2.1-PRE version][ctap21pre] (`0x40`)
13//!
14//! Both implement [BioEnrollmentRequestTrait] for common functionality, and
15//! return [BioEnrollmentResponse] to commands.
16//!
17//! [ctap21]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorBioEnrollment
18//! [ctap21pre]: https://fidoalliance.org/specs/fido2/vendor/BioEnrollmentPrototype.pdf
19use std::{fmt::Debug, time::Duration};
20
21use num_traits::cast::{FromPrimitive, ToPrimitive};
22use serde::{Deserialize, Serialize};
23use serde_cbor_2::Value;
24
25use crate::types::EnrollSampleStatus;
26
27use super::*;
28
29/// Default maximum fingerprint friendly name length, in bytes.
30///
31/// Reference: <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#setFriendlyName>
32const DEFAULT_MAX_FRIENDLY_NAME: usize = 64;
33
34/// Macro to generate [BioEnrollmentRequest] for both CTAP 2.1 and 2.1-PRE.
35macro_rules! bio_struct {
36    (
37        $(#[$outer:meta])*
38        $vis:vis struct $name:ident = $cmd:tt
39    ) => {
40        $(#[$outer])*
41        ///
42        /// Related:
43        ///
44        /// * [BioSubCommand] for dynamically-constructed commands
45        /// * [GET_MODALITY][Self::GET_MODALITY]
46        /// * [GET_FINGERPRINT_SENSOR_INFO][Self::GET_FINGERPRINT_SENSOR_INFO]
47        /// * [FINGERPRINT_CANCEL_CURRENT_ENROLLMENT][Self::FINGERPRINT_CANCEL_CURRENT_ENROLLMENT]
48        ///
49        /// Reference: [CTAP protocol reference][ref]
50        #[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)]
51        #[serde(into = "BTreeMap<u32, Value>")]
52        pub struct $name {
53            modality: Option<Modality>,
54            /// Action being requested (specific to modality)
55            sub_command: Option<u8>,
56            sub_command_params: Option<BTreeMap<Value, Value>>,
57            /// PIN / UV protocol version chosen by the platform
58            pin_uv_protocol: Option<u32>,
59            /// Output of calling "Authenticate" on some context specific to [Self::sub_command]
60            pin_uv_auth_param: Option<Vec<u8>>,
61            /// Gets the supported bio modality for the authenticator.
62            ///
63            /// See [GET_MODALITY][Self::GET_MODALITY].
64            get_modality: bool,
65        }
66
67        impl CBORCommand for $name {
68            const CMD: u8 = $cmd;
69            type Response = BioEnrollmentResponse;
70        }
71
72        impl BioEnrollmentRequestTrait for $name {
73            const GET_MODALITY: Self = Self {
74                get_modality: true,
75                modality: None,
76                sub_command: None,
77                sub_command_params: None,
78                pin_uv_protocol: None,
79                pin_uv_auth_param: None,
80            };
81
82            const GET_FINGERPRINT_SENSOR_INFO: Self = Self {
83                modality: Some(Modality::Fingerprint),
84                sub_command: Some(0x07), // getFingerprintSensorInfo
85                sub_command_params: None,
86                pin_uv_protocol: None,
87                pin_uv_auth_param: None,
88                get_modality: false,
89            };
90
91            const FINGERPRINT_CANCEL_CURRENT_ENROLLMENT: Self =
92                Self {
93                    modality: Some(Modality::Fingerprint),
94                    sub_command: Some(0x03), // cancelCurrentEnrollment
95                    sub_command_params: None,
96                    pin_uv_protocol: None,
97                    pin_uv_auth_param: None,
98                    get_modality: false,
99                };
100
101            fn new(
102                s: BioSubCommand,
103                pin_uv_protocol: Option<u32>,
104                pin_uv_auth_param: Option<Vec<u8>>,
105            ) -> Self {
106                let (modality, sub_command) = (&s).into();
107                let sub_command_params = s.into();
108
109                Self {
110                    modality: Some(modality),
111                    sub_command: Some(sub_command),
112                    sub_command_params,
113                    pin_uv_protocol,
114                    pin_uv_auth_param,
115                    get_modality: false,
116                }
117            }
118        }
119
120        impl From<$name> for BTreeMap<u32, Value> {
121            fn from(value: $name) -> Self {
122                let $name {
123                    modality,
124                    sub_command,
125                    sub_command_params,
126                    pin_uv_protocol,
127                    pin_uv_auth_param,
128                    get_modality,
129                } = value;
130
131                let mut keys = BTreeMap::new();
132
133                modality
134                    .and_then(|v| v.to_i128())
135                    .map(|v| keys.insert(0x01, Value::Integer(v)));
136
137                if let Some(v) = sub_command {
138                    keys.insert(0x02, Value::Integer(v.into()));
139                }
140
141                if let Some(v) = sub_command_params {
142                    keys.insert(0x03, Value::Map(v));
143                }
144
145                if let Some(v) = pin_uv_protocol {
146                    keys.insert(0x04, Value::Integer(v.into()));
147                }
148
149                if let Some(v) = pin_uv_auth_param {
150                    keys.insert(0x05, Value::Bytes(v));
151                }
152
153                if get_modality {
154                    keys.insert(0x06, Value::Bool(true));
155                }
156
157                keys
158            }
159        }
160    };
161}
162
163/// Common functionality for CTAP 2.1 and 2.1-PRE `BioEnrollment` request types.
164pub trait BioEnrollmentRequestTrait: CBORCommand<Response = BioEnrollmentResponse> {
165    /// Command to get the supported biometric modality for the authenticator.
166    ///
167    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getUserVerificationModality>
168    const GET_MODALITY: Self;
169
170    /// Command to get information about the authenticator's fingerprint sensor.
171    ///
172    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getFingerprintSensorInfo>
173    const GET_FINGERPRINT_SENSOR_INFO: Self;
174
175    /// Command to cancel an in-progress fingerprint enrollment.
176    ///
177    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#cancelEnrollment>
178    const FINGERPRINT_CANCEL_CURRENT_ENROLLMENT: Self;
179
180    /// Creates a new [BioEnrollmentRequest] from the given [BioSubCommand].
181    fn new(
182        s: BioSubCommand,
183        pin_uv_protocol: Option<u32>,
184        pin_uv_auth_param: Option<Vec<u8>>,
185    ) -> Self;
186}
187
188/// Metadata about a stored fingerprint.
189#[derive(Deserialize, Debug, Default, PartialEq, Eq, Clone)]
190#[serde(try_from = "BTreeMap<Value, Value>")]
191pub struct TemplateInfo {
192    /// The `template_id` of the fingerprint.
193    pub id: Vec<u8>,
194
195    /// A human-readable name for the fingerprint.
196    pub friendly_name: Option<String>,
197}
198
199impl TryFrom<BTreeMap<Value, Value>> for TemplateInfo {
200    type Error = &'static str;
201    fn try_from(mut raw: BTreeMap<Value, Value>) -> Result<Self, Self::Error> {
202        // trace!(?raw);
203        Ok(Self {
204            id: raw
205                .remove(&Value::Integer(0x01))
206                .and_then(|v| value_to_vec_u8(v, "0x01"))
207                .unwrap_or_default(),
208            friendly_name: raw
209                .remove(&Value::Integer(0x02))
210                .and_then(|v| value_to_string(v, "0x02")),
211        })
212    }
213}
214
215impl From<TemplateInfo> for BTreeMap<Value, Value> {
216    fn from(value: TemplateInfo) -> Self {
217        let TemplateInfo { id, friendly_name } = value;
218
219        let mut keys = BTreeMap::new();
220        keys.insert(Value::Integer(0x01), Value::Bytes(id));
221        friendly_name.map(|v| keys.insert(Value::Integer(0x02), Value::Text(v)));
222
223        keys
224    }
225}
226
227/// Modality for biometric authentication.
228///
229/// Returned in [BioEnrollmentResponse::modality] in response to a
230/// [BioEnrollmentRequestTrait::GET_MODALITY] request.
231#[derive(FromPrimitive, ToPrimitive, Debug, PartialEq, Eq, Clone, Default)]
232#[repr(u8)]
233pub enum Modality {
234    /// Unsupported modality.
235    #[default]
236    Unknown = 0x00,
237
238    /// Fingerprint authentication.
239    Fingerprint = 0x01,
240}
241
242/// The type of fingerprint sensor on the device.
243#[derive(FromPrimitive, ToPrimitive, Debug, PartialEq, Eq, Clone)]
244#[repr(u8)]
245pub enum FingerprintKind {
246    /// A fingerprint sensor which requires placing the finger straight down
247    /// on the sensor.
248    Touch = 0x01,
249
250    /// A fingerprint sensor which requires swiping the finger across the
251    /// sensor.
252    Swipe = 0x02,
253}
254
255/// Wrapper for biometric command types, which can be passed to
256/// [BioEnrollmentRequestTrait::new].
257///
258/// Static commands are declared as constants of [BioEnrollmentRequestTrait], see:
259///
260/// * [GET_MODALITY][BioEnrollmentRequestTrait::GET_MODALITY]
261/// * [GET_FINGERPRINT_SENSOR_INFO][BioEnrollmentRequestTrait::GET_FINGERPRINT_SENSOR_INFO]
262/// * [FINGERPRINT_CANCEL_CURRENT_ENROLLMENT][BioEnrollmentRequestTrait::FINGERPRINT_CANCEL_CURRENT_ENROLLMENT]
263#[derive(Debug, Clone, PartialEq, Eq, Default)]
264pub enum BioSubCommand {
265    #[default]
266    Unknown,
267
268    /// Begins enrollment of a new fingerprint on the device:
269    ///
270    /// * [Duration]: time-out for the operation.
271    ///
272    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#enrollingFingerprint>
273    FingerprintEnrollBegin(/* timeout in milliseconds */ Duration),
274
275    /// Captures another sample of a fingerprint while enrollment is in
276    /// progress:
277    ///
278    /// * [`Vec<u8>`]: `template_id` of the partially-enrolled fingerprint.
279    /// * [`Duration`]: time-out for the operation.
280    ///
281    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#enrollingFingerprint>
282    FingerprintEnrollCaptureNextSample(/* id */ Vec<u8>, /* timeout */ Duration),
283
284    /// Lists all enrolled fingerprints.
285    ///
286    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#enumerateEnrollments>
287    FingerprintEnumerateEnrollments,
288
289    /// Renames or sets the friendly name of an enrolled fingerprint.
290    ///
291    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#setFriendlyName>
292    FingerprintSetFriendlyName(TemplateInfo),
293
294    /// Removes an enrolled fingerprint.
295    ///
296    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#removeEnrollment>
297    FingerprintRemoveEnrollment(/* id */ Vec<u8>),
298}
299
300impl From<&BioSubCommand> for (Modality, u8) {
301    fn from(c: &BioSubCommand) -> Self {
302        use BioSubCommand::*;
303        use Modality::*;
304        match c {
305            BioSubCommand::Unknown => (Modality::Unknown, 0x00),
306            FingerprintEnrollBegin(_) => (Fingerprint, 0x01),
307            FingerprintEnrollCaptureNextSample(_, _) => (Fingerprint, 0x02),
308            FingerprintEnumerateEnrollments => (Fingerprint, 0x04),
309            FingerprintSetFriendlyName(_) => (Fingerprint, 0x05),
310            FingerprintRemoveEnrollment(_) => (Fingerprint, 0x06),
311        }
312    }
313}
314
315impl From<BioSubCommand> for Option<BTreeMap<Value, Value>> {
316    fn from(c: BioSubCommand) -> Self {
317        use BioSubCommand::*;
318        match c {
319            Unknown => None,
320            FingerprintEnrollBegin(timeout) => Some(BTreeMap::from([(
321                Value::Integer(0x03),
322                Value::Integer(timeout.as_millis() as i128),
323            )])),
324            FingerprintEnrollCaptureNextSample(id, timeout) => Some(BTreeMap::from([
325                (Value::Integer(0x01), Value::Bytes(id)),
326                (
327                    Value::Integer(0x03),
328                    Value::Integer(timeout.as_millis() as i128),
329                ),
330            ])),
331            FingerprintEnumerateEnrollments => None,
332            FingerprintSetFriendlyName(t) => Some(t.into()),
333            FingerprintRemoveEnrollment(id) => {
334                Some(BTreeMap::from([(Value::Integer(0x01), Value::Bytes(id))]))
335            }
336        }
337    }
338}
339
340impl BioSubCommand {
341    pub fn prf(&self) -> Vec<u8> {
342        // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#prfValues
343        let (modality, subcommand) = self.into();
344        let sub_command_params: Option<BTreeMap<Value, Value>> = self.to_owned().into();
345
346        let mut o = Vec::new();
347        o.push(modality.to_u8().expect("Could not coerce modality into u8"));
348        o.push(subcommand);
349        if let Some(p) = sub_command_params
350            .as_ref()
351            .and_then(|p| serde_cbor_2::to_vec(p).ok())
352        {
353            o.extend_from_slice(p.as_slice())
354        }
355
356        o
357    }
358}
359
360/// `authenticatorBioEnrollment` response type.
361///
362/// References:
363/// * <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorBioEnrollment>
364/// * <https://fidoalliance.org/specs/fido2/vendor/BioEnrollmentPrototype.pdf>
365#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
366#[serde(try_from = "BTreeMap<u32, Value>")]
367pub struct BioEnrollmentResponse {
368    /// Biometric authentication modality supported by the authenticator.
369    ///
370    /// Returned in response to a
371    /// [BioEnrollmentRequestTrait::GET_MODALITY] request.
372    pub modality: Option<Modality>,
373
374    /// The kind of fingerprint sensor used on the device.
375    ///
376    /// Returned in response to a
377    /// [BioEnrollmentRequestTrait::GET_FINGERPRINT_SENSOR_INFO] request.
378    pub fingerprint_kind: Option<FingerprintKind>,
379
380    /// The maximum number of good fingerprint samples required for enrollment.
381    ///
382    /// Returned in response to a
383    /// [BioEnrollmentRequestTrait::GET_FINGERPRINT_SENSOR_INFO] request.
384    pub max_capture_samples_required_for_enroll: Option<u32>,
385
386    /// The identifier for the fingerprint being enrolled.
387    ///
388    /// Returned in response to a [BioSubCommand::FingerprintEnrollBegin]
389    /// request.
390    pub template_id: Option<Vec<u8>>,
391
392    /// The state of the last collected fingerprint sample.
393    ///
394    /// Returned in response to a [BioSubCommand::FingerprintEnrollBegin] or
395    /// [BioSubCommand::FingerprintEnrollCaptureNextSample] request.
396    pub last_enroll_sample_status: Option<EnrollSampleStatus>,
397
398    /// The number of good fingerprint samples required to complete enrollment.
399    ///
400    /// Returned in response to a [BioSubCommand::FingerprintEnrollBegin] or
401    /// [BioSubCommand::FingerprintEnrollCaptureNextSample] request.
402    pub remaining_samples: Option<u32>,
403
404    /// A list of all enrolled fingerprints on the device.
405    ///
406    /// Returned in response to a
407    /// [BioSubCommand::FingerprintEnumerateEnrollments] request.
408    pub template_infos: Vec<TemplateInfo>,
409
410    /// The maximum length for a [TemplateInfo::friendly_name] used on the
411    /// device.
412    ///
413    /// Returned in response to a
414    /// [BioEnrollmentRequestTrait::GET_FINGERPRINT_SENSOR_INFO] request.
415    ///
416    /// Prefer using the
417    /// [get_max_template_friendly_name()][Self::get_max_template_friendly_name]
418    /// method instead of this field, which also provides a default value if
419    /// this is missing.
420    pub max_template_friendly_name: Option<usize>,
421}
422
423impl BioEnrollmentResponse {
424    /// Gets the maximum template friendly name size in bytes, or the default
425    /// if none is provided.
426    ///
427    /// This value is only valid as a response to
428    /// [BioEnrollmentRequestTrait::GET_FINGERPRINT_SENSOR_INFO].
429    pub fn get_max_template_friendly_name(&self) -> usize {
430        self.max_template_friendly_name
431            .unwrap_or(DEFAULT_MAX_FRIENDLY_NAME)
432    }
433}
434
435impl TryFrom<BTreeMap<u32, Value>> for BioEnrollmentResponse {
436    type Error = &'static str;
437    fn try_from(mut raw: BTreeMap<u32, Value>) -> Result<Self, Self::Error> {
438        trace!(?raw);
439        Ok(Self {
440            modality: raw
441                .remove(&0x01)
442                .and_then(|v| value_to_u32(&v, "0x01"))
443                .and_then(Modality::from_u32),
444            fingerprint_kind: raw
445                .remove(&0x02)
446                .and_then(|v| value_to_u32(&v, "0x02"))
447                .and_then(FingerprintKind::from_u32),
448            max_capture_samples_required_for_enroll: raw
449                .remove(&0x03)
450                .and_then(|v| value_to_u32(&v, "0x03")),
451            template_id: raw.remove(&0x04).and_then(|v| value_to_vec_u8(v, "0x04")),
452            last_enroll_sample_status: raw
453                .remove(&0x05)
454                .and_then(|v| value_to_u32(&v, "0x05"))
455                .and_then(EnrollSampleStatus::from_u32),
456            remaining_samples: raw.remove(&0x06).and_then(|v| value_to_u32(&v, "0x06")),
457            template_infos: raw
458                .remove(&0x07)
459                .and_then(|v| {
460                    if let Value::Array(v) = v {
461                        let mut infos = vec![];
462                        for i in v {
463                            if let Value::Map(i) = i {
464                                if let Ok(i) = TemplateInfo::try_from(i) {
465                                    infos.push(i)
466                                }
467                            }
468                        }
469                        Some(infos)
470                    } else {
471                        None
472                    }
473                })
474                .unwrap_or_default(),
475            max_template_friendly_name: raw
476                .remove(&0x08)
477                .and_then(|v| value_to_u32(&v, "0x08"))
478                .map(|v| v as usize),
479        })
480    }
481}
482
483crate::deserialize_cbor!(BioEnrollmentResponse);
484
485bio_struct! {
486    /// CTAP 2.1 `authenticatorBioEnrollment` command (`0x09`).
487    ///
488    /// [ref]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorBioEnrollment
489    pub struct BioEnrollmentRequest = 0x09
490}
491
492bio_struct! {
493    /// CTAP 2.1-PRE prototype `authenticatorBioEnrollment` command (`0x40`).
494    ///
495    /// [ref]: https://fidoalliance.org/specs/fido2/vendor/BioEnrollmentPrototype.pdf
496    pub struct PrototypeBioEnrollmentRequest = 0x40
497}