webauthn_authenticator_rs/nfc/
mod.rs

1//! [NFCTransport] communicates with a FIDO authenticator using the PC/SC API.
2//!
3//! ## Transport
4//!
5//! The CTAP specifications describe an "ISO 7816, ISO 14443 and Near Field
6//! Communication" transport, but PC/SC supports both contact (ISO 7816-3) and
7//! contactless (ISO 14443 and others) smart card interfaces.
8//!
9//! For consistency with other implementations (Windows) and the existing
10//! WebAuthn specification, this library calls them all "NFC", even though
11//! interface entirely operates on an ISO 7816 level. This module should work
12//! with FIDO tokens regardless of physical transport.
13//!
14//! Some tokens (like Yubikey) provide a USB CCID (smart card) interface for
15//! other applets (such as PGP and PIV), but do not typically expose the FIDO
16//! applet over the same interface due to the possibility of websites bypassing
17//! the WebAuthn API on ChromeOS by using WebUSB[^1].
18//!
19//! [^1]: [ChromeOS' PC/SC implementation][2] is pcsclite running in a browser
20//! extension, which accesses USB CCID interfaces via WebUSB. By comparison,
21//! other platforms' PC/SC implementations take exclusive control of USB CCID
22//! devices outside of the browser, preventing access from WebUSB.
23//!
24//! [2]: https://github.com/GoogleChromeLabs/chromeos_smart_card_connector/blob/main/docs/index-developer.md
25//!
26//! ## Windows
27//!
28//! ### Windows 10 WebAuthn API
29//!
30//! Windows' WebAuthn API (on Windows 10 build 1903 and later) blocks
31//! non-Administrator access to **all** NFC FIDO tokens, throwing an error
32//! whenever an application attempts to select the FIDO applet, even if it is
33//! not present!
34//!
35//! Use [Win10][crate::win10::Win10] (available with the `win10` feature) on
36//! Windows instead.
37//!
38//! ### Smart card service
39//!
40//! By default, Windows runs the [Smart Card service][3] (`SCardSvr`) in
41//! "Manual (Triggered)" start-up mode. Rather than starting the service on
42//! boot, Windows will wait for an application to use the PC/SC API.
43//!
44//! However, Windows *does not* automatically start `SCardSvr` if:
45//!
46//! * there has *never* been a smart card reader (or other CCID interface, such
47//!   as a Yubikey) connected to the PC
48//!
49//! * the user has explicitly disabled the service (in `services.msc`)
50//!
51//! Instead, Windows returns an error ([`NoService`][4]) when establishing a
52//! context (in [`NFCTransport::new()`]).
53//!
54//! [`AnyTransport`] will ignore unavailability of `SCardSvr`, as it is presumed
55//! that PC/SC is one of many potentially-available transports.
56//!
57//! [3]: https://learn.microsoft.com/en-us/windows/security/identity-protection/smart-cards/smart-card-smart-cards-for-windows-service
58//! [4]: pcsc::Error::NoService
59use crate::ctap2::commands::to_short_apdus;
60use crate::error::{CtapError, WebauthnCError};
61use crate::ui::UiCallback;
62
63#[cfg(doc)]
64use crate::stubs::*;
65
66use async_trait::async_trait;
67use futures::executor::block_on;
68use futures::{stream::BoxStream, Stream};
69use tokio::sync::mpsc;
70use tokio::task::spawn_blocking;
71use tokio_stream::wrappers::ReceiverStream;
72
73use pcsc::*;
74use std::ffi::{CStr, CString};
75use std::fmt;
76use std::ops::Deref;
77use std::pin::Pin;
78use std::sync::Mutex;
79use std::time::Duration;
80use webauthn_rs_proto::AuthenticatorTransport;
81
82mod atr;
83mod tlv;
84
85pub use self::atr::*;
86use crate::transport::iso7816::*;
87use crate::transport::*;
88
89/// Version string for a token which supports CTAP v1 / U2F (`U2F_V2`)
90pub const APPLET_U2F_V2: [u8; 6] = [0x55, 0x32, 0x46, 0x5f, 0x56, 0x32];
91/// Version string for a token which only supports CTAP v2 (`FIDO_2_0`)
92pub const APPLET_FIDO_2_0: [u8; 8] = [0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30];
93/// ISO 7816 FIDO applet name
94pub const APPLET_DF: [u8; 8] = [
95    /* RID */ 0xA0, 0x00, 0x00, 0x06, 0x47, /* PIX */ 0x2F, 0x00, 0x01,
96];
97
98/// List of strings, which if they appear in a PC/SC card reader's name,
99/// indicate we should ignore it.
100///
101/// **See:** [`ignored_reader()`]
102const IGNORED_READERS: [&str; 3] = [
103    // Trussed (used by Nitrokey 3 and SoloKeys Solo 2) expose a USB CCID
104    // interface which allows U2F applet selection, but then returns 0x6985
105    // (conditions of use not satisfied) to every command sent thereafter:
106    // https://github.com/trussed-dev/fido-authenticator/blob/7bd0c3bc5105a122fa11d9b354457746f391c4fb/src/dispatch/apdu.rs#L44-L48
107    // https://github.com/trussed-dev/fido-authenticator/issues/38
108    "Nitrokey", "SoloKey",
109    // YubiKey exposes a CCID interface when OpenGPG or PIV support is enabled,
110    // and this interface doesn't support FIDO.
111    "YubiKey",
112];
113
114/// Is the PC/SC card reader one which should be ignored?
115///
116/// Some USB security keys expose a USB CCID interface, which either don't
117/// expose a FIDO applet (as recommended by [non-public FIDO documentation][0])
118/// or is unusable.
119///
120/// Don't waste time enumerating these!
121///
122/// If `reader_name` does not contain valid UTF-8, this returns `false`.
123///
124/// [0]: https://github.com/w3c/webauthn/issues/1835#issuecomment-1382660217
125fn ignored_reader(reader_name: &CStr) -> bool {
126    let reader_name = match reader_name.as_ref().to_str() {
127        Ok(r) => r,
128        Err(e) => {
129            error!("could not convert {reader_name:?} to UTF-8: {e:?}");
130            return false;
131        }
132    };
133
134    let r = IGNORED_READERS.iter().any(|i| reader_name.contains(i));
135
136    #[cfg(feature = "nfc_allow_ignored_readers")]
137    if r {
138        warn!("allowing ignored reader: {reader_name:?}");
139        return false;
140    }
141
142    r
143}
144
145struct NFCDeviceWatcher {
146    stream: ReceiverStream<TokenEvent<NFCCard>>,
147}
148
149impl NFCDeviceWatcher {
150    fn new(ctx: Context) -> Self {
151        let (tx, rx) = mpsc::channel(16);
152        let stream = ReceiverStream::from(rx);
153
154        spawn_blocking(move || {
155            let mut enumeration_complete = false;
156            let mut reader_states: Vec<ReaderState> =
157                vec![ReaderState::new(PNP_NOTIFICATION(), State::UNAWARE)];
158
159            'main: while !tx.is_closed() {
160                // trace!(
161                //     "{} known reader(s), pruning ignored readers",
162                //     reader_states.len()
163                // );
164
165                // Remove all disconnected readers
166                reader_states.retain(|state| {
167                    !state
168                        .event_state()
169                        .intersects(State::UNKNOWN | State::IGNORE)
170                });
171
172                // trace!("{} reader(s) remain after pruning", reader_states.len());
173
174                // Get a list of readers right now
175                let readers = ctx.list_readers_owned()?;
176                // trace!(
177                //     "{} reader(s) currently connected: {:?}",
178                //     readers.len(),
179                //     readers
180                // );
181
182                if readers.is_empty() && !enumeration_complete {
183                    // When there are no real readers connected (ie: other than
184                    // PNP_NOTIFICATION), get_status_change() waits for either
185                    // a reader to be connected, or timeout (1 second)... which
186                    // is quite slow.
187                    //
188                    // When there are real reader(s), get_status_change
189                    // immediately reports status change(s) for anything in the
190                    // UNAWARE state.
191                    enumeration_complete = true;
192                    if tx.blocking_send(TokenEvent::EnumerationComplete).is_err() {
193                        // Channel lost!
194                        break 'main;
195                    }
196                }
197
198                // Add any new readers to the list
199                for reader_name in readers {
200                    if !reader_states
201                        .iter()
202                        .any(|s| s.name() == reader_name.as_ref())
203                    {
204                        // We still need to keep track of ignored readers, so
205                        // that we don't get spurious PNP_NOTIFICATION events
206                        // for them.
207                        trace!(
208                            "New reader: {reader_name:?} {}",
209                            if ignored_reader(&reader_name) {
210                                "(ignored)"
211                            } else {
212                                ""
213                            }
214                        );
215                        reader_states.push(ReaderState::new(reader_name, State::UNAWARE));
216                    }
217                }
218
219                // Update view of current states
220                // trace!("Updating {} reader states", reader_states.len());
221                for state in &mut reader_states {
222                    state.sync_current_state();
223                }
224
225                // Wait for further changes...
226                let r = ctx.get_status_change(Duration::from_secs(1), &mut reader_states);
227
228                if let Err(e) = r {
229                    use pcsc::Error::*;
230                    match e {
231                        Timeout | UnknownReader => {
232                            continue;
233                        }
234
235                        e => {
236                            error!("while watching for PC/SC status changes: {e:?}");
237                            r?;
238                        }
239                    }
240                }
241
242                // trace!("Updated reader states");
243                let mut tasks = Vec::new();
244                for state in &reader_states {
245                    if state.name() == PNP_NOTIFICATION()
246                        || !state.event_state().contains(State::CHANGED)
247                        || ignored_reader(state.name())
248                    {
249                        continue;
250                    }
251                    trace!(
252                        "Reader {:?} current_state: {:?}, event_state: {:?}",
253                        state.name(),
254                        state.current_state(),
255                        state.event_state()
256                    );
257
258                    if state
259                        .event_state()
260                        .intersects(State::INUSE | State::EXCLUSIVE)
261                    {
262                        // TODO: The card could have been captured by something
263                        // else, and we try again later.
264                        trace!("ignoring in-use card");
265                        continue;
266                    }
267
268                    if state.event_state().contains(State::PRESENT)
269                        && !state.current_state().contains(State::PRESENT)
270                    {
271                        if let Ok(mut card) = NFCCard::new(&ctx, state.name(), state.atr()) {
272                            let tx = tx.clone();
273                            tasks.push(tokio::spawn(async move {
274                                match card.init().await {
275                                    Ok(()) => {
276                                        let _ = tx.send(TokenEvent::Added(card)).await;
277                                    }
278                                    Err(e) => {
279                                        error!("initialising card: {e:?}");
280                                    }
281                                };
282                            }));
283                        }
284                    } else if state.event_state().contains(State::EMPTY)
285                        && !state.current_state().contains(State::EMPTY)
286                    {
287                        if tx
288                            .blocking_send(TokenEvent::Removed(state.name().to_owned()))
289                            .is_err()
290                        {
291                            // Channel lost!
292                            break 'main;
293                        }
294                    } else {
295                        // Unhandled state transition
296                    }
297                }
298
299                if !enumeration_complete {
300                    // This condition is hit when there was at least one real
301                    // reader connected on the first loop (which was in the
302                    // UNAWARE state).
303                    enumeration_complete = true;
304
305                    // Wait for outstanding tasks which could signal "added"
306                    // before EnumerationCompleted.
307                    for task in tasks {
308                        let _ = block_on(task);
309                    }
310
311                    if tx.blocking_send(TokenEvent::EnumerationComplete).is_err() {
312                        // Channel lost!
313                        break 'main;
314                    }
315                }
316            }
317
318            Ok::<(), WebauthnCError>(())
319        });
320
321        Self { stream }
322    }
323}
324
325impl Stream for NFCDeviceWatcher {
326    type Item = TokenEvent<NFCCard>;
327
328    fn poll_next(
329        self: Pin<&mut Self>,
330        cx: &mut std::task::Context<'_>,
331    ) -> std::task::Poll<Option<Self::Item>> {
332        ReceiverStream::poll_next(Pin::new(&mut Pin::get_mut(self).stream), cx)
333    }
334}
335
336/// Wrapper for PC/SC context
337pub struct NFCTransport {
338    ctx: Context,
339}
340
341// Connection to a single NFC card
342pub struct NFCCard {
343    card: Mutex<Card>,
344    reader_name: CString,
345    pub atr: Atr,
346    initialised: bool,
347}
348
349impl fmt::Debug for NFCTransport {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        f.debug_struct("NFCTransport").finish()
352    }
353}
354
355impl fmt::Debug for NFCCard {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        f.debug_struct("NFCCard")
358            .field("reader_name", &self.reader_name)
359            .field("atr", &self.atr)
360            .field("initialised", &self.initialised)
361            .finish()
362    }
363}
364
365impl NFCTransport {
366    /// Creates a new [NFCTransport] instance in a given [Scope].
367    ///
368    /// Example:
369    ///
370    /// ```no_run
371    /// # #[cfg(feature = "nfc")]
372    /// use pcsc::Scope;
373    /// # #[cfg(feature = "nfc")]
374    /// use webauthn_authenticator_rs::nfc::NFCTransport;
375    ///
376    /// # #[cfg(feature = "nfc")]
377    /// let reader = NFCTransport::new(Scope::User);
378    /// // TODO: Handle errors
379    /// ```
380    ///
381    /// This returns an error [if the smart card service is unavailable][0].
382    ///
383    /// [0]: crate::nfc#smart-card-service
384    pub fn new(scope: Scope) -> Result<Self, WebauthnCError> {
385        Ok(NFCTransport {
386            ctx: Context::establish(scope).map_err(WebauthnCError::PcscError)?,
387        })
388    }
389}
390
391#[async_trait]
392impl Transport<'_> for NFCTransport {
393    type Token = NFCCard;
394
395    async fn watch(&self) -> Result<BoxStream<TokenEvent<Self::Token>>, WebauthnCError> {
396        let watcher = NFCDeviceWatcher::new(self.ctx.clone());
397
398        Ok(Box::pin(watcher))
399    }
400
401    async fn tokens(&self) -> Result<Vec<Self::Token>, WebauthnCError> {
402        let mut r = Vec::new();
403        {
404            let readers = self.ctx.list_readers_owned()?;
405            let mut reader_states: Vec<ReaderState> = readers
406                .into_iter()
407                .map(|n| ReaderState::new(n, State::UNAWARE))
408                .collect();
409            self.ctx
410                .get_status_change(Duration::from_secs(1), &mut reader_states)?;
411
412            for state in reader_states.iter() {
413                if !state.event_state().contains(State::PRESENT) {
414                    continue;
415                }
416
417                let c = match NFCCard::new(&self.ctx, state.name(), state.atr()) {
418                    Err(_) => continue,
419                    Ok(c) => c,
420                };
421
422                r.push(c);
423            }
424        }
425
426        let mut i = 0;
427        while i < r.len() {
428            match r[i].init().await {
429                Err(e) => {
430                    error!("init card: {e:?}");
431                    r.remove(i);
432                }
433                Ok(()) => {
434                    i += 1;
435                }
436            }
437        }
438
439        Ok(r)
440    }
441}
442
443/// Transmits a single ISO 7816-4 APDU to the card.
444fn transmit(
445    card: &Card,
446    request: &ISO7816RequestAPDU,
447    form: &ISO7816LengthForm,
448) -> Result<ISO7816ResponseAPDU, WebauthnCError> {
449    let req = request.to_bytes(form).map_err(|e| {
450        error!("Failed to build APDU command: {:?}", e);
451        WebauthnCError::ApduConstruction
452    })?;
453    let mut resp = vec![0; MAX_BUFFER_SIZE_EXTENDED];
454
455    trace!(">>> {}", hex::encode(&req));
456
457    let rapdu = card.transmit(&req, &mut resp).inspect_err(|err| {
458        error!("Failed to transmit APDU command to card: {}", err);
459    })?;
460
461    trace!("<<< {}", hex::encode(rapdu));
462
463    ISO7816ResponseAPDU::try_from(rapdu).map_err(|e| {
464        error!("Failed to parse card response: {:?}", e);
465        WebauthnCError::ApduTransmission
466    })
467}
468/// Transmit multiple chunks of data to the card, and handle a chunked
469/// response. All requests must be transmittable in short form.
470pub fn transmit_chunks(
471    card: &Card,
472    requests: &[ISO7816RequestAPDU],
473) -> Result<ISO7816ResponseAPDU, WebauthnCError> {
474    let mut r = EMPTY_RESPONSE;
475
476    for chunk in requests {
477        r = transmit(card, chunk, &ISO7816LengthForm::ShortOnly)?;
478        if !r.is_success() {
479            return Err(WebauthnCError::ApduTransmission);
480        }
481    }
482
483    if r.ctap_needs_get_response() {
484        error!("NFCCTAP_GETRESPONSE not supported, but token sent it");
485        return Err(WebauthnCError::ApduTransmission);
486    }
487
488    if r.bytes_available() == 0 {
489        return Ok(r);
490    }
491
492    let mut response_data = Vec::new();
493    response_data.extend_from_slice(&r.data);
494
495    while r.bytes_available() > 0 {
496        r = transmit(
497            card,
498            &get_response(0x80, r.bytes_available()),
499            &ISO7816LengthForm::ShortOnly,
500        )?;
501        if !r.is_success() {
502            return Err(WebauthnCError::ApduTransmission);
503        }
504        response_data.extend_from_slice(&r.data);
505    }
506
507    r.data = response_data;
508    Ok(r)
509}
510
511const DESELECT_APPLET: ISO7816RequestAPDU = ISO7816RequestAPDU {
512    cla: 0x80,
513    ins: 0x12,
514    p1: 0x01,
515    p2: 0x00,
516    data: vec![],
517    ne: 256,
518};
519
520impl NFCCard {
521    pub fn new(ctx: &Context, reader_name: &CStr, atr: &[u8]) -> Result<NFCCard, WebauthnCError> {
522        trace!("ATR: {}", hex::encode(atr));
523        let atr = Atr::try_from(atr)?;
524        trace!("Parsed: {:?}", &atr);
525        trace!("issuer data: {:?}", atr.card_issuers_data_str());
526
527        if atr.storage_card {
528            return Err(WebauthnCError::StorageCard);
529        }
530
531        let card = ctx
532            .connect(reader_name, ShareMode::Exclusive, Protocols::ANY)
533            .inspect_err(|err| {
534                error!("Error connecting to card: {:?}", err);
535            })?;
536
537        Ok(NFCCard {
538            card: Mutex::new(card),
539            reader_name: reader_name.to_owned(),
540            atr,
541            initialised: false,
542        })
543    }
544
545    #[cfg(feature = "nfc_raw_transmit")]
546    /// Transmits a single ISO 7816-4 APDU to the card.
547    ///
548    /// This API is only intended for conformance testing.
549    pub fn transmit(
550        &self,
551        request: &ISO7816RequestAPDU,
552        form: &ISO7816LengthForm,
553    ) -> Result<ISO7816ResponseAPDU, WebauthnCError> {
554        let guard = self.card.lock()?;
555        transmit(guard.deref(), request, form)
556    }
557
558    /// Gets the name of the card reader being used to communicate with this
559    /// token.
560    pub fn reader_name(&self) -> Option<&str> {
561        self.reader_name.to_str().ok()
562    }
563}
564
565#[async_trait]
566impl Token for NFCCard {
567    type Id = CString;
568
569    fn has_button(&self) -> bool {
570        false
571    }
572
573    async fn transmit_raw<U>(&mut self, cmd: &[u8], _ui: &U) -> Result<Vec<u8>, WebauthnCError>
574    where
575        U: UiCallback,
576    {
577        if !self.initialised {
578            error!("attempted to transmit to uninitialised card");
579            return Err(WebauthnCError::Internal);
580        }
581        // let apdu = cmd.to_extended_apdu().map_err(|_| WebauthnCError::Cbor)?;
582        // let mut resp = self.transmit(&apdu, &ISO7816LengthForm::ExtendedOnly)?;
583
584        // while resp.ctap_needs_get_response() {
585        //     // TODO: sleep here, add retry limit?
586        //     info!("Needs GetResponse");
587
588        //     resp = self.transmit(&NFCCTAP_GETRESPONSE, &ISO7816LengthForm::ExtendedOnly)?;
589        // };
590        let apdus = to_short_apdus(cmd);
591        let guard = self.card.lock()?;
592        let resp = transmit_chunks(guard.deref(), &apdus)?;
593        let mut data = resp.data;
594        let err = CtapError::from(data.remove(0));
595        if !err.is_ok() {
596            return Err(err.into());
597        }
598
599        Ok(data)
600    }
601
602    /// Initialises the connected FIDO token.
603    ///
604    /// ## Platform-specific issues
605    ///
606    /// ### Windows
607    ///
608    /// This may fail with "permission denied" on Windows 10 build 1903 or
609    /// later, unless the program is run as Administrator.
610    async fn init(&mut self) -> Result<(), WebauthnCError> {
611        if self.initialised {
612            warn!("attempted to init an already-initialised card");
613            return Ok(());
614        } else {
615            // FIXME: macOS likes to drop in on our **exclusive** connection with a
616            // SELECT(a0 00 00 03 08 00 00 10 00 01 00) (for the PIV applet). This
617            // seems to confuse some cards.
618            //
619            // So, lets wait a moment for it to butt in.
620            tokio::time::sleep(Duration::from_millis(500)).await;
621        }
622
623        let guard = self.card.lock()?;
624        let resp = transmit(
625            guard.deref(),
626            &select_by_df_name(&APPLET_DF),
627            &ISO7816LengthForm::ShortOnly,
628        )?;
629
630        if !resp.is_ok() {
631            error!("Error selecting applet: {:02x} {:02x}", resp.sw1, resp.sw2);
632            return Err(WebauthnCError::NotSupported);
633        }
634
635        if resp.data != APPLET_U2F_V2 && resp.data != APPLET_FIDO_2_0 {
636            error!("Unsupported applet: {:02x?}", &resp.data);
637            return Err(WebauthnCError::NotSupported);
638        }
639
640        self.initialised = true;
641        Ok(())
642    }
643
644    async fn close(&mut self) -> Result<(), WebauthnCError> {
645        if !self.initialised {
646            // Card wasn't initialised, but close() may be called
647            // unconditionally.
648            return Ok(());
649        }
650
651        let guard = self.card.lock()?;
652        let resp = transmit(
653            guard.deref(),
654            &DESELECT_APPLET,
655            &ISO7816LengthForm::ShortOnly,
656        )?;
657
658        if !resp.is_ok() {
659            Err(WebauthnCError::ApduTransmission)
660        } else {
661            Ok(())
662        }
663    }
664
665    fn get_transport(&self) -> AuthenticatorTransport {
666        AuthenticatorTransport::Nfc
667    }
668
669    async fn cancel(&mut self) -> Result<(), WebauthnCError> {
670        // There does not appear to be a "cancel" command over NFC.
671        Ok(())
672    }
673}
674
675#[cfg(test)]
676mod test {
677    use super::*;
678
679    #[test]
680    fn ignored_readers() -> Result<(), Box<dyn std::error::Error>> {
681        let _ = tracing_subscriber::fmt().try_init();
682
683        // CCID interfaces on tokens
684        const IGNORED: [&[u8]; 4] = [
685            b"Nitrokey Nitrokey 3",
686            b"Nitrokey Nitrokey 3 [CCID/ICCD Interface] 00 00",
687            b"SoloKeys Solo 2",
688            b"Yubico YubiKey FIDO+CCID",
689        ];
690
691        // Smartcard readers
692        const ALLOWED: [&[u8]; 6] = [
693            b"ACS ACR122U 00 00",
694            b"ACS ACR122U 01 00",
695            b"ACS ACR122U PICC Interface",
696            b"ACS ACR123 3S Reader [ACR123U-PICC] (1.00.xx) 00 00",
697            // Invalid UTF-8 should be allowed, even if it contains ignored
698            // reader names.
699            b"\xFF\xFF",
700            b"\xFFYubico YubiKey FIDO+CCID\xFF",
701        ];
702
703        for n in IGNORED {
704            let e = String::from_utf8(n.to_vec()).unwrap_or_else(|_| hex::encode(n));
705            assert!(
706                ignored_reader(&CString::new(n)?),
707                "expected {e} to be ignored"
708            );
709        }
710
711        for n in ALLOWED {
712            let e = String::from_utf8(n.to_vec()).unwrap_or_else(|_| hex::encode(n));
713            assert!(
714                !ignored_reader(&CString::new(n)?),
715                "expected {e} to be allowed"
716            );
717        }
718
719        Ok(())
720    }
721}