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}