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}