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}