webauthn_authenticator_rs/ctap2/mod.rs
1//! This package provides a [CTAP 2.0][Ctap20Authenticator],
2//! [CTAP 2.1-PRE][Ctap21PreAuthenticator] and [CTAP 2.1][Ctap21Authenticator]
3//! protocol implementation on top of [Token], allowing you to interface with
4//! FIDO authenticators.
5//!
6//! The main interface for this package is [CtapAuthenticator].
7//!
8//! ## Warning
9//!
10//! This is "alpha" quality code: it still a work in progress, and missing core
11//! functionality.
12//!
13//! **There are edge cases that which cause you to be locked out of your
14//! authenticator.**
15//!
16//! **The API is not final, and subject to change without warning.**
17//!
18//! ### Known issues
19//!
20//! There are many limitations with this implementation, which are intended to
21//! be addressed in the future:
22//!
23//! * lock-outs aren't handled; this will just use up all your PIN and UV
24//! retries without warning, **potentially locking you out**.
25//!
26//! This also doesn't fall-back to PIN auth if UV (fingerprint) auth is locked
27//! out.
28//!
29//! * multiple authenticators doesn't work particularly well, and connecting
30//! devices while an action is in progress doesn't work
31//!
32//! * cancellations and timeouts
33//!
34//! * session management (re-using `pin_uv_auth_token`)
35//!
36//! * [U2F compatibility and fall-back][u2f]
37//!
38//! * [secured state][secure]
39//!
40//! Many CTAP2 features are unsupported:
41//!
42//! * creating and using [discoverable credentials]
43//!
44//! * [large blobs] (`authenticatorLargeBlobs`)
45//!
46//! * [enterprise attestation]
47//!
48//! * [request extensions]
49//!
50//! ## Features
51//!
52//! * Basic [registration][Ctap20Authenticator::perform_register] and
53//! [authentication][Ctap20Authenticator::perform_auth] with a
54//! [CLI interface][crate::ui::Cli] (or
55//! [implement your own][crate::ui::UiCallback])
56//!
57//! * [Bluetooth Low Energy][crate::bluetooth], [caBLE / Hybrid][crate::cable],
58//! [NFC][crate::nfc] and [USB HID][crate::usb] authenticators
59//!
60//! * CTAP 2.1 and NFC [authenticator selection][select_one_token]
61//!
62//! * Fingerprint (biometric) authentication,
63//! [enrollment and management][BiometricAuthenticator]
64//! (CTAP 2.1 and 2.1-PRE)
65//!
66//! * Built-in user verification
67//!
68//! * [Setting][Ctap20Authenticator::set_new_pin] and
69//! [changing][Ctap20Authenticator::change_pin] device PINs
70//!
71//! * PIN/UV Auth [Protocol One] and [Protocol Two], [getPinToken],
72//! [getPinUvAuthTokenUsingPinWithPermissions], and
73//! [getPinUvAuthTokenUsingUvWithPermissions]
74//!
75//! * [Factory-resetting authenticators][Ctap20Authenticator::factory_reset]
76//!
77//! * configuring [user verification][Ctap21Authenticator::toggle_always_uv]
78//! and [minimum PIN length][Ctap21Authenticator::set_min_pin_length]
79//! requirements
80//!
81//! * [managing discoverable credentials][CredentialManagementAuthenticator]
82//!
83//! ## Examples
84//!
85//! * `webauthn-authenticator-rs/examples/authenticate.rs` works with any
86//! [crate::AuthenticatorBackend], including [CtapAuthenticator].
87//!
88//! * `fido-key-manager` will connect to a key, pull hardware information, and
89//! let you reconfigure the key (reset, PIN, fingerprints, etc.)
90//!
91//! ## Device-specific issues
92//!
93//! * [Some YubiKey USB tokens][yubi] provide a USB CCID (smartcard) interface,
94//! in addition to a USB HID FIDO interface, which will be detected as an
95//! "NFC reader".
96//!
97//! This only provides access to the PIV, OATH or OpenPGP applets, not FIDO.
98//!
99//! Use [USBTransport][crate::usb::USBTransport] for these tokens.
100//!
101//! ## Platform-specific issues
102//!
103//! See `fido-key-manager/README.md`.
104//!
105//! [discoverable credentials]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-discoverable
106//! [enterprise attestation]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#enable-enterprise-attestation
107//! [getPinToken]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getPinToken
108//! [getPinUvAuthTokenUsingPinWithPermissions]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getPinUvAuthTokenUsingPinWithPermissions
109//! [getPinUvAuthTokenUsingUvWithPermissions]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#getPinUvAuthTokenUsingUvWithPermissions
110//! [large blobs]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorLargeBlobs
111//! [PC/SC Lite]: https://pcsclite.apdu.fr/
112//! [Protocol One]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#pinProto1
113//! [Protocol Two]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#pinProto2
114//! [request extensions]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-defined-extensions
115//! [secure]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-secure-interaction
116//! [u2f]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#u2f-interoperability
117//! [yubi]: https://support.yubico.com/hc/en-us/articles/360016614920-YubiKey-USB-ID-Values
118
119// TODO: `commands` may become private in future.
120pub mod commands;
121#[doc(hidden)]
122mod ctap20;
123#[doc(hidden)]
124mod ctap21;
125mod ctap21_bio;
126mod ctap21_cred;
127#[doc(hidden)]
128mod ctap21pre;
129mod internal;
130mod pin_uv;
131#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))]
132#[doc(hidden)]
133mod solokey;
134#[cfg(any(all(doc, not(doctest)), feature = "vendor-yubikey"))]
135#[doc(hidden)]
136mod yubikey;
137
138use std::ops::{Deref, DerefMut};
139use std::pin::Pin;
140
141use futures::stream::{BoxStream, FuturesUnordered};
142use futures::{select, Future, StreamExt};
143
144use crate::authenticator_hashed::AuthenticatorBackendHashedClientData;
145use crate::error::WebauthnCError;
146use crate::transport::{Token, TokenEvent};
147use crate::ui::UiCallback;
148
149use self::{
150 commands::GetInfoRequest, ctap21_bio::BiometricAuthenticatorInfo,
151 ctap21_cred::CredentialManagementAuthenticatorInfo, internal::CtapAuthenticatorVersion,
152};
153
154#[doc(inline)]
155pub use self::{
156 commands::{CBORCommand, CBORResponse, GetInfoResponse},
157 ctap20::Ctap20Authenticator,
158 ctap21::Ctap21Authenticator,
159 ctap21pre::Ctap21PreAuthenticator,
160};
161
162#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
163#[doc(inline)]
164pub use self::{
165 ctap21_bio::BiometricAuthenticator, ctap21_cred::CredentialManagementAuthenticator,
166};
167
168#[cfg(any(all(doc, not(doctest)), feature = "vendor-solokey"))]
169#[doc(inline)]
170pub use self::solokey::SoloKeyAuthenticator;
171
172#[cfg(any(all(doc, not(doctest)), feature = "vendor-yubikey"))]
173#[doc(inline)]
174pub use self::yubikey::YubiKeyAuthenticator;
175
176/// Abstraction for different versions of the CTAP2 protocol.
177///
178/// All tokens can [Deref] into [Ctap20Authenticator].
179#[derive(Debug)]
180pub enum CtapAuthenticator<'a, T: Token, U: UiCallback> {
181 /// Interface for CTAP 2.0 tokens.
182 Fido20(Ctap20Authenticator<'a, T, U>),
183 /// Interface for CTAP 2.1-PRE tokens.
184 Fido21Pre(Ctap21PreAuthenticator<'a, T, U>),
185 /// Interface for CTAP 2.1 tokens.
186 Fido21(Ctap21Authenticator<'a, T, U>),
187}
188
189impl<'a, T: Token, U: UiCallback> CtapAuthenticator<'a, T, U> {
190 /// Initialises the token, and gets a reference to the highest supported FIDO version.
191 ///
192 /// Returns `None` if we don't support any version of CTAP which the token supports.
193 pub async fn new(mut token: T, ui_callback: &'a U) -> Option<CtapAuthenticator<'a, T, U>> {
194 token
195 .init()
196 .await
197 .map_err(|e| {
198 error!("Error initialising token: {e:?}");
199 e
200 })
201 .ok()?;
202 let info = token.transmit(GetInfoRequest {}, ui_callback).await.ok()?;
203
204 Self::new_with_info(info, token, ui_callback)
205 }
206
207 /// Creates a connection to an already-initialized token, and gets a reference to the highest supported FIDO version.
208 ///
209 /// Returns `None` if we don't support any version of CTAP which the token supports.
210 pub(crate) fn new_with_info(
211 info: GetInfoResponse,
212 token: T,
213 ui_callback: &'a U,
214 ) -> Option<CtapAuthenticator<'a, T, U>> {
215 if info
216 .versions
217 .contains(Ctap21Authenticator::<'a, T, U>::VERSION)
218 {
219 Some(Self::Fido21(Ctap21Authenticator::new_with_info(
220 info,
221 token,
222 ui_callback,
223 )))
224 } else if info
225 .versions
226 .contains(Ctap21PreAuthenticator::<'a, T, U>::VERSION)
227 {
228 Some(Self::Fido21Pre(Ctap21PreAuthenticator::new_with_info(
229 info,
230 token,
231 ui_callback,
232 )))
233 } else if info
234 .versions
235 .contains(Ctap20Authenticator::<'a, T, U>::VERSION)
236 {
237 Some(Self::Fido20(Ctap20Authenticator::new_with_info(
238 info,
239 token,
240 ui_callback,
241 )))
242 } else {
243 None
244 }
245 }
246
247 /// Returns `true` if the token supports biometric commands.
248 pub fn supports_biometrics(&self) -> bool {
249 match self {
250 Self::Fido21(a) => a.supports_biometrics(),
251 Self::Fido21Pre(a) => a.supports_biometrics(),
252 _ => false,
253 }
254 }
255
256 /// Returns `true` if the token has configured biometric authentication.
257 pub fn configured_biometrics(&self) -> bool {
258 match self {
259 Self::Fido21(a) => a.configured_biometrics(),
260 Self::Fido21Pre(a) => a.configured_biometrics(),
261 _ => false,
262 }
263 }
264
265 #[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
266 /// Gets a mutable reference to a [BiometricAuthenticator] trait for the
267 /// token, if it supports biometric commands.
268 ///
269 /// Returns `None` if the token does not support biometrics.
270 pub fn bio(&mut self) -> Option<&mut dyn BiometricAuthenticator> {
271 match self {
272 Self::Fido21(a) => a.supports_biometrics().then_some(a),
273 Self::Fido21Pre(a) => a.supports_biometrics().then_some(a),
274 _ => None,
275 }
276 }
277
278 /// Returns `true` if the token supports credential management.
279 pub fn supports_credential_management(&self) -> bool {
280 match self {
281 Self::Fido21(a) => a.supports_credential_management(),
282 Self::Fido21Pre(a) => a.supports_credential_management(),
283 _ => false,
284 }
285 }
286
287 #[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
288 /// Gets a mutable reference to a [CredentialManagementAuthenticator] trait
289 /// for the token, if it supports credential management commands.
290 ///
291 /// Returns `None` if the token does not support credential management.
292 pub fn credential_management(&mut self) -> Option<&mut dyn CredentialManagementAuthenticator> {
293 match self {
294 Self::Fido21(a) => a.supports_credential_management().then_some(a),
295 Self::Fido21Pre(a) => a.supports_credential_management().then_some(a),
296 _ => None,
297 }
298 }
299}
300
301/// Gets a reference to a [CTAP 2.0 compatible interface][Ctap20Authenticator].
302///
303/// All CTAP2 tokens support these base commands.
304impl<'a, T: Token, U: UiCallback> Deref for CtapAuthenticator<'a, T, U> {
305 type Target = Ctap20Authenticator<'a, T, U>;
306
307 fn deref(&self) -> &Self::Target {
308 use CtapAuthenticator::*;
309 match self {
310 Fido20(a) => a,
311 Fido21Pre(a) => a,
312 Fido21(a) => a,
313 }
314 }
315}
316
317/// Gets a mutable reference to a
318/// [CTAP 2.0 compatible interface][Ctap20Authenticator].
319///
320/// All CTAP2 tokens support these base commands.
321impl<T: Token, U: UiCallback> DerefMut for CtapAuthenticator<'_, T, U> {
322 fn deref_mut(&mut self) -> &mut Self::Target {
323 use CtapAuthenticator::*;
324 match self {
325 Fido20(a) => a,
326 Fido21Pre(a) => a,
327 Fido21(a) => a,
328 }
329 }
330}
331
332/// Wrapper for [Ctap20Authenticator]'s implementation of
333/// [AuthenticatorBackendHashedClientData].
334impl<'a, T: Token, U: UiCallback> AuthenticatorBackendHashedClientData
335 for CtapAuthenticator<'a, T, U>
336{
337 fn perform_register(
338 &mut self,
339 client_data_hash: Vec<u8>,
340 options: webauthn_rs_proto::PublicKeyCredentialCreationOptions,
341 timeout_ms: u32,
342 ) -> Result<webauthn_rs_proto::RegisterPublicKeyCredential, WebauthnCError> {
343 <Ctap20Authenticator<'a, T, U> as AuthenticatorBackendHashedClientData>::perform_register(
344 self,
345 client_data_hash,
346 options,
347 timeout_ms,
348 )
349 }
350
351 fn perform_auth(
352 &mut self,
353 client_data_hash: Vec<u8>,
354 options: webauthn_rs_proto::PublicKeyCredentialRequestOptions,
355 timeout_ms: u32,
356 ) -> Result<webauthn_rs_proto::PublicKeyCredential, WebauthnCError> {
357 <Ctap20Authenticator<'a, T, U> as AuthenticatorBackendHashedClientData>::perform_auth(
358 self,
359 client_data_hash,
360 options,
361 timeout_ms,
362 )
363 }
364}
365
366/// Selects one [Token] from an [Iterator] of Tokens.
367///
368/// This only works on NFC authenticators and CTAP 2.1 (not "2.1 PRE")
369/// authenticators.
370pub async fn select_one_token<'a, T: Token + 'a, U: UiCallback + 'a>(
371 tokens: impl Iterator<Item = &'a mut CtapAuthenticator<'a, T, U>>,
372) -> Option<&'a mut CtapAuthenticator<'a, T, U>> {
373 let mut tasks: FuturesUnordered<_> = tokens
374 .map(|token| async move {
375 if !token.token.has_button() {
376 // The token doesn't have a button on a transport level (ie: NFC),
377 // so immediately mark this as the "selected" token, even if it
378 // doesn't support FIDO v2.1.
379 trace!("Token has no button, implicitly treading as selected");
380 Ok::<_, WebauthnCError>(token)
381 } else if let CtapAuthenticator::Fido21(t) = token {
382 t.selection().await?;
383 Ok::<_, WebauthnCError>(token)
384 } else {
385 Err(WebauthnCError::NotSupported)
386 }
387 })
388 .collect();
389
390 let token = loop {
391 select! {
392 res = tasks.select_next_some() => {
393 if let Ok(token) = res {
394 break Some(token);
395 }
396 }
397 complete => {
398 // No tokens available
399 break None;
400 }
401 }
402 };
403
404 tasks.clear();
405 token
406}
407
408/// Selects an authenticator device to use from a [`TokenEvent`] stream.
409///
410/// The first device matching these conditions is returned:
411///
412/// 1. any newly-connected device _after enumeration has completed_
413/// 2. any device without a button (ie: NFC authenticator)
414/// 3. a device which responds to [`Ctap20Authenticator::selection()`]
415pub async fn select_one_device<'a, T: Token + 'a, U: UiCallback + 'a>(
416 stream: BoxStream<'a, TokenEvent<T>>,
417 ui_callback: &'a U,
418) -> Option<CtapAuthenticator<'a, T, U>> {
419 let mut tasks = FuturesUnordered::new();
420 let mut enumerated = false;
421
422 let mut stream = stream.fuse();
423
424 loop {
425 select! {
426 event = stream.select_next_some() => {
427 match event {
428 TokenEvent::EnumerationComplete => {
429 trace!("now enumerated");
430 enumerated = true;
431 },
432 TokenEvent::Added(token) => {
433 trace!("added: {token:?}");
434 let local_enumerated = enumerated;
435 let mut authenticator = if let Some(a) = CtapAuthenticator::new(token, ui_callback).await {
436 a
437 } else {
438 // Couldn't initialise
439 continue;
440 };
441
442 if local_enumerated || !authenticator.token.has_button() {
443 // implicitly choose the new or buttonless device
444 return Some(authenticator);
445 } else {
446 tasks.push(async move {
447 authenticator.selection().await.ok()?;
448 Some(authenticator)
449 });
450 }
451 }
452
453 // Ignore removals
454 TokenEvent::Removed(_) => (),
455 }
456 }
457
458 res = tasks.select_next_some() => {
459 if res.is_some() {
460 return res;
461 }
462 }
463
464 complete => return None,
465 }
466 }
467}
468
469/// Selects an authenticator device to use from a [`TokenEvent`] stream.
470///
471/// The first device matching these conditions is returned:
472///
473/// 1. any newly-connected device _after enumeration has completed_
474/// 2. any device without a button (ie: NFC authenticator)
475/// 3. a device which responds to [`Ctap20Authenticator::selection()`]
476pub async fn select_one_device_predicate<'a, T: Token + 'a, U: UiCallback + 'a>(
477 stream: BoxStream<'a, TokenEvent<T>>,
478 ui_callback: &'a U,
479 predicate: fn(&CtapAuthenticator<'a, T, U>) -> bool,
480) -> Option<CtapAuthenticator<'a, T, U>> {
481 let mut tasks = FuturesUnordered::new();
482 let mut enumerated = false;
483
484 let mut stream = stream.fuse();
485
486 loop {
487 select! {
488 event = stream.select_next_some() => {
489 match event {
490 TokenEvent::EnumerationComplete => {
491 trace!("now enumerated");
492 enumerated = true;
493 },
494 TokenEvent::Added(token) => {
495 trace!("added: {token:?}");
496 let local_enumerated = enumerated;
497 let mut authenticator = if let Some(a) = CtapAuthenticator::new(token, ui_callback).await {
498 a
499 } else {
500 // Couldn't initialise
501 continue;
502 };
503
504 if !predicate(&authenticator) {
505 continue;
506 }
507
508 if local_enumerated || !authenticator.token.has_button() {
509 // implicitly choose the new or buttonless device
510 return Some(authenticator);
511 } else {
512 tasks.push(async move {
513 authenticator.selection().await.ok()?;
514 Some(authenticator)
515 });
516 }
517 }
518
519 // Ignore removals
520 TokenEvent::Removed(_) => (),
521 }
522 }
523
524 res = tasks.select_next_some() => {
525 if res.is_some() {
526 return res;
527 }
528 }
529
530 complete => return None,
531 }
532 }
533}
534
535/// Selects an authenticator device to use from a [`TokenEvent`] stream, using
536/// a specific CTAP version.
537///
538/// The first device matching these conditions is returned:
539///
540/// 1. any newly-connected device _after enumeration has completed_
541/// 2. any device without a button (ie: NFC authenticator)
542/// 3. a device which responds to [`Ctap20Authenticator::selection()`]
543pub async fn select_one_device_version<
544 'a,
545 C: CtapAuthenticatorVersion<'a, T, U> + DerefMut<Target = Ctap20Authenticator<'a, T, U>>,
546 T: Token + 'a,
547 U: UiCallback + 'a,
548>(
549 stream: BoxStream<'a, TokenEvent<T>>,
550 ui_callback: &'a U,
551 predicate: fn(&C) -> bool,
552) -> Option<C> {
553 let mut tasks: FuturesUnordered<Pin<Box<dyn Future<Output = Option<C>>>>> =
554 FuturesUnordered::new();
555 let mut enumerated = false;
556
557 let mut stream = stream.fuse();
558
559 loop {
560 select! {
561 event = stream.select_next_some() => {
562 match event {
563 TokenEvent::EnumerationComplete => {
564 trace!("now enumerated");
565 enumerated = true;
566 },
567 TokenEvent::Added(mut token) => {
568 trace!("added: {token:?}");
569 let local_enumerated = enumerated;
570
571 if let Err(e) = token.init().await {
572 error!("Error initialising token: {e:?}");
573 continue;
574 };
575
576 let info = match token.transmit(GetInfoRequest {}, ui_callback).await {
577 Ok(i) => i,
578 Err(e) => {
579 error!("error getting token info: {e:?}");
580 continue;
581 }
582 };
583
584 if !info.versions.contains(C::VERSION) {
585 warn!("token does not support {:?}: {:?}", C::VERSION, info.versions);
586 continue;
587 }
588
589 let mut authenticator = C::new_with_info(info, token, ui_callback);
590 if !predicate(&authenticator) {
591 continue;
592 }
593
594 trace!(?local_enumerated);
595 if local_enumerated {
596 // implicitly choose the new device
597 return Some(authenticator);
598 } else {
599 tasks.push(Box::pin(async move {
600 authenticator.selection().await.ok()?;
601 Some(authenticator)
602 }));
603 }
604 }
605
606 // Ignore removals
607 TokenEvent::Removed(_) => (),
608 }
609 }
610
611 res = tasks.select_next_some() => {
612 if res.is_some() {
613 return res;
614 }
615 }
616
617 complete => return None,
618 }
619 }
620}